mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Fixes user query cache invalidation (#1014)
This commit is contained in:
parent
c2a3e04283
commit
9b2c8ca0a5
@ -1,4 +1,4 @@
|
||||
import { removeQueries } from '../operations/resources'
|
||||
import { invalidateAndRemoveQueries } from '../operations/resources'
|
||||
import api, { setAuthToken, handleApiError } from '../api.js'
|
||||
|
||||
export default async function login(username, password) {
|
||||
@ -9,7 +9,7 @@ export default async function login(username, password) {
|
||||
setAuthToken(response.data.token)
|
||||
// This isn't really neccessary because we remove all private queries after
|
||||
// logout, but we do it to be extra safe.
|
||||
//
|
||||
//
|
||||
// For example, in future versions, users might be able to get to an SPA
|
||||
// login page while there's an active session. This code will prevent data
|
||||
// leaks in such cases.
|
||||
@ -17,7 +17,7 @@ export default async function login(username, password) {
|
||||
// TODO(filip): We are currently removing all the queries, but we should
|
||||
// remove only non-public, user-dependent queries - public queries are
|
||||
// expected not to change in respect to the currently logged in user.
|
||||
await removeQueries()
|
||||
await invalidateAndRemoveQueries()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
|
@ -40,16 +40,15 @@ export function getActiveOptimisticUpdates(queryKey) {
|
||||
return updateHandlers.getUpdateHandlers(queryKey)
|
||||
}
|
||||
|
||||
export async function removeQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
queryClient.removeQueries()
|
||||
}
|
||||
|
||||
export async function invalidateAndRemoveQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
// If we don't reset the queries before removing them, Wasp will stay on
|
||||
// the same page. The user would have to manually refresh the page to "finish"
|
||||
// logging out.
|
||||
// When a query is removed, the `Observer` is removed as well, and the components
|
||||
// that are using the query are not re-rendered. This is why we need to reset
|
||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
||||
queryClient.resetQueries()
|
||||
// If we don't remove the queries after invalidating them, the old query data
|
||||
// remains in the cache, casuing a potential privacy issue.
|
||||
|
@ -354,7 +354,7 @@
|
||||
"file",
|
||||
"web-app/src/operations/resources.js"
|
||||
],
|
||||
"d815397e45efe773908b48de42f21ee22481396c318b21ea215ad6343d3f4170"
|
||||
"8bb71b7d7ef3962c28b9db1554500452e7f91b0c9fd898d5cafcdb7229f6db1e"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -40,16 +40,15 @@ export function getActiveOptimisticUpdates(queryKey) {
|
||||
return updateHandlers.getUpdateHandlers(queryKey)
|
||||
}
|
||||
|
||||
export async function removeQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
queryClient.removeQueries()
|
||||
}
|
||||
|
||||
export async function invalidateAndRemoveQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
// If we don't reset the queries before removing them, Wasp will stay on
|
||||
// the same page. The user would have to manually refresh the page to "finish"
|
||||
// logging out.
|
||||
// When a query is removed, the `Observer` is removed as well, and the components
|
||||
// that are using the query are not re-rendered. This is why we need to reset
|
||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
||||
queryClient.resetQueries()
|
||||
// If we don't remove the queries after invalidating them, the old query data
|
||||
// remains in the cache, casuing a potential privacy issue.
|
||||
|
@ -354,7 +354,7 @@
|
||||
"file",
|
||||
"web-app/src/operations/resources.js"
|
||||
],
|
||||
"d815397e45efe773908b48de42f21ee22481396c318b21ea215ad6343d3f4170"
|
||||
"8bb71b7d7ef3962c28b9db1554500452e7f91b0c9fd898d5cafcdb7229f6db1e"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -40,16 +40,15 @@ export function getActiveOptimisticUpdates(queryKey) {
|
||||
return updateHandlers.getUpdateHandlers(queryKey)
|
||||
}
|
||||
|
||||
export async function removeQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
queryClient.removeQueries()
|
||||
}
|
||||
|
||||
export async function invalidateAndRemoveQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
// If we don't reset the queries before removing them, Wasp will stay on
|
||||
// the same page. The user would have to manually refresh the page to "finish"
|
||||
// logging out.
|
||||
// When a query is removed, the `Observer` is removed as well, and the components
|
||||
// that are using the query are not re-rendered. This is why we need to reset
|
||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
||||
queryClient.resetQueries()
|
||||
// If we don't remove the queries after invalidating them, the old query data
|
||||
// remains in the cache, casuing a potential privacy issue.
|
||||
|
@ -368,7 +368,7 @@
|
||||
"file",
|
||||
"web-app/src/operations/resources.js"
|
||||
],
|
||||
"d815397e45efe773908b48de42f21ee22481396c318b21ea215ad6343d3f4170"
|
||||
"8bb71b7d7ef3962c28b9db1554500452e7f91b0c9fd898d5cafcdb7229f6db1e"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -40,16 +40,15 @@ export function getActiveOptimisticUpdates(queryKey) {
|
||||
return updateHandlers.getUpdateHandlers(queryKey)
|
||||
}
|
||||
|
||||
export async function removeQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
queryClient.removeQueries()
|
||||
}
|
||||
|
||||
export async function invalidateAndRemoveQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
// If we don't reset the queries before removing them, Wasp will stay on
|
||||
// the same page. The user would have to manually refresh the page to "finish"
|
||||
// logging out.
|
||||
// When a query is removed, the `Observer` is removed as well, and the components
|
||||
// that are using the query are not re-rendered. This is why we need to reset
|
||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
||||
queryClient.resetQueries()
|
||||
// If we don't remove the queries after invalidating them, the old query data
|
||||
// remains in the cache, casuing a potential privacy issue.
|
||||
|
@ -354,7 +354,7 @@
|
||||
"file",
|
||||
"web-app/src/operations/resources.js"
|
||||
],
|
||||
"d815397e45efe773908b48de42f21ee22481396c318b21ea215ad6343d3f4170"
|
||||
"8bb71b7d7ef3962c28b9db1554500452e7f91b0c9fd898d5cafcdb7229f6db1e"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -40,16 +40,15 @@ export function getActiveOptimisticUpdates(queryKey) {
|
||||
return updateHandlers.getUpdateHandlers(queryKey)
|
||||
}
|
||||
|
||||
export async function removeQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
queryClient.removeQueries()
|
||||
}
|
||||
|
||||
export async function invalidateAndRemoveQueries() {
|
||||
const queryClient = await queryClientInitialized
|
||||
// If we don't reset the queries before removing them, Wasp will stay on
|
||||
// the same page. The user would have to manually refresh the page to "finish"
|
||||
// logging out.
|
||||
// When a query is removed, the `Observer` is removed as well, and the components
|
||||
// that are using the query are not re-rendered. This is why we need to reset
|
||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
||||
queryClient.resetQueries()
|
||||
// If we don't remove the queries after invalidating them, the old query data
|
||||
// remains in the cache, casuing a potential privacy issue.
|
||||
|
6
waspc/examples/todoApp/prettier.config.js
Normal file
6
waspc/examples/todoApp/prettier.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
@ -1,10 +1,32 @@
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import useAuth from '@wasp/auth/useAuth.js'
|
||||
|
||||
import './Main.css'
|
||||
|
||||
export function App({ children }) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<header className="mb-6">
|
||||
<h1 className="text-3xl font-bold">ToDo App</h1>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const { data: user } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="app border-spacing-2 p-4">
|
||||
<header className="flex justify-between">
|
||||
<h1 className="font-bold text-3xl mb-5">
|
||||
<a href="/">ToDo App</a>
|
||||
</h1>
|
||||
{user && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div>
|
||||
Hello, <a href="/profile">{user.username}</a>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-primary" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
<footer className="mt-8 text-center">Created with Wasp</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -9,13 +9,17 @@
|
||||
a {
|
||||
@apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600;
|
||||
}
|
||||
input[type='submit'] {
|
||||
@apply btn btn-blue;
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
@apply rounded;
|
||||
}
|
||||
input[type="submit"] {
|
||||
@apply btn btn-primary;
|
||||
}
|
||||
form.auth-form input {
|
||||
@apply my-2;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
@apply appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer;
|
||||
}
|
||||
h1 {
|
||||
@ -28,15 +32,15 @@
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply font-bold py-2 px-4;
|
||||
@apply font-bold py-2 px-4 rounded;
|
||||
}
|
||||
.btn-blue {
|
||||
.btn-primary {
|
||||
@apply bg-blue-500 text-white;
|
||||
}
|
||||
.btn-red {
|
||||
@apply bg-red-500 text-white;
|
||||
}
|
||||
.btn-blue:hover {
|
||||
@apply bg-blue-700;
|
||||
.btn-primary:hover {
|
||||
@apply bg-blue-600;
|
||||
}
|
||||
}
|
||||
|
@ -14,23 +14,31 @@ type GetTasksError = { message: string }
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]]
|
||||
|
||||
function areThereAnyTasks(tasks: Task[] | undefined): tasks is NonEmptyArray<Task> {
|
||||
function areThereAnyTasks(
|
||||
tasks: Task[] | undefined
|
||||
): tasks is NonEmptyArray<Task> {
|
||||
return !!(tasks && tasks.length > 0)
|
||||
}
|
||||
|
||||
const Todo = () => {
|
||||
const { data: tasks, isError, error: tasksError } = useQuery<{}, Task[], GetTasksError>(getTasks)
|
||||
const {
|
||||
data: tasks,
|
||||
isError,
|
||||
error: tasksError,
|
||||
} = useQuery<{}, Task[], GetTasksError>(getTasks)
|
||||
|
||||
const TasksError = () => {
|
||||
return <div>{'Error during fetching tasks: ' + (tasksError?.message || '')}</div>
|
||||
return (
|
||||
<div>{'Error during fetching tasks: ' + (tasksError?.message || '')}</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-center'>
|
||||
<div className='w-3/6 shadow-md rounded p-6'>
|
||||
<div className="flex justify-center">
|
||||
<div className="w-3/6 shadow-md rounded p-6">
|
||||
<h1>Todos</h1>
|
||||
|
||||
<div className='flex justify-start'>
|
||||
<div className="flex justify-start gap-2">
|
||||
<ToggleAllTasksButton disabled={!areThereAnyTasks(tasks)} />
|
||||
<NewTaskForm />
|
||||
</div>
|
||||
@ -50,8 +58,8 @@ const Todo = () => {
|
||||
}
|
||||
|
||||
const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
const numCompletedTasks = tasks.filter(t => t.isDone).length
|
||||
const numUncompletedTasks = tasks.filter(t => !t.isDone).length
|
||||
const numCompletedTasks = tasks.filter((t) => t.isDone).length
|
||||
const numUncompletedTasks = tasks.filter((t) => !t.isDone).length
|
||||
|
||||
const handleDeleteCompletedTasks = async () => {
|
||||
try {
|
||||
@ -62,10 +70,8 @@ const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-between'>
|
||||
<div>
|
||||
{numUncompletedTasks} items left
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div>{numUncompletedTasks} items left</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
@ -82,32 +88,40 @@ const Footer = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
const Tasks = ({ tasks }: { tasks: NonEmptyArray<Task> }) => {
|
||||
return (
|
||||
<div>
|
||||
<table className='border-separate border-spacing-2'>
|
||||
<table className="border-separate border-spacing-2">
|
||||
<tbody>
|
||||
{tasks.map((task, idx) => <TaskView task={task} key={idx} />)}
|
||||
{tasks.map((task, idx) => (
|
||||
<TaskView task={task} key={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type UpdateTaskIsDonePayload = Pick<Task, "id" | "isDone">
|
||||
type UpdateTaskIsDonePayload = Pick<Task, 'id' | 'isDone'>
|
||||
|
||||
const TaskView = ({ task }: { task: Task }) => {
|
||||
const updateTaskIsDoneOptimistically = useAction(updateTaskIsDone, {
|
||||
optimisticUpdates: [{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) => {
|
||||
if (oldTasks === undefined) {
|
||||
// cache is empty
|
||||
return [{ ...task, ...updatedTask }];
|
||||
} else {
|
||||
return oldTasks.map(task => task.id === updatedTask.id ? { ...task, ...updatedTask } : task)
|
||||
}
|
||||
}
|
||||
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, Task[]>]
|
||||
});
|
||||
const handleTaskIsDoneChange: ChangeEventHandler<HTMLInputElement> = async (event) => {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) => {
|
||||
if (oldTasks === undefined) {
|
||||
// cache is empty
|
||||
return [{ ...task, ...updatedTask }]
|
||||
} else {
|
||||
return oldTasks.map((task) =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
)
|
||||
}
|
||||
},
|
||||
} as OptimisticUpdateDefinition<UpdateTaskIsDonePayload, Task[]>,
|
||||
],
|
||||
})
|
||||
const handleTaskIsDoneChange: ChangeEventHandler<HTMLInputElement> = async (
|
||||
event
|
||||
) => {
|
||||
const id = parseInt(event.target.id)
|
||||
const isDone = event.target.checked
|
||||
|
||||
@ -122,11 +136,11 @@ const TaskView = ({ task }: { task: Task }) => {
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type='checkbox'
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleTaskIsDoneChange}
|
||||
color='default'
|
||||
color="default"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@ -145,7 +159,9 @@ const NewTaskForm = () => {
|
||||
await createTask(task)
|
||||
}
|
||||
|
||||
const handleNewTaskSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
const handleNewTaskSubmit: FormEventHandler<HTMLFormElement> = async (
|
||||
event
|
||||
) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await createNewTask(description)
|
||||
@ -156,16 +172,14 @@ const NewTaskForm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleNewTaskSubmit} className='content-start'>
|
||||
<form onSubmit={handleNewTaskSubmit} className="content-start flex gap-2">
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Enter task'
|
||||
type="text"
|
||||
placeholder="Enter task"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<button className='btn btn-blue'>
|
||||
Create new task
|
||||
</button>
|
||||
<button className="btn btn-primary">Create new task</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@ -181,7 +195,7 @@ const ToggleAllTasksButton = ({ disabled }: { disabled: boolean }) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
className='btn btn-blue'
|
||||
className="btn btn-primary"
|
||||
disabled={disabled}
|
||||
onClick={handleToggleAllTasks}
|
||||
>
|
||||
|
@ -1,18 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import Todo from '../Todo'
|
||||
import '../Main.css'
|
||||
|
||||
const Main = () => {
|
||||
return (
|
||||
<>
|
||||
<button className='btn btn-blue' onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
<Todo />
|
||||
</>
|
||||
)
|
||||
return <Todo />
|
||||
}
|
||||
|
||||
export default Main
|
||||
|
@ -2,12 +2,15 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { User } from '@wasp/entities'
|
||||
|
||||
export const ProfilePage = ({ username }: User) => {
|
||||
export const ProfilePage = ({ user: { username } }: { user: User }) => {
|
||||
return (
|
||||
<>
|
||||
<div>I am Profile page for { username }!</div>
|
||||
<br />
|
||||
<Link to='/'>Go to dashboard</Link>
|
||||
<h2>Profile page</h2>
|
||||
<div>
|
||||
Hello <strong>{username}</strong>!
|
||||
</div>
|
||||
<br />
|
||||
<Link to="/">Go to dashboard</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user