diff --git a/web/docs/tutorial/01-create.md b/web/docs/tutorial/01-create.md
index c4a9bd213e..2a37a27573 100644
--- a/web/docs/tutorial/01-create.md
+++ b/web/docs/tutorial/01-create.md
@@ -3,6 +3,7 @@ title: 1. Creating a New Project
---
import useBaseUrl from '@docusaurus/useBaseUrl';
+import { TutorialAction } from './TutorialAction';
:::info
You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the [QuickStart](../quick-start) guide!
@@ -24,9 +25,12 @@ You can find the complete code of the app we're about to build [here](https://gi
To setup a new Wasp project, run the following command in your terminal:
+
+
```sh
wasp new TodoApp -t minimal
```
+
diff --git a/web/docs/tutorial/03-pages.md b/web/docs/tutorial/03-pages.md
index cc849778f6..b13980cae7 100644
--- a/web/docs/tutorial/03-pages.md
+++ b/web/docs/tutorial/03-pages.md
@@ -6,6 +6,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs } from '@site/src/components/TsJsHelpers';
import WaspStartNote from '../\_WaspStartNote.md'
import TypescriptServerNote from '../\_TypescriptServerNote.md'
+import { TutorialAction } from './TutorialAction';
In the default `main.wasp` file created by `wasp new`, there is a **page** and a **route** declaration:
@@ -47,7 +48,7 @@ import './Main.css';
export function MainPage() {
// ...
-}
+};
```
This is a regular functional React component. It also imports some CSS and a logo from the `assets` folder.
@@ -74,12 +75,12 @@ page HelloPage {
When a user visits `/hello/their-name`, Wasp renders the component exported from `src/HelloPage.{jsx,tsx}` and you can use the `useParams` hook from `react-router-dom` to access the `name` parameter:
```tsx title="src/HelloPage.tsx" auto-js
-import { useParams } from 'react-router-dom'
+import { useParams } from "react-router-dom";
export const HelloPage = () => {
- const { name } = useParams<'name'>()
- return
Here's {name}!
-}
+ const { name } = useParams<"name">();
+ return
Here's {name}!
;
+};
```
Now you can visit `/hello/johnny` and see "Here's johnny!"
@@ -96,12 +97,14 @@ Now that you've seen how Wasp deals with Routes and Pages, it's finally time to
Start by cleaning up the starter project and removing unnecessary code and files.
+
+
First, remove most of the code from the `MainPage` component:
```tsx title="src/MainPage.tsx" auto-js
export const MainPage = () => {
- return
Hello world!
-}
+ return
Hello world!
;
+};
```
At this point, the main page should look like this:
@@ -130,6 +133,7 @@ page MainPage {
component: import { MainPage } from "@src/MainPage"
}
```
+
Excellent work!
diff --git a/web/docs/tutorial/04-entities.md b/web/docs/tutorial/04-entities.md
index 04bd4b8878..ab75b28c36 100644
--- a/web/docs/tutorial/04-entities.md
+++ b/web/docs/tutorial/04-entities.md
@@ -3,6 +3,7 @@ title: 4. Database Entities
---
import useBaseUrl from '@docusaurus/useBaseUrl';
+import { TutorialAction } from './TutorialAction';
Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.
@@ -10,6 +11,8 @@ Wasp uses Prisma to talk to the database, and you define Entities by defining Pr
Since our Todo app is all about tasks, we'll define a Task entity by adding a Task model in the `schema.prisma` file:
+
+
```prisma title="schema.prisma"
// ...
@@ -19,6 +22,7 @@ model Task {
isDone Boolean @default(false)
}
```
+
:::note
Read more about how Wasp Entities work in the [Entities](../data-model/entities.md) section or how Wasp uses the `schema.prisma` file in the [Prisma Schema File](../data-model/prisma-file.md) section.
@@ -26,6 +30,7 @@ Read more about how Wasp Entities work in the [Entities](../data-model/entities.
To update the database schema to include this entity, stop the `wasp start` process, if it's running, and run:
+
```sh
wasp db migrate-dev
```
diff --git a/web/docs/tutorial/05-queries.md b/web/docs/tutorial/05-queries.md
index 65270c78e0..8c111df5c3 100644
--- a/web/docs/tutorial/05-queries.md
+++ b/web/docs/tutorial/05-queries.md
@@ -4,6 +4,7 @@ title: 5. Querying the Database
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
+import { TutorialAction } from './TutorialAction';
We want to know which tasks we need to do, so let's list them!
@@ -24,6 +25,7 @@ We'll create a new Query called `getTasks`. We'll need to declare the Query in t
We need to add a **query** declaration to `main.wasp` so that Wasp knows it exists:
+
```wasp title="main.wasp"
@@ -39,6 +41,7 @@ We need to add a **query** declaration to `main.wasp` so that Wasp knows it exis
entities: [Task]
}
```
+
@@ -59,8 +62,10 @@ We need to add a **query** declaration to `main.wasp` so that Wasp knows it exis
:::note
To generate the types used in the next section, make sure that `wasp start` is still running.
:::
+
+
### Implementing a Query
@@ -72,17 +77,19 @@ We need to add a **query** declaration to `main.wasp` so that Wasp knows it exis
Next, create a new file called `src/queries.ts` and define the TypeScript function we've just imported in our `query` declaration:
+
+
```ts title="src/queries.ts" auto-js
-import type { Task } from 'wasp/entities'
-import type { GetTasks } from 'wasp/server/operations'
+import type { Task } from "wasp/entities";
+import type { GetTasks } from "wasp/server/operations";
export const getTasks: GetTasks = async (args, context) => {
return context.entities.Task.findMany({
- orderBy: { id: 'asc' },
- })
-}
+ orderBy: { id: "asc" },
+ });
+};
```
-
+
Wasp automatically generates the types `GetTasks` and `Task` based on the contents of `main.wasp`:
@@ -116,25 +123,27 @@ While we implement Queries on the server, Wasp generates client-side functions t
This makes it easy for us to use the `getTasks` Query we just created in our React component:
+
+
```tsx title="src/MainPage.tsx" auto-js
-import type { Task } from 'wasp/entities'
+import type { Task } from "wasp/entities";
// highlight-next-line
-import { getTasks, useQuery } from 'wasp/client/operations'
+import { getTasks, useQuery } from "wasp/client/operations";
export const MainPage = () => {
// highlight-start
- const { data: tasks, isLoading, error } = useQuery(getTasks)
+ const { data: tasks, isLoading, error } = useQuery(getTasks);
return (
- )
-}
+ );
+};
// highlight-end
```
+
Most of this code is regular React, the only exception being the twothree special `wasp` imports:
@@ -172,7 +182,7 @@ Most of this code is regular React, the only exception being the two<
- `useQuery` - Wasp's [useQuery](../data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name.
- `Task` - The type for the Task entity defined in `schema.prisma`.
- Notice how you don't need to annotate the type of the Query's return value: Wasp uses the types you defined while implementing the Query for the generated client-side function. This is **full-stack type safety**: the types on the client always match the types on the server.
+Notice how you don't need to annotate the type of the Query's return value: Wasp uses the types you defined while implementing the Query for the generated client-side function. This is **full-stack type safety**: the types on the client always match the types on the server.
We could have called the Query directly using `getTasks()`, but the `useQuery` hook makes it reactive: React will re-render the component every time the Query changes. Remember that Wasp automatically refreshes Queries whenever the data is modified.
diff --git a/web/docs/tutorial/06-actions.md b/web/docs/tutorial/06-actions.md
index 4ae5d6840f..a00d9b9935 100644
--- a/web/docs/tutorial/06-actions.md
+++ b/web/docs/tutorial/06-actions.md
@@ -5,6 +5,7 @@ title: 6. Modifying Data
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
import Collapse from '@site/src/components/Collapse';
+import { TutorialAction } from './TutorialAction';
In the previous section, you learned about using Queries to fetch data.
Let's now learn about Actions so you can add and update tasks in the database.
@@ -22,6 +23,8 @@ Creating an Action is very similar to creating a Query.
We must first declare the Action in `main.wasp`:
+
+
```wasp title="main.wasp"
// ...
@@ -30,32 +33,35 @@ action createTask {
entities: [Task]
}
```
+
### Implementing an Action
Let's now define a JavaScriptTypeScript function for our `createTask` Action:
+
+
```ts title="src/actions.ts" auto-js
-import type { Task } from 'wasp/entities'
-import type { CreateTask } from 'wasp/server/operations'
+import type { Task } from "wasp/entities";
+import type { CreateTask } from "wasp/server/operations";
-type CreateTaskPayload = Pick
+type CreateTaskPayload = Pick;
export const createTask: CreateTask = async (
args,
- context
+ context,
) => {
return context.entities.Task.create({
data: { description: args.description },
- })
-}
+ });
+};
```
+
Once again, we've annotated the Action with the `CreateTask` and `Task` types generated by Wasp. Just like with queries, defining the types on the implementation makes them available on the frontend, giving us **full-stack type safety**.
-
:::tip
We put the function in a new file `src/actions.{js,ts}`, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within `src` directory.
:::
@@ -64,41 +70,44 @@ We put the function in a new file `src/actions.{js,ts}`, but we could have put i
Start by defining a form for creating new tasks.
+
+
```tsx title="src/MainPage.tsx" auto-js
-import type { FormEvent } from 'react'
-import type { Task } from 'wasp/entities'
+import type { FormEvent } from "react";
+import type { Task } from "wasp/entities";
import {
// highlight-next-line
createTask,
getTasks,
- useQuery
-} from 'wasp/client/operations'
+ useQuery,
+} from "wasp/client/operations";
// ... MainPage, TaskView, TaskList ...
// highlight-start
const NewTaskForm = () => {
const handleSubmit = async (event: FormEvent) => {
- event.preventDefault()
+ event.preventDefault();
try {
- const target = event.target as HTMLFormElement
- const description = target.description.value
- target.reset()
- await createTask({ description })
+ const target = event.target as HTMLFormElement;
+ const description = target.description.value;
+ target.reset();
+ await createTask({ description });
} catch (err: any) {
- window.alert('Error: ' + err.message)
+ window.alert("Error: " + err.message);
}
- }
+ };
return (
- )
-}
+ );
+};
// highlight-end
```
+
Unlike Queries, you can call Actions directly (without wrapping them in a hook) because they don't need reactivity. The rest is just regular React code.
@@ -108,27 +117,30 @@ Unlike Queries, you can call Actions directly (without wrapping them in a hook)
All that's left now is adding this form to the page component:
+
+
```tsx title="src/MainPage.tsx" auto-js
-import type { FormEvent } from 'react'
-import type { Task } from 'wasp/entities'
-import { createTask, getTasks, useQuery } from 'wasp/client/operations'
+import type { FormEvent } from "react";
+import type { Task } from "wasp/entities";
+import { createTask, getTasks, useQuery } from "wasp/client/operations";
const MainPage = () => {
- const { data: tasks, isLoading, error } = useQuery(getTasks)
+ const { data: tasks, isLoading, error } = useQuery(getTasks);
return (
- )
-}
+ );
+};
// ... TaskList, TaskView, NewTaskForm ...
```
+
Great work!
@@ -163,51 +175,60 @@ Since we've already created one task together, try to create this one yourself.
Declaring the Action in `main.wasp`:
- ```wasp title="main.wasp"
- // ...
+
- action updateTask {
- fn: import { updateTask } from "@src/actions",
- entities: [Task]
- }
- ```
+```wasp title="main.wasp"
+// ...
- Implementing the Action on the server:
+action updateTask {
+ fn: import { updateTask } from "@src/actions",
+ entities: [Task]
+}
+```
+
+
+Implementing the Action on the server:
+
+
+
+```ts title="src/actions.ts" auto-js
+import type { CreateTask, UpdateTask } from "wasp/server/operations";
+
+// ...
- ```ts title="src/actions.ts" auto-js
- import type { CreateTask, UpdateTask } from 'wasp/server/operations'
+type UpdateTaskPayload = Pick;
- // ...
+export const updateTask: UpdateTask = async (
+ { id, isDone },
+ context,
+) => {
+ return context.entities.Task.update({
+ where: { id },
+ data: {
+ isDone: isDone,
+ },
+ });
+};
+```
- type UpdateTaskPayload = Pick
+
- export const updateTask: UpdateTask = async (
- { id, isDone },
- context
- ) => {
- return context.entities.Task.update({
- where: { id },
- data: {
- isDone: isDone,
- },
- })
- }
- ```
You can now call `updateTask` from the React component:
+
+
```tsx title="src/MainPage.tsx" auto-js
-import type { FormEvent, ChangeEvent } from 'react'
-import type { Task } from 'wasp/entities'
+import type { FormEvent, ChangeEvent } from "react";
+import type { Task } from "wasp/entities";
import {
// highlight-next-line
updateTask,
createTask,
getTasks,
useQuery,
-} from 'wasp/client/operations'
-
+} from "wasp/client/operations";
// ... MainPage ...
@@ -218,11 +239,11 @@ const TaskView = ({ task }: { task: Task }) => {
await updateTask({
id: task.id,
isDone: event.target.checked,
- })
+ });
} catch (error: any) {
- window.alert('Error while updating task: ' + error.message)
+ window.alert("Error while updating task: " + error.message);
}
- }
+ };
// highlight-end
return (
@@ -236,11 +257,12 @@ const TaskView = ({ task }: { task: Task }) => {
/>
{task.description}
- )
-}
+ );
+};
// ... TaskList, NewTaskForm ...
```
+
Awesome!
You can now mark this task as done.
diff --git a/web/docs/tutorial/07-auth.md b/web/docs/tutorial/07-auth.md
index 5928e6eddc..f70cbb49ab 100644
--- a/web/docs/tutorial/07-auth.md
+++ b/web/docs/tutorial/07-auth.md
@@ -4,6 +4,7 @@ title: 7. Adding Authentication
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
+import { TutorialAction } from './TutorialAction';
Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support.
@@ -23,6 +24,8 @@ Since Wasp manages authentication, it will create [the auth related entities](..
You must only add the `User` Entity to keep track of who owns which tasks:
+
+
```prisma title="schema.prisma"
// ...
@@ -30,11 +33,14 @@ model User {
id Int @id @default(autoincrement())
}
```
+
## Adding Auth to the Project
Next, tell Wasp to use full-stack [authentication](../auth/overview):
+
+
```wasp title="main.wasp"
app TodoApp {
wasp: {
@@ -60,9 +66,12 @@ app TodoApp {
// ...
```
+
Don't forget to update the database schema by running:
+
+
```sh
wasp db migrate-dev
```
@@ -82,6 +91,8 @@ Wasp also supports authentication using [Google](../auth/social-auth/google), [G
Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:
+
+
```wasp title="main.wasp"
// ...
@@ -95,46 +106,53 @@ page LoginPage {
component: import { LoginPage } from "@src/LoginPage"
}
```
+
Great, Wasp now knows these pages exist!
Here's the React code for the pages you've just imported:
+
+
```tsx title="src/LoginPage.tsx" auto-js
-import { Link } from 'react-router-dom'
-import { LoginForm } from 'wasp/client/auth'
+import { Link } from "react-router-dom";
+import { LoginForm } from "wasp/client/auth";
export const LoginPage = () => {
return (
-
+
I don't have an account yet (go to signup).
- )
-}
+ );
+};
```
+
The signup page is very similar to the login page:
+
+
```tsx title="src/SignupPage.tsx" auto-js
-import { Link } from 'react-router-dom'
-import { SignupForm } from 'wasp/client/auth'
+import { Link } from "react-router-dom";
+import { SignupForm } from "wasp/client/auth";
export const SignupPage = () => {
return (
-
+
I already have an account (go to login).
- )
-}
+ );
+};
```
+
:::tip Type-safe links
@@ -146,6 +164,8 @@ export const SignupPage = () => {
We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:
+
+
```wasp title="main.wasp"
// ...
@@ -155,20 +175,24 @@ page MainPage {
component: import { MainPage } from "@src/MainPage"
}
```
+
Now that auth is required for this page, unauthenticated users will be redirected to `/login`, as we specified with `app.auth.onAuthFailedRedirectTo`.
Additionally, when `authRequired` is `true`, the page's React component will be provided a `user` object as prop.
+
+
```tsx title="src/MainPage.tsx" auto-js
-import type { AuthUser } from 'wasp/auth'
+import type { AuthUser } from "wasp/auth";
// highlight-next-line
export const MainPage = ({ user }: { user: AuthUser }) => {
// Do something with the user
// ...
-}
+};
```
+
Ok, time to test this out. Navigate to the main page (`/`) of the app. You'll get redirected to `/login`, where you'll be asked to authenticate.
@@ -194,6 +218,8 @@ However, you will notice that if you try logging in as different users and creat
First, let's define a one-to-many relation between users and tasks (check the [Prisma docs on relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)):
+
+
```prisma title="schema.prisma"
// ...
@@ -213,9 +239,11 @@ model Task {
userId Int?
}
```
+
As always, you must migrate the database after changing the Entities:
+
```sh
wasp db migrate-dev
```
@@ -232,41 +260,46 @@ Instead, we would do a data migration to take care of those tasks, even if it me
Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:
+
+
```ts title="src/queries.ts" auto-js
-import type { Task } from 'wasp/entities'
+import type { Task } from "wasp/entities";
// highlight-next-line
-import { HttpError } from 'wasp/server'
-import type { GetTasks } from 'wasp/server/operations'
+import { HttpError } from "wasp/server";
+import type { GetTasks } from "wasp/server/operations";
export const getTasks: GetTasks = async (args, context) => {
// highlight-start
if (!context.user) {
- throw new HttpError(401)
+ throw new HttpError(401);
}
// highlight-end
return context.entities.Task.findMany({
// highlight-next-line
where: { user: { id: context.user.id } },
- orderBy: { id: 'asc' },
- })
-}
+ orderBy: { id: "asc" },
+ });
+};
```
+
+
+
```ts title="src/actions.ts" auto-js
-import type { Task } from 'wasp/entities'
+import type { Task } from "wasp/entities";
// highlight-next-line
-import { HttpError } from 'wasp/server'
-import type { CreateTask, UpdateTask } from 'wasp/server/operations'
+import { HttpError } from "wasp/server";
+import type { CreateTask, UpdateTask } from "wasp/server/operations";
-type CreateTaskPayload = Pick
+type CreateTaskPayload = Pick;
export const createTask: CreateTask = async (
args,
- context
+ context,
) => {
// highlight-start
if (!context.user) {
- throw new HttpError(401)
+ throw new HttpError(401);
}
// highlight-end
return context.entities.Task.create({
@@ -275,10 +308,10 @@ export const createTask: CreateTask = async (
// highlight-next-line
user: { connect: { id: context.user.id } },
},
- })
-}
+ });
+};
-type UpdateTaskPayload = Pick
+type UpdateTaskPayload = Pick;
export const updateTask: UpdateTask<
UpdateTaskPayload,
@@ -286,15 +319,16 @@ export const updateTask: UpdateTask<
> = async (args, context) => {
// highlight-start
if (!context.user) {
- throw new HttpError(401)
+ throw new HttpError(401);
}
// highlight-end
return context.entities.Task.updateMany({
where: { id: args.id, user: { id: context.user.id } },
data: { isDone: args.isDone },
- })
-}
+ });
+};
```
+
:::note
Due to how Prisma works, we had to convert `update` to `updateMany` in `updateTask` action to be able to specify the user id in `where`.
@@ -316,10 +350,12 @@ You will see that each user has their tasks, just as we specified in our code!
Last, but not least, let's add the logout functionality:
+
+
```tsx title="src/MainPage.tsx" auto-js with-hole
// ...
// highlight-next-line
-import { logout } from 'wasp/client/auth'
+import { logout } from "wasp/client/auth";
//...
const MainPage = () => {
@@ -330,9 +366,10 @@ const MainPage = () => {
// highlight-next-line
- )
-}
+ );
+};
```
+
This is it, we have a working authentication system, and our Todo app is multi-user!
diff --git a/web/docs/tutorial/TutorialAction.tsx b/web/docs/tutorial/TutorialAction.tsx
new file mode 100644
index 0000000000..0b26631321
--- /dev/null
+++ b/web/docs/tutorial/TutorialAction.tsx
@@ -0,0 +1,120 @@
+import { useState } from "react";
+/*
+`TutorialAction` component is related to the Tutorial Actions Executor (TACTE) which you can find in the `web/tutorial-actions-executor` folder.
+
+Its main purpose is to provide metadata on how to execute tutorial actions programmatically (using TACTE)
+Additionally, it renders tutorial action names during development for easier debugging.
+
+`TutorialAction` component is used in the `web/docs/tutorial/*.md` files to annotate specific tutorial actions.
+*/
+
+// IMPORTANT: If you change actions here, make sure to also update the types in `web/tutorial-actions-executor/src/actions/actions.ts`.
+type ActionProps = {
+ id: string;
+} & (
+ | {
+ action: "INIT_APP";
+ starterTemplateName: string;
+ }
+ | {
+ action: "APPLY_PATCH";
+ }
+ | {
+ action: "MIGRATE_DB";
+ }
+);
+
+export function TutorialAction(props: React.PropsWithChildren) {
+ const isDevelopment = process.env.NODE_ENV !== "production";
+
+ return isDevelopment ? (
+
+ ) : (
+ props.children
+ );
+}
+
+function TutorialActionDebugInfo({
+ id,
+ action,
+ children,
+}: React.PropsWithChildren) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(id);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ };
+
+ return (
+