mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-22 01:24:49 +03:00
Restructures docs (#1333)
Co-authored-by: Mihovil Ilakovac <mihovil@ilakovac.com> Co-authored-by: Martin Šošić <Martinsos@users.noreply.github.com> Co-authored-by: Filip Sodić <filip.sodic@gmail.com> Co-authored-by: Filip Sodić <filip.sodic@fer.hr>
This commit is contained in:
parent
89ea9efbf2
commit
60233dcbcc
69
web/WRITING-DOCS.md
Normal file
69
web/WRITING-DOCS.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Writing Documentation
|
||||
|
||||
To keep Wasp's documentation organized and consistent, follow the guidelines set
|
||||
out in this document when updating or writing new documentation.
|
||||
|
||||
## Organization
|
||||
|
||||
When adding a new page to the docs, read the guidelines below to determine the
|
||||
best place for the page to go.
|
||||
|
||||
If a new section is created, add it to the list below and include some information
|
||||
on how to decide if a page belongs in that section or not.
|
||||
|
||||
### Getting Started
|
||||
|
||||
Answers: What is Wasp and how do I get it?
|
||||
|
||||
### Essentials
|
||||
|
||||
Answers: Now that I have Wasp, what can I do with it?
|
||||
|
||||
Explains the workflow of using Wasp and the most important parts:
|
||||
- How do I make a new project
|
||||
- What's in the project
|
||||
- How do I add more pages to the website
|
||||
- How do I store stuff in a database
|
||||
|
||||
How do we know if a feature belongs here?
|
||||
- A feature belongs here when it's a _core feature_: nearly every Wasp project is
|
||||
going to use this feature in some capacity.
|
||||
|
||||
### Data Model
|
||||
|
||||
Answers: How do persist data in Wasp?
|
||||
|
||||
How do we know if a feature belongs here?
|
||||
- What's the broad purpose of the feature? If the answer includes data or database,
|
||||
then it probably belongs here.
|
||||
|
||||
The data model is a core part of Wasp. Why is this separate from Essentials?
|
||||
- Some parts of the data model are "extras." Projects will use some of them, but
|
||||
it varies from project to project.
|
||||
|
||||
### Advanced Features
|
||||
|
||||
Answers: What else does Wasp have to offer?
|
||||
|
||||
How do we know if a feature belongs here vs somewhere else?
|
||||
- It's not a core feature.
|
||||
- It's one conceptual "unit." As in, the feature doesn't have several complex
|
||||
subcomponents (like how auth has several providers requiring long explanation
|
||||
of how to use each one).
|
||||
|
||||
### Authentication
|
||||
|
||||
Answers: I know auth exists in Wasp, now tell me all the options!
|
||||
|
||||
--> Why is this not an Advanced Feature? Auth providers have large differences
|
||||
in use and how they are configured. Auth UI is also a large topic, as well
|
||||
as best practices for using auth.
|
||||
|
||||
### Project Setup
|
||||
|
||||
Answers: How do I add/configure X in my project?
|
||||
|
||||
--> Why are these not Advanced Features? Each are pretty boring and small, and
|
||||
not really an interesting feature of Wasp, just something that can be done.
|
||||
And they all relate to configuring the project/what can exist within the
|
||||
project.
|
@ -139,7 +139,7 @@ page Main {
|
||||
}
|
||||
```
|
||||
|
||||
All pretty straightforward so far! As you can see here, Wasp also provides [authentication out-of-the-box](/docs/language/features#authentication--authorization).
|
||||
All pretty straightforward so far! As you can see here, Wasp also provides [authentication out-of-the-box](/docs/auth/overview).
|
||||
|
||||
Currently, the majority of the client logic of Waspello is contained in `src/client/MainPage.js` (we should break it down a little 😅 - [you can help us!](https://github.com/wasp-lang/wasp/issues/334)). Just to give you an idea, here's a quick glimpse into it:
|
||||
|
||||
@ -163,7 +163,7 @@ const MainPage = ({ user }) => {
|
||||
)
|
||||
}
|
||||
```
|
||||
Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the `@wasp` prefix in the import path. `useQuery` ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it [here](/docs/language/features#usequery).
|
||||
Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the `@wasp` prefix in the import path. `useQuery` ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it [here](/docs/data-model/operations/queries#the-usequery-hook-1).
|
||||
|
||||
This is pretty much it from the stuff that works 😄 ! I kinda rushed a bit through things here - for more details on all Wasp features and to build your first app with Wasp, check out our [docs](/docs/).
|
||||
|
||||
|
@ -5,14 +5,13 @@ tags: [wasp, gitpod, hackathon]
|
||||
---
|
||||
|
||||
import InBlogCta from './components/InBlogCta';
|
||||
import WaspIntro from './_wasp-intro.md';
|
||||
import WaspIntro from './\_wasp-intro.md';
|
||||
|
||||
![Wasp app deploye to Gitpod](../static/img/gitpod-hackathon-preview.png)
|
||||
|
||||
>*"All good thoughts and ideas mean nothing without the proper tools to achieve them."*<br/>
|
||||
>*Jason Statham*
|
||||
> _"All good thoughts and ideas mean nothing without the proper tools to achieve them."_<br/> >_Jason Statham_
|
||||
|
||||
**TL;DR:** Wasp allows you to build and deploy a full-stack JS web app with a single config file. [Gitpod](https://gitpod.io/) spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.
|
||||
**TL;DR:** Wasp allows you to build and deploy a full-stack JS web app with a single config file. [Gitpod](https://gitpod.io/) spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
@ -22,8 +21,9 @@ import WaspIntro from './_wasp-intro.md';
|
||||
## Intro:
|
||||
|
||||
Usually, every hackathon starts from similar activities:<br/>
|
||||
1) **setting up a local dev environment**, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.<br/>
|
||||
2) **building project backbone** (folder structure, basic services, CRUD APIs, and so on).
|
||||
|
||||
1. **setting up a local dev environment**, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.<br/>
|
||||
2. **building project backbone** (folder structure, basic services, CRUD APIs, and so on).
|
||||
|
||||
Both of them are time-consuming, boring, and cause issues.
|
||||
|
||||
@ -33,49 +33,49 @@ Thankfully, those issues can be avoided! Gitpod allows you to spin up a clean, a
|
||||
|
||||
![Pennywise luring into his openspace](../static/img/hackathon-pennywise.jpg)
|
||||
|
||||
## Dev environment setup:
|
||||
## Dev environment setup:
|
||||
|
||||
Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.
|
||||
Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.
|
||||
|
||||
Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.
|
||||
Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.
|
||||
|
||||
1) Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.<br/>
|
||||
2) In your newly created repo, check the Readme and click the “Open in Gitpod” button<br/>
|
||||
3) Login via Github<br/>
|
||||
4) Allow pop-ups<br/>
|
||||
5) That’s it! Enjoy your fresh cloud-based dev environment!<br/>
|
||||
1. Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.<br/>
|
||||
2. In your newly created repo, check the Readme and click the “Open in Gitpod” button<br/>
|
||||
3. Login via Github<br/>
|
||||
4. Allow pop-ups<br/>
|
||||
5. That’s it! Enjoy your fresh cloud-based dev environment!<br/>
|
||||
|
||||
![Pennywise luring to take part in hackathon](../static/img/gitpod-roadmap.png)
|
||||
|
||||
An optional thing might be enabling the “Share” option to make the app accessible from the external internet.
|
||||
An optional thing might be enabling the “Share” option to make the app accessible from the external internet.
|
||||
|
||||
![How to share a workspace](../static/img/gitpod-share.jpg)
|
||||
|
||||
You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.
|
||||
You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.
|
||||
|
||||
![Gitpod IDE types](../static/img/gitpod-ide.jpg)
|
||||
|
||||
So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.
|
||||
So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.
|
||||
|
||||
Voila! 🥳
|
||||
|
||||
The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.
|
||||
The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.
|
||||
|
||||
No need to fix local issues, ensure the Node version is aligned, or configure the deployment pipeline for DigitalOcean. Gitpod does all development preparations. The only thing the team has to do – is to implement the idea ASAP. And here Wasp comes into play!
|
||||
|
||||
## Building project backbone:
|
||||
|
||||
Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!
|
||||
Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!
|
||||
|
||||
The ideal case would be to:<br/>
|
||||
1) Check out the language overview: https://wasp-lang.dev/docs/language/overview <br/>
|
||||
2) Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorials/todo-app <br/>
|
||||
|
||||
It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.
|
||||
1. Check out the language overview: https://wasp-lang.dev/docs/general/language <br/>
|
||||
2. Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorial/create <br/>
|
||||
|
||||
It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.
|
||||
|
||||
And 20 minutes is time well spent to become more productive. Setting up each team member's environment locally likely takes more than 20 minutes if you don't use Gitpod.
|
||||
|
||||
|
||||
## To wrap up:
|
||||
## To wrap up:
|
||||
|
||||
We think that Wasp + Gitpod is a powerful toolset for speedrunning any hackathon. No matter how complex or ambitious your project is. If it’s built with Node and React – nothing can stop you from winning. Good luck, have fun, and enjoy that pizza 🍕!
|
||||
|
@ -144,7 +144,7 @@ For those interested, check out the [full diff here](https://github.com/wasp-lan
|
||||
|
||||
## Looks neat! What’s next?
|
||||
|
||||
First off, please check out our docs for Jobs: [https://wasp-lang.dev/docs/language/features#jobs](https://wasp-lang.dev/docs/language/features#jobs) There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: [https://github.com/wasp-lang/wasp/tree/release/examples/waspleau](https://github.com/wasp-lang/wasp/tree/release/examples/waspleau)
|
||||
First off, please check out our docs for [Jobs](/docs/advanced/jobs). There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: [https://github.com/wasp-lang/wasp/tree/release/examples/waspleau](https://github.com/wasp-lang/wasp/tree/release/examples/waspleau)
|
||||
|
||||
In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!
|
||||
|
||||
|
@ -184,7 +184,7 @@ auth: {
|
||||
onAuthSucceededRedirectTo: "/dashboard"
|
||||
}
|
||||
```
|
||||
Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually [works in Wasp](/docs/language/features#authentication--authorization)).
|
||||
Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually [works in Wasp](/docs/auth/overview)).
|
||||
|
||||
We don’t need to care what exact packages or encryption methods are used beneath the hood - it is the responsibility we trust with the authors and maintainers of the abstraction layer, just like we trust that Python knows the best how to sum two numbers on the assembly level and that it is kept in sync with the latest advancements in the field. The same happens when we rely on the built-in data structures or count on the garbage collector to manage our program’s memory well.
|
||||
|
||||
|
@ -137,7 +137,7 @@ We’ve added Tailwind to make our UI more pretty and Axios for making API reque
|
||||
|
||||
Also, we’ve declared a database entity called `Excuse`, queries, and action. The `Excuse` entity consists of the entity’s ID and the text.
|
||||
|
||||
`Queries` are here when we need to fetch/read something, while `actions` are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info [in the docs](https://wasp-lang.dev/docs/tutorials/todo-app/listing-tasks#introducing-operations-queries-and-actions). So let’s proceed with queries/actions.
|
||||
`Queries` are here when we need to fetch/read something, while `actions` are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info [in the docs](/docs/data-model/operations/overview). So let’s proceed with queries/actions.
|
||||
|
||||
|
||||
**2) Create two files: “actions.js” and “queries.js” in the `src/server` folder.**
|
||||
|
@ -113,4 +113,4 @@ export default Login
|
||||
|
||||
## Epilogue
|
||||
|
||||
No need to move off the grid out of frustration when adding authentication and social login to your web app. [Here](https://github.com/shayneczyzewski/authExample) is a complete, minimal example if you want to jump right in, and [here](https://wasp-lang.dev/docs/language/features#authentication--authorization) are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!
|
||||
No need to move off the grid out of frustration when adding authentication and social login to your web app. [Here](https://github.com/shayneczyzewski/authExample) is a complete, minimal example if you want to jump right in, and [here](/docs/auth/overview) are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!
|
||||
|
@ -116,7 +116,7 @@ We were already aware that TypeScript support is an important feature, but didn
|
||||
|
||||
**The good parts**
|
||||
|
||||
Testers’ favourite part was the batteries-included experience, particularly the [auth model](/docs/tutorials/todo-app/06-auth).
|
||||
Testers’ favourite part was the batteries-included experience, particularly the [auth model](/docs/tutorial/auth).
|
||||
|
||||
<ImgWithCaption
|
||||
alt="Feedback survey - the good parts"
|
||||
|
@ -157,7 +157,7 @@ function Task({ id, isDone, description }) {
|
||||
)
|
||||
}
|
||||
```
|
||||
Those are all the changes we need, the rest of the code (i.e., `main.wasp`, `queries.js` and `actions.js`) remains the same. We won't describe the API in detail, but if you're curious, everything is covered by [our official docs](/docs/language/features#the-useaction-hook).
|
||||
Those are all the changes we need, the rest of the code (i.e., `main.wasp`, `queries.js` and `actions.js`) remains the same. We won't describe the API in detail, but if you're curious, everything is covered by [our official docs](/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates).
|
||||
|
||||
Finally, let's see how this version of the app looks in action:
|
||||
|
||||
|
@ -49,7 +49,7 @@ To check out the winning projects and see where devs found Wasp most helpful, ta
|
||||
|
||||
## 🔑 New auth method - GitHub! 🐙
|
||||
|
||||
Next to [username/password](/docs/language/features#username-and-password) and [Google](/docs/language/features#google), **Wasp now also supports [GitHub](/docs/language/features#github)** as an authentication method!
|
||||
Next to [username/password](/docs/auth/username-and-pass) and [Google](/docs/auth/social-auth/google), **Wasp now also supports [GitHub](/docs/auth/social-auth/github)** as an authentication method!
|
||||
|
||||
<ImgWithCaption
|
||||
alt="Support for GitHub auth in Wasp"
|
||||
@ -58,7 +58,7 @@ Next to [username/password](/docs/language/features#username-and-password) and [
|
||||
|
||||
Putting the code above in your `main.wasp` file and specifying your GitHub env variables is all you need to do! Wasp will provide you with a full-stack GitHub authentication along with UI helpers (GitHub sign-up button) you can immediately use in your React component.
|
||||
|
||||
For more details, check the docs [here](/docs/language/features#github).
|
||||
For more details, check the docs [here](/docs/auth/social-auth/github).
|
||||
|
||||
## 💬 Let's discuss - on GitHub Discussions!
|
||||
|
||||
|
@ -92,7 +92,7 @@ It's Saturday, so you get two features for the price of one!
|
||||
caption="Adding a custom route handler at /foo/bar endpoint"
|
||||
/>
|
||||
|
||||
Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via [operations](/docs/tutorials/todo-app/03-listing-tasks#introducing-operations-queries-and-actions), sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style!
|
||||
Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via [operations](/docs/data-model/operations/overview), sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style.
|
||||
|
||||
### Email sending: Wasp + Sendgrid/Mailgun/...
|
||||
|
||||
|
@ -37,7 +37,7 @@ If you're looking for a quick way to start your project, check out our [Ultimate
|
||||
|
||||
**Source code**: https://github.com/vincanger/coverlettergpt
|
||||
|
||||
**Wasp features used**: [Social login with Google + auth UI](/blog/2023/04/12/auth-ui), [email sending](http://localhost:3002/docs/guides/sending-emails)
|
||||
**Wasp features used**: [Social login with Google + auth UI](/blog/2023/04/12/auth-ui), [email sending](/docs/advanced/email)
|
||||
|
||||
**UI Framework**: [Chakra UI](https://chakra-ui.com/)
|
||||
|
||||
@ -55,7 +55,7 @@ Try it out and have fun or use it as an inspiration for your next project!
|
||||
|
||||
**Try it out**: [amicus.work](https://www.amicus.work/)
|
||||
|
||||
**Wasp features used**: [Authentication](https://wasp-lang.dev/docs/language/features#authentication--authorization), [email sending](http://localhost:3002/docs/guides/sending-emails), [async/cron jobs](https://wasp-lang.dev/docs/language/features#jobs)
|
||||
**Wasp features used**: [Authentication](/docs/auth/overview), [email sending](/docs/advanced/email), [async/cron jobs](/docs/advanced/jobs)
|
||||
|
||||
**UI Framework**: [Material UI](https://mui.com/)
|
||||
|
||||
@ -89,7 +89,7 @@ What's special about Description Generator is that it was recently sold , making
|
||||
|
||||
**Source code**: https://github.com/vincanger/banger-tweet-bot
|
||||
|
||||
**Wasp features used**:[Authentication](https://wasp-lang.dev/docs/language/features#authentication--authorization), [async/cron jobs](https://wasp-lang.dev/docs/language/features#jobs)
|
||||
**Wasp features used**:[Authentication](/docs/auth/overview), [async/cron jobs](/docs/advanced/jobs)
|
||||
|
||||
**UI Framework**: [Tailwind](https://tailwindcss.com/)
|
||||
|
||||
|
@ -66,7 +66,7 @@ The generated apps are full-stack and consist of front-end, back-end and databas
|
||||
|
||||
This app does exactly what it says - makes sure that you water your plants on time! It comes with a fully functioning front-end, back-end and the database with `User` and `Plant` entities. It also features a [full-stack authentication](/blog/2023/04/12/auth-ui) (username & password) and a Tailwind-based design.
|
||||
|
||||
The next step would be to add more advanced features, such as email reminders (via [Wasp email sending support](/docs/guides/sending-emails)) when it is time to water your plant.
|
||||
The next step would be to add more advanced features, such as email reminders (via [Wasp email sending support](/docs/advanced/email)) when it is time to water your plant.
|
||||
|
||||
You can see and download the [entire source code](https://magic-app-generator.wasp-lang.dev/result/3bb5dca2-f134-4f96-89d6-0812deab6e0c) and add more features and deploy the app yourself!
|
||||
|
||||
|
@ -710,7 +710,7 @@ You should see a login screen this time. Go ahead and first register a user, the
|
||||
|
||||
Once logged in, you’ll see the same hardcoded poll data as in the previous example, because, again, we haven’t set up the [Socket.IO](http://Socket.IO) client on the frontend. But this time it should be much easier.
|
||||
|
||||
Why? Well, besides less configuration, another nice benefit of working with [TypeScript with Wasp](https://wasp-lang.dev/docs/typescript#websocket-full-stack-type-support), is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client!
|
||||
Why? Well, besides less configuration, another nice benefit of working with [TypeScript with Wasp](/docs/typescript#websocket-full-stack-type-support), is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client!
|
||||
|
||||
Let’s take a look at how that works now.
|
||||
|
||||
|
25
web/docs/OldDocsNote.tsx
Normal file
25
web/docs/OldDocsNote.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import Admonition from "@theme/Admonition";
|
||||
import Link from "@docusaurus/Link";
|
||||
import React from "react";
|
||||
|
||||
export default function OldDocsNote() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: "calc(var(--ifm-navbar-height) + 1rem)",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Admonition type="caution" title="Deprecated Page">
|
||||
This page is part of a previous documentation version and is no longer
|
||||
actively maintained. The content is likely out of date and may no longer
|
||||
be relevant to current releases.
|
||||
<br />
|
||||
<br />
|
||||
Go to the <Link to="/docs">current documentation</Link> for updated
|
||||
content.
|
||||
</Admonition>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
:::tip Using an external auth method?
|
||||
|
||||
if your app is using an external authentication method(s) supported by Wasp (such as [Google](/docs/language/features#google) or [GitHub](/docs/language/features#github)), make sure to set the necessary environment variables.
|
||||
|
||||
:::
|
@ -1,7 +1,7 @@
|
||||
:::info Sending emails while developing
|
||||
|
||||
When you run your app in development mode, the e-mails are not actually sent. Instead, they are logged to the console.
|
||||
When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.
|
||||
|
||||
In order to enable sending e-mails in development mode, you need to set the `SEND_EMAILS_IN_DEVELOPMENT` env variable to `true` in your `.env.server` file.
|
||||
To enable sending emails in development mode, you need to set the `SEND_EMAILS_IN_DEVELOPMENT` env variable to `true` in your `.env.server` file.
|
||||
|
||||
:::
|
||||
|
343
web/docs/advanced/apis.md
Normal file
343
web/docs/advanced/apis.md
Normal file
@ -0,0 +1,343 @@
|
||||
---
|
||||
title: Custom HTTP API Endpoints
|
||||
---
|
||||
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'
|
||||
import { Required } from '@site/src/components/Required'
|
||||
|
||||
In Wasp, the default client-server interaction mechanism is through [Operations](/docs/data-model/operations/overview). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`. Best of all, they should look and feel very familiar.
|
||||
|
||||
## How to Create an API
|
||||
|
||||
APIs are used to tie a JS function to a certain endpoint e.g. `POST /something/special`. They are distinct from Operations and have no client-side helpers (like `useQuery`).
|
||||
|
||||
To create a Wasp API, you must:
|
||||
|
||||
1. Declare the API in Wasp using the `api` declaration
|
||||
2. Define the API's NodeJS implementation
|
||||
|
||||
After completing these two steps, you'll be able to call the API from the client code (via our `Axios` wrapper), or from the outside world.
|
||||
|
||||
### Declaring the API in Wasp
|
||||
|
||||
First, we need to declare the API in the Wasp file and you can easily do this with the `api` declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
api fooBar { // APIs and their implementations don't need to (but can) have the same name.
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
api fooBar { // APIs and their implementations don't need to (but can) have the same name.
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the supported fields in the [API Reference](#api-reference).
|
||||
|
||||
|
||||
### Defining the API's NodeJS Implementation
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
:::note
|
||||
To make sure the Wasp compiler generates the types for APIs for use in the NodeJS implementation, you should add your `api` declarations to your `.wasp` file first _and_ keep the `wasp start` command running.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
After you defined the API, it should be implemented as a NodeJS function that takes three arguments:
|
||||
|
||||
1. `req`: Express Request object
|
||||
2. `res`: Express Response object
|
||||
3. `context`: An additional context object **injected into the API by Wasp**. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the [section about using entities in APIs](#using-entities-in-apis).
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title="src/server/apis.js"
|
||||
export const fooBar = (req, res, context) => {
|
||||
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
||||
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/apis.ts"
|
||||
import { FooBar } from "@wasp/apis/types"; // This type is generated by Wasp based on the `api` declaration above.
|
||||
|
||||
export const fooBar: FooBar = (req, res, context) => {
|
||||
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
||||
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
#### Providing Extra Type Information
|
||||
|
||||
We'll see how we can provide extra type information to an API function.
|
||||
|
||||
Let's say you wanted to create some `GET` route that would take an email address as a param, and provide them the answer to "Life, the Universe and Everything." 😀 What would this look like in TypeScript?
|
||||
|
||||
Define the API in Wasp:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
httpRoute: (GET, "/foo/bar/:email")
|
||||
}
|
||||
```
|
||||
|
||||
We can use the `FooBar` type to which we'll provide the generic **params** and **response** types, which then gives us full type safety in the implementation.
|
||||
|
||||
```ts title="src/server/apis.ts"
|
||||
import { FooBar } from "@wasp/apis/types";
|
||||
|
||||
export const fooBar: FooBar<
|
||||
{ email: string }, // params
|
||||
{ answer: number } // response
|
||||
> = (req, res, _context) => {
|
||||
console.log(req.params.email);
|
||||
res.json({ answer: 42 });
|
||||
};
|
||||
```
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
## Using the API
|
||||
|
||||
### Using the API externally
|
||||
|
||||
To use the API externally, you simply call the endpoint using the method and path you used.
|
||||
|
||||
For example, if your app is running at `https://example.com` then from the above you could issue a `GET` to `https://example/com/foo/callback` (in your browser, Postman, `curl`, another web service, etc.).
|
||||
|
||||
### Using the API from the Client
|
||||
|
||||
To use the API from your client, including with auth support, you can import the Axios wrapper from `@wasp/api` and invoke a call. For example:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/pages/SomePage.jsx"
|
||||
import React, { useEffect } from "react";
|
||||
import api from "@wasp/api";
|
||||
|
||||
async function fetchCustomRoute() {
|
||||
const res = await api.get("/foo/bar");
|
||||
console.log(res.data);
|
||||
}
|
||||
|
||||
export const Foo = () => {
|
||||
useEffect(() => {
|
||||
fetchCustomRoute();
|
||||
}, []);
|
||||
|
||||
return <>// ...</>;
|
||||
};
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/pages/SomePage.tsx"
|
||||
import React, { useEffect } from "react";
|
||||
import api from "@wasp/api";
|
||||
|
||||
async function fetchCustomRoute() {
|
||||
const res = await api.get("/foo/bar");
|
||||
console.log(res.data);
|
||||
}
|
||||
|
||||
export const Foo = () => {
|
||||
useEffect(() => {
|
||||
fetchCustomRoute();
|
||||
}, []);
|
||||
|
||||
return <>// ...</>;
|
||||
};
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Making Sure CORS Works
|
||||
|
||||
APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.
|
||||
|
||||
You can do this by defining custom middleware for your APIs in the Wasp file.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
For example, an `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo"
|
||||
}
|
||||
```
|
||||
|
||||
And then in the implementation file:
|
||||
|
||||
```js title="src/server/apis.js"
|
||||
export const apiMiddleware = (config) => {
|
||||
return config;
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
For example, an `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo"
|
||||
}
|
||||
```
|
||||
|
||||
And then in the implementation file (returning the default config):
|
||||
|
||||
```ts title="src/server/apis.ts"
|
||||
import { MiddlewareConfigFn } from "@wasp/middleware";
|
||||
export const apiMiddleware: MiddlewareConfigFn = (config) => {
|
||||
return config;
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We are returning the default middleware which enables CORS for all APIs under the `/foo` path.
|
||||
|
||||
For more information about middleware configuration, please see: [Middleware Configuration](/docs/advanced/middleware-config)
|
||||
|
||||
## Using Entities in APIs
|
||||
|
||||
In many cases, resources used in APIs will be [Entities](/docs/data-model/entities.md).
|
||||
To use an Entity in your API, add it to the `api` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {3} title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {3} title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Wasp will inject the specified Entity into the APIs `context` argument, giving you access to the Entity's Prisma API:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title="src/server/apis.js"
|
||||
export const fooBar = (req, res, context) => {
|
||||
res.json({ count: await context.entities.Task.count() });
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/apis.ts"
|
||||
import { FooBar } from "@wasp/apis/types";
|
||||
|
||||
export const fooBar: FooBar = (req, res, context) => {
|
||||
res.json({ count: await context.entities.Task.count() });
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
||||
|
||||
## API Reference
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
httpRoute: (GET, "/foo/bar"),
|
||||
entities: [Task],
|
||||
auth: true,
|
||||
middlewareConfigFn: import { apiMiddleware } from "@server/apis.js"
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
httpRoute: (GET, "/foo/bar"),
|
||||
entities: [Task],
|
||||
auth: true,
|
||||
middlewareConfigFn: import { apiMiddleware } from "@server/apis.js"
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `api` declaration has the following fields:
|
||||
|
||||
- `fn: ServerImport` <Required />
|
||||
|
||||
The import statement of the APIs NodeJs implementation.
|
||||
|
||||
- `httpRoute: (HttpMethod, string)` <Required />
|
||||
|
||||
The HTTP (method, path) pair, where the method can be one of:
|
||||
|
||||
- `ALL`, `GET`, `POST`, `PUT` or `DELETE`
|
||||
- and path is an Express path `string`.
|
||||
|
||||
- `entities: [Entity]`
|
||||
|
||||
A list of entities you wish to use inside your API. You can read more about it [here](#using-entities-in-apis).
|
||||
|
||||
- `auth: bool`
|
||||
|
||||
If auth is enabled, this will default to `true` and provide a `context.user` object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to `false`.
|
||||
|
||||
- `middlewareConfigFn: ServerImport`
|
||||
|
||||
The import statement to an Express middleware config function for this API. See more in [middleware section](/docs/advanced/middleware-config) of the docs.
|
29
web/docs/advanced/deployment/DeploymentOptionsGrid.css
Normal file
29
web/docs/advanced/deployment/DeploymentOptionsGrid.css
Normal file
@ -0,0 +1,29 @@
|
||||
.deployment-methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.deployment-method-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-pagination-nav-border-radius);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
.deployment-method-box:hover {
|
||||
border-color: var(--ifm-pagination-nav-color-hover);
|
||||
}
|
||||
.deployment-method-box h3 {
|
||||
margin: 0;
|
||||
color: var(--ifm-link-color);
|
||||
}
|
||||
.deployment-method-box p {
|
||||
margin: 0;
|
||||
color: var(--ifm-color-secondary-contrast-foreground);
|
||||
}
|
||||
.deployment-methods-info {
|
||||
color: var(--ifm-color-secondary-contrast-foreground);
|
||||
}
|
50
web/docs/advanced/deployment/DeploymentOptionsGrid.tsx
Normal file
50
web/docs/advanced/deployment/DeploymentOptionsGrid.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import "./DeploymentOptionsGrid.css";
|
||||
|
||||
export function DeploymentOptionsGrid() {
|
||||
const deploymentMethods = [
|
||||
{
|
||||
title: "Using Wasp CLI",
|
||||
description: "One command deployment & redeployment",
|
||||
linkToDocs: "/docs/advanced/deployment/cli",
|
||||
},
|
||||
{
|
||||
title: "Deploying Manually",
|
||||
description: "Build the app and deploy it manually",
|
||||
linkToDocs: "/docs/advanced/deployment/manually",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="deployment-methods-grid">
|
||||
{deploymentMethods.map((deploymentMethod) => (
|
||||
<DeploymentOptionBox
|
||||
title={deploymentMethod.title}
|
||||
description={deploymentMethod.description}
|
||||
linkToDocs={deploymentMethod.linkToDocs}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="deployment-methods-info">
|
||||
<small>Click on each deployment method for more details.</small>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DeploymentOptionBox({
|
||||
linkToDocs,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
linkToDocs: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<a href={linkToDocs} className="deployment-method-box">
|
||||
<h3>{title} »</h3>
|
||||
<p>{description}</p>
|
||||
</a>
|
||||
);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
:::tip Using an external auth method?
|
||||
|
||||
If your app is using an external authentication method(s) supported by Wasp (such as [Google](/docs/auth/social-auth/google#4-adding-environment-variables) or [GitHub](/docs/auth/social-auth/github#4-adding-environment-variables)), make sure to set the necessary environment variables.
|
||||
:::
|
13
web/docs/advanced/deployment/_building-the-web-client.md
Normal file
13
web/docs/advanced/deployment/_building-the-web-client.md
Normal file
@ -0,0 +1,13 @@
|
||||
To build the web app, position yourself in `.wasp/build/web-app` directory:
|
||||
|
||||
```
|
||||
cd .wasp/build/web-app
|
||||
```
|
||||
|
||||
Run
|
||||
|
||||
```
|
||||
npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
|
||||
```
|
||||
|
||||
where `<url_to_wasp_backend>` is the URL of the Wasp server that you previously deployed.
|
243
web/docs/advanced/deployment/cli.md
Normal file
243
web/docs/advanced/deployment/cli.md
Normal file
@ -0,0 +1,243 @@
|
||||
---
|
||||
title: Deploying with the Wasp CLI
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
Wasp CLI can deploy your full-stack application with only a single command.
|
||||
The command automates the manual deployment process and is the recommended way of deploying Wasp apps.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
Wasp supports automated deployment to the following providers:
|
||||
|
||||
- [Fly.io](#flyio) - they offer 5$ free credit each month
|
||||
- Railway (coming soon, track it here [#1157](https://github.com/wasp-lang/wasp/pull/1157))
|
||||
|
||||
## Fly.io
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Fly provides [free allowances](https://fly.io/docs/about/pricing/#plans) for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.
|
||||
|
||||
You can add the required credit card information on the [account's billing page](https://fly.io/dashboard/personal/billing).
|
||||
|
||||
:::info Fly.io CLI
|
||||
You will need the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/) installed on your machine before you can deploy to Fly.io.
|
||||
:::
|
||||
|
||||
### Deploying
|
||||
|
||||
Using the Wasp CLI, you can easily deploy a new app to [Fly.io](https://fly.io) with just a single command:
|
||||
|
||||
```shell
|
||||
wasp deploy fly launch my-wasp-app mia
|
||||
```
|
||||
|
||||
<small>
|
||||
|
||||
Please do not CTRL-C or exit your terminal while the commands are running.
|
||||
</small>
|
||||
|
||||
Under the covers, this runs the equivalent of the following commands:
|
||||
|
||||
```shell
|
||||
wasp deploy fly setup my-wasp-app mia
|
||||
wasp deploy fly create-db mia
|
||||
wasp deploy fly deploy
|
||||
```
|
||||
|
||||
The commands above use the app basename `my-wasp-app` and deploy it to the _Miami, Florida (US) region_ (called `mia`).
|
||||
|
||||
The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:
|
||||
|
||||
- `my-wasp-app-client`
|
||||
- `my-wasp-app-server`
|
||||
- `my-wasp-app-db`
|
||||
|
||||
:::caution Unique Name
|
||||
Your app name must be unique across all of Fly or deployment will fail.
|
||||
:::
|
||||
|
||||
Read more about Fly.io regions [here](#flyio-regions).
|
||||
|
||||
### Using a Custom Domain For Your App
|
||||
|
||||
Setting up a custom domain is a three-step process:
|
||||
|
||||
1. You need to add your domain to your Fly client app. You can do this by running:
|
||||
|
||||
```shell
|
||||
wasp deploy fly cmd --context client certs create mycoolapp.com
|
||||
```
|
||||
|
||||
:::note Use Your Domain
|
||||
Make sure to replace `mycoolapp.com` with your domain in all of the commands mentioned in this section.
|
||||
:::
|
||||
|
||||
This command will output the instructions to add the DNS records to your domain. It will look something like this:
|
||||
|
||||
```shell-session
|
||||
You can direct traffic to mycoolapp.com by:
|
||||
|
||||
1: Adding an A record to your DNS service which reads
|
||||
|
||||
A @ 66.241.1XX.154
|
||||
|
||||
You can validate your ownership of mycoolapp.com by:
|
||||
|
||||
2: Adding an AAAA record to your DNS service which reads:
|
||||
|
||||
AAAA @ 2a09:82XX:1::1:ff40
|
||||
```
|
||||
|
||||
2. You need to add the DNS records for your domain:
|
||||
|
||||
_This will depend on your domain provider, but it should be a matter of adding an A record for `@` and an AAAA record for `@` with the values provided by the previous command._
|
||||
|
||||
3. You need to set your domain as the `WASP_WEB_CLIENT_URL` environment variable for your server app:
|
||||
|
||||
```shell
|
||||
wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com
|
||||
```
|
||||
|
||||
<small>
|
||||
|
||||
We need to do this to keep our CORS configuration up to date.
|
||||
</small>
|
||||
|
||||
That's it, your app should be available at `https://mycoolapp.com`! 🎉
|
||||
|
||||
## API Reference
|
||||
|
||||
### `launch`
|
||||
|
||||
`launch` is a convenience command that runs `setup`, `create-db`, and `deploy` in sequence.
|
||||
|
||||
```shell
|
||||
wasp deploy fly launch <app-name> <region>
|
||||
```
|
||||
|
||||
It accepts the following arguments:
|
||||
|
||||
- `<app-name>` - the name of your app <Required />
|
||||
- `<region>` - the region where your app will be deployed <Required />
|
||||
|
||||
Read how to find the available regions [here](#flyio-regions).
|
||||
|
||||
It gives you the same result as running the following commands:
|
||||
|
||||
```shell
|
||||
wasp deploy fly setup <app-name> <region>
|
||||
wasp deploy fly create-db <region>
|
||||
wasp deploy fly deploy
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the `--server-secret` option:
|
||||
|
||||
```
|
||||
wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>
|
||||
```
|
||||
|
||||
### `setup`
|
||||
|
||||
`setup` will create your client and server apps on Fly, and add some secrets, but does _not_ deploy them.
|
||||
|
||||
```shell
|
||||
wasp deploy fly setup <app-name> <region>
|
||||
```
|
||||
|
||||
It accepts the following arguments:
|
||||
|
||||
- `<app-name>` - the name of your app <Required />
|
||||
- `<region>` - the region where your app will be deployed <Required />
|
||||
|
||||
Read how to find the available regions [here](#flyio-regions).
|
||||
|
||||
After running `setup`, Wasp creates two new files in your project root directory: `fly-server.toml` and `fly-client.toml`.
|
||||
You should include these files in your version control.
|
||||
|
||||
If you want to maintain multiple apps, you can add the `--fly-toml-dir <abs-path>` option to point to different directories, like "dev" or "staging".
|
||||
|
||||
:::caution Execute Only Once
|
||||
You should only run `setup` once per app. If you run it multiple times, it will create unnecessary apps on Fly.
|
||||
:::
|
||||
|
||||
### `create-db`
|
||||
|
||||
`create-db` will create a new database for your app.
|
||||
|
||||
```shell
|
||||
wasp deploy fly create-db <region>
|
||||
```
|
||||
|
||||
It accepts the following arguments:
|
||||
|
||||
- `<region>` - the region where your app will be deployed <Required />
|
||||
|
||||
Read how to find the available regions [here](#flyio-regions).
|
||||
|
||||
:::caution Execute Only Once
|
||||
You should only run `create-db` once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.
|
||||
:::
|
||||
|
||||
### `deploy`
|
||||
|
||||
```shell
|
||||
wasp deploy fly deploy
|
||||
```
|
||||
|
||||
`deploy` pushes your client and server live.
|
||||
|
||||
Run this command whenever you want to **update your deployed app** with the latest changes:
|
||||
|
||||
```shell
|
||||
wasp deploy fly deploy
|
||||
```
|
||||
|
||||
### `cmd`
|
||||
|
||||
If want to run arbitrary Fly commands (e.g. `flyctl secrets list` for your server app), here's how to do it:
|
||||
|
||||
```shell
|
||||
wasp deploy fly cmd secrets list --context server
|
||||
```
|
||||
|
||||
### Fly.io Regions
|
||||
|
||||
> Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.
|
||||
|
||||
<small>
|
||||
|
||||
Read more on Fly regions [here](https://fly.io/docs/reference/regions/).
|
||||
</small>
|
||||
|
||||
You can find the list of all available Fly regions by running:
|
||||
|
||||
```shell
|
||||
flyctl platform regions
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the `secrets set` command:
|
||||
|
||||
```
|
||||
wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server
|
||||
```
|
||||
|
||||
### Mutliple Fly Organizations
|
||||
|
||||
If you have multiple organizations, you can specify a `--org` option. For example:
|
||||
|
||||
```shell
|
||||
wasp deploy fly launch my-wasp-app mia --org hive
|
||||
```
|
||||
|
||||
### Building Locally
|
||||
|
||||
Fly.io offers support for both **locally** built Docker containers and **remotely** built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.
|
||||
|
||||
If you want to build locally, supply the `--build-locally` option to `wasp deploy fly launch` or `wasp deploy fly deploy`.
|
553
web/docs/advanced/deployment/manually.md
Normal file
553
web/docs/advanced/deployment/manually.md
Normal file
@ -0,0 +1,553 @@
|
||||
---
|
||||
title: Deploying Manually
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import AddExternalAuthEnvVarsReminder from './\_addExternalAuthEnvVarsReminder.md'
|
||||
import BuildingTheWebClient from './\_building-the-web-client.md'
|
||||
|
||||
We'll cover how to deploy your Wasp app manually to a variety of providers:
|
||||
|
||||
- [Fly.io](#flyio)
|
||||
- [Netlify](#netlify)
|
||||
- [Railway](#railway)
|
||||
- [Heroku](#heroku)
|
||||
|
||||
## Deploying a Wasp App
|
||||
|
||||
Deploying a Wasp app comes down to the following:
|
||||
|
||||
1. Generating deployable code.
|
||||
1. Deploying the API server (backend).
|
||||
1. Deploying the web client (frontend).
|
||||
1. Deploying a PostgreSQL database and keeping it running.
|
||||
|
||||
Let's go through each of these steps.
|
||||
|
||||
### 1. Generating Deployable Code
|
||||
|
||||
Running the command `wasp build` generates deployable code for the whole app in the `.wasp/build/` directory.
|
||||
|
||||
```
|
||||
wasp build
|
||||
```
|
||||
|
||||
:::caution PostgreSQL in production
|
||||
You won't be able to build the app if you are using SQLite as a database (which is the default database).
|
||||
You'll have to [switch to PostgreSQL](/docs/data-model/backends#migrating-from-sqlite-to-postgresql) before deploying to production.
|
||||
:::
|
||||
|
||||
### 2. Deploying the API Server (backend)
|
||||
|
||||
There's a Dockerfile that defines an image for building the server in the `.wasp/build` directory.
|
||||
|
||||
To run the server in production, deploy this Docker image to a hosting provider and ensure it has access to correct environment variables (this varies depending on the provider).
|
||||
All necessary environment variables are listed in the next section.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
Here are the environment variables your server requires to run:
|
||||
|
||||
- `PORT`
|
||||
|
||||
The server's HTTP port number. This is where the server listens for requests (e.g., `3001`).
|
||||
|
||||
- `DATABASE_URL`
|
||||
|
||||
The URL of the Postgres database you want your app to use (e.g., `postgresql://mydbuser:mypass@localhost:5432/nameofmydb`).
|
||||
|
||||
- `WASP_WEB_CLIENT_URL`
|
||||
|
||||
The URL where you plan to deploy your frontend app is running (e.g., `https://<app-name>.netlify.app`).
|
||||
The server needs to know about it to properly configure Same-Origin Policy (CORS) headers.
|
||||
|
||||
- `JWT_SECRET`
|
||||
|
||||
You only need this environment variable if you're using Wasp's `auth` features.
|
||||
Set it to a random string at least 32 characters long (you can use an [online generator](https://djecrety.ir/)).
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
### 3. Deploying the Web Client (frontend)
|
||||
|
||||
<BuildingTheWebClient />
|
||||
|
||||
The command above will build the web client and put it in the `build/` directory in the `web-app` directory.
|
||||
|
||||
Since the app's frontend is just a bunch of static files, you can deploy it to any static hosting provider.
|
||||
|
||||
### 4. Deploying the Database
|
||||
|
||||
Any PostgreSQL database will do, as long as you set the `DATABASE_URL` env var correctly and ensure that the database is accessible from the server.
|
||||
|
||||
## Different Providers
|
||||
|
||||
We'll cover a few different deployment providers below:
|
||||
|
||||
- Fly.io (server and database)
|
||||
- Netlify (client)
|
||||
- Railway (server, client and database)
|
||||
- Heroku (server and database)
|
||||
|
||||
## Fly.io
|
||||
|
||||
:::tip We automated this process for you
|
||||
If you want to do all of the work below with one command, you can use the [Wasp CLI](/docs/advanced/deployment/cli#flyio).
|
||||
|
||||
Wasp CLI deploys the server, deploys the client, and sets up a database.
|
||||
It also gives you a way to redeploy (update) your app with a single command.
|
||||
:::
|
||||
|
||||
Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/).
|
||||
|
||||
:::note
|
||||
Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.
|
||||
|
||||
Additionally, `fly` is a symlink for `flyctl` on most systems and they can be used interchangeably.
|
||||
:::
|
||||
|
||||
Make sure you are logged in with `flyctl` CLI. You can check if you are logged in with `flyctl auth whoami`, and if you are not, you can log in with `flyctl auth login`.
|
||||
|
||||
### Set Up a Fly.io App
|
||||
|
||||
:::info
|
||||
You need to do this only once per Wasp app.
|
||||
:::
|
||||
|
||||
Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.
|
||||
|
||||
After you have [built the app](#1-generating-deployable-code), position yourself in `.wasp/build/` directory:
|
||||
|
||||
```shell
|
||||
cd .wasp/build
|
||||
```
|
||||
|
||||
Next, run the launch command to set up a new app and create a `fly.toml` file:
|
||||
|
||||
```bash
|
||||
flyctl launch --remote-only
|
||||
```
|
||||
|
||||
This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.
|
||||
|
||||
- Say **yes** to **Would you like to set up a Postgresql database now?** and select **Development**. Fly.io will set a `DATABASE_URL` for you.
|
||||
- Say **no** to **Would you like to deploy now?** (and to any additional questions).
|
||||
|
||||
We still need to set up several environment variables.
|
||||
|
||||
:::info What if the database setup fails?
|
||||
If your attempts to initiate a new app fail for whatever reason, then you should run `flyctl apps destroy <app-name>` before trying again. Fly does not allow you to create multiple apps with the same name.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
What does it look like when your DB is deployed correctly?
|
||||
</summary>
|
||||
<div>
|
||||
<p>When your DB is deployed correctly, you'll see it in the <a href="https://fly.io/dashboard">Fly.io dashboard</a>:</p>
|
||||
<img width="662" alt="image" src="/img/deploying/fly-db.png" />
|
||||
</div>
|
||||
</details>
|
||||
:::
|
||||
|
||||
Next, let's copy the `fly.toml` file up to our Wasp project dir for safekeeping.
|
||||
```shell
|
||||
cp fly.toml ../../
|
||||
```
|
||||
|
||||
Next, let's add a few more environment variables:
|
||||
|
||||
```bash
|
||||
flyctl secrets set PORT=8080
|
||||
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
||||
```
|
||||
|
||||
:::note
|
||||
If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
||||
:::
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.
|
||||
|
||||
### Deploy to a Fly.io App
|
||||
|
||||
While still in the `.wasp/build/` directory, run:
|
||||
|
||||
```bash
|
||||
flyctl deploy --remote-only --config ../../fly.toml
|
||||
```
|
||||
|
||||
This will build and deploy the backend of your Wasp app on Fly.io to `https://<app-name>.fly.dev` 🤘🎸
|
||||
|
||||
Now, if you haven't, you can deploy your frontend and add the client url by running `flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_frontend>`. We suggest using [Netlify](#netlify) for your frontend, but you can use any static hosting provider.
|
||||
|
||||
Additionally, some useful `flyctl` commands:
|
||||
|
||||
```bash
|
||||
flyctl logs
|
||||
flyctl secrets list
|
||||
flyctl ssh console
|
||||
```
|
||||
|
||||
### Redeploying After Wasp Builds
|
||||
|
||||
When you rebuild your Wasp app (with `wasp build`), it will remove your `.wasp/build/` directory. In there, you may have a `fly.toml` from any prior Fly.io deployments.
|
||||
|
||||
While we will improve this process in the future, in the meantime, you have a few options:
|
||||
|
||||
1. Copy the `fly.toml` file to a versioned directory, like your Wasp project dir.
|
||||
|
||||
From there, you can reference it in `flyctl deploy --config <path>` commands, like above.
|
||||
|
||||
1. Backup the `fly.toml` file somewhere before running `wasp build`, and copy it into .wasp/build/ after.
|
||||
|
||||
When the `fly.toml` file exists in .wasp/build/ dir, you do not need to specify the `--config <path>`.
|
||||
|
||||
1. Run `flyctl config save -a <app-name>` to regenerate the `fly.toml` file from the remote state stored in Fly.io.
|
||||
|
||||
## Netlify
|
||||
|
||||
Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and [Netlify CLI](https://docs.netlify.com/cli/get-started/) installed to follow these instructions.
|
||||
|
||||
Make sure you are logged in with Netlify CLI. You can check if you are logged in with `netlify status`, and if you are not, you can log in with `netlify login`.
|
||||
|
||||
First, make sure you have [built the Wasp app](#1-generating-deployable-code). We'll build the client web app next.
|
||||
|
||||
<BuildingTheWebClient />
|
||||
|
||||
We can now deploy the client with:
|
||||
|
||||
```shell
|
||||
netlify deploy
|
||||
```
|
||||
|
||||
<small>
|
||||
|
||||
Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.
|
||||
|
||||
</small>
|
||||
|
||||
The final step is to run:
|
||||
|
||||
```shell
|
||||
netlify deploy --prod`
|
||||
```
|
||||
|
||||
That is it! Your client should be live at `https://<app-name>.netlify.app` ✨
|
||||
|
||||
:::note
|
||||
Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in your server hosting environment (e.g., Fly.io or Heroku).
|
||||
:::
|
||||
|
||||
## Railway
|
||||
|
||||
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/plans) for more info.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To get started, follow these steps:
|
||||
|
||||
1. Make sure your Wasp app is built by running `wasp build` in the project dir.
|
||||
2. Create a [Railway](https://railway.app/) account
|
||||
|
||||
:::tip Free Tier
|
||||
Sign up with your GitHub account to be eligible for the free tier
|
||||
:::
|
||||
|
||||
3. Install the [Railway CLI](https://docs.railway.app/develop/cli#installation)
|
||||
4. Run `railway login` and a browser tab will open to authenticate you.
|
||||
|
||||
### Create New Project
|
||||
|
||||
Let's create our Railway project:
|
||||
|
||||
1. Go to your [Railway dashboard](https://railway.app/dashboard), click on **New Project**, and select `Provision PostgreSQL` from the dropdown menu.
|
||||
2. Once it initializes, right-click on the **New** button in the top right corner and select **Empty Service**.
|
||||
3. Once it initializes, click on it, go to **Settings > General** and change the name to `server`
|
||||
4. Go ahead and create another empty service and name it `client`
|
||||
|
||||
![Changing the name](/img/deploying/railway-rename.png)
|
||||
|
||||
### Deploy Your App to Railway
|
||||
|
||||
#### Setup Domains
|
||||
|
||||
We'll need the domains for both the `server` and `client` services:
|
||||
|
||||
1. Go to the `server` instance's `Settings` tab, and click `Generate Domain`.
|
||||
2. Do the same under the `client`'s `Settings`.
|
||||
|
||||
Copy the domains as we will need them later.
|
||||
|
||||
#### Deploying the Server
|
||||
|
||||
Let's deploy our server first:
|
||||
|
||||
1. Move into your app's `.wasp/build/` directory:
|
||||
|
||||
```shell
|
||||
cd .wasp/build
|
||||
```
|
||||
|
||||
2. Link your app build to your newly created Railway project:
|
||||
|
||||
```shell
|
||||
railway link
|
||||
```
|
||||
|
||||
3. Go into the Railway dashboard and set up the required env variables:
|
||||
|
||||
Open the `Settings` and go to the `Variables` tab:
|
||||
|
||||
- click **Variable reference** and select `DATABASE_URL` (it will populate it with the correct value)
|
||||
- add `WASP_WEB_CLIENT_URL` - enter the the `client` domain (e.g. `https://client-production-XXXX.up.railway.app`)
|
||||
- add `JWT_SECRET` - enter a random string at least 32 characters long (use an [online generator](https://djecrety.ir/))
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
4. Push and deploy the project:
|
||||
|
||||
```shell
|
||||
railway up
|
||||
```
|
||||
|
||||
Select `server` when prompted with `Select Service`.
|
||||
|
||||
Railway will now locate the Dockerfile and deploy your server 👍
|
||||
|
||||
#### Deploying the Client
|
||||
|
||||
1. Next, change into your app's frontend build directory `.wasp/build/web-app`:
|
||||
|
||||
```shell
|
||||
cd web-app
|
||||
```
|
||||
|
||||
2. Create the production build, using the `server` domain as the `REACT_APP_API_URL`:
|
||||
|
||||
```shell
|
||||
npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
|
||||
```
|
||||
|
||||
3. Next, we want to link this specific frontend directory to our project as well:
|
||||
|
||||
```shell
|
||||
railway link
|
||||
```
|
||||
|
||||
4. We need to configure Railway's static hosting for our client.
|
||||
|
||||
:::info Setting Up Static Hosting
|
||||
|
||||
Copy the `build` folder within the `web-app` directory to `dist`:
|
||||
|
||||
```shell
|
||||
cp -r build dist
|
||||
```
|
||||
|
||||
We'll need to create the following files:
|
||||
|
||||
- `Dockerfile` with:
|
||||
|
||||
```Dockerfile title="Dockerfile"
|
||||
FROM pierrezemb/gostatic
|
||||
CMD [ "-fallback", "index.html" ]
|
||||
COPY ./dist/ /srv/http/
|
||||
```
|
||||
|
||||
- `.dockerignore` with:
|
||||
```bash title=".dockerignore"
|
||||
node_modules/
|
||||
```
|
||||
|
||||
You'll need to repeat these steps **each time** you run `wasp build` as it will remove the `.wasp/build/web-app` directory.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Here's a useful shell script to do the process
|
||||
</summary>
|
||||
|
||||
If you want to automate the process, save the following as `deploy_client.sh` in the root of your project:
|
||||
|
||||
```bash title="deploy_client.sh"
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$REACT_APP_API_URL" ]
|
||||
then
|
||||
echo "REACT_APP_API_URL is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wasp build
|
||||
cd .wasp/build/web-app
|
||||
|
||||
npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build
|
||||
|
||||
cp -r build dist
|
||||
|
||||
dockerfile_contents=$(cat <<EOF
|
||||
FROM pierrezemb/gostatic
|
||||
CMD [ "-fallback", "index.html" ]
|
||||
COPY ./dist/ /srv/http/
|
||||
EOF
|
||||
)
|
||||
|
||||
dockerignore_contents=$(cat <<EOF
|
||||
node_modules/
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "$dockerfile_contents" > Dockerfile
|
||||
echo "$dockerignore_contents" > .dockerignore
|
||||
|
||||
railway up
|
||||
```
|
||||
|
||||
Make it executable with:
|
||||
|
||||
```shell
|
||||
chmod +x deploy_client.sh
|
||||
```
|
||||
|
||||
You can run it with:
|
||||
|
||||
```shell
|
||||
REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
|
||||
```
|
||||
|
||||
</details>
|
||||
:::
|
||||
|
||||
5. Set the `PORT` environment variable to `8043` under the `Variables` tab.
|
||||
|
||||
6. Deploy the client and select `client` when prompted with `Select Service`:
|
||||
|
||||
```shell
|
||||
railway up
|
||||
```
|
||||
|
||||
#### Conclusion
|
||||
|
||||
And now your Wasp should be deployed! 🐝 🚂 🚀
|
||||
|
||||
Back in your [Railway dashboard](https://railway.app/dashboard), click on your project and you should see your newly deployed services: Postgres, Server, and Client.
|
||||
|
||||
### Updates & Redeploying
|
||||
|
||||
When you make updates and need to redeploy:
|
||||
|
||||
- run `wasp build` to rebuild your app
|
||||
- run `railway up` in the `.wasp/build` directory (server)
|
||||
- repeat all the steps in the `.wasp/build/web-app` directory (client)
|
||||
|
||||
## Heroku
|
||||
|
||||
:::note
|
||||
Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter
|
||||
|
||||
As such, we recommend using an alternative provider like [Fly.io](#flyio) for your first apps.
|
||||
:::
|
||||
|
||||
You will need Heroku account, `heroku` [CLI](https://devcenter.heroku.com/articles/heroku-cli) and `docker` CLI installed to follow these instructions.
|
||||
|
||||
Make sure you are logged in with `heroku` CLI. You can check if you are logged in with `heroku whoami`, and if you are not, you can log in with `heroku login`.
|
||||
|
||||
### Set Up a Heroku App
|
||||
|
||||
:::info
|
||||
You need to do this only once per Wasp app.
|
||||
:::
|
||||
|
||||
Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:
|
||||
|
||||
```
|
||||
heroku create <app-name>
|
||||
```
|
||||
|
||||
Unless you have an external Postgres database that you want to use, let's create a new database on Heroku and attach it to our app:
|
||||
|
||||
```
|
||||
heroku addons:create --app <app-name> heroku-postgresql:mini
|
||||
```
|
||||
|
||||
:::caution
|
||||
Heroku does not offer a free plan anymore and `mini` is their cheapest database instance - it costs $5/mo.
|
||||
:::
|
||||
|
||||
Heroku will also set `DATABASE_URL` env var for us at this point. If you are using an external database, you will have to set it up yourself.
|
||||
|
||||
The `PORT` env var will also be provided by Heroku, so the only two left to set are the `JWT_SECRET` and `WASP_WEB_CLIENT_URL` env vars:
|
||||
|
||||
```
|
||||
heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||
heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
||||
```
|
||||
|
||||
:::note
|
||||
If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
||||
:::
|
||||
|
||||
### Deploy to a Heroku App
|
||||
|
||||
After you have [built the app](#1-generating-deployable-code), position yourself in `.wasp/build/` directory:
|
||||
|
||||
```shell
|
||||
cd .wasp/build
|
||||
```
|
||||
|
||||
assuming you were at the root of your Wasp project at that moment.
|
||||
|
||||
Log in to Heroku Container Registry:
|
||||
|
||||
```shell
|
||||
heroku container:login
|
||||
```
|
||||
|
||||
Build the docker image and push it to Heroku:
|
||||
|
||||
```shell
|
||||
heroku container:push --app <app-name> web
|
||||
```
|
||||
|
||||
App is still not deployed at this point.
|
||||
This step might take some time, especially the very first time, since there are no cached docker layers.
|
||||
|
||||
:::note Note for Apple Silicon Users
|
||||
Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of `heroku container:push`, users instead should:
|
||||
|
||||
```shell
|
||||
docker buildx build --platform linux/amd64 -t <app-name> .
|
||||
docker tag <app-name> registry.heroku.com/<app-name>/web
|
||||
docker push registry.heroku.com/<app-name>/web
|
||||
```
|
||||
|
||||
You are now ready to proceed to the next step.
|
||||
:::
|
||||
|
||||
Deploy the pushed image and restart the app:
|
||||
|
||||
```shell
|
||||
heroku container:release --app <app-name> web
|
||||
```
|
||||
|
||||
This is it, the backend is deployed at `https://<app-name>-XXXX.herokuapp.com` 🎉
|
||||
|
||||
Find out the exact app URL with:
|
||||
|
||||
```shell
|
||||
heroku info --app <app-name>
|
||||
```
|
||||
|
||||
Additionally, you can check out the logs with:
|
||||
|
||||
```shell
|
||||
heroku logs --tail --app <app-name>
|
||||
```
|
||||
|
||||
:::note Using `pg-boss` with Heroku
|
||||
|
||||
If you wish to deploy an app leveraging [Jobs](/docs/advanced/jobs) that use `pg-boss` as the executor to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
|
||||
|
||||
Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||
:::
|
44
web/docs/advanced/deployment/overview.md
Normal file
44
web/docs/advanced/deployment/overview.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
import { DeploymentOptionsGrid } from './DeploymentOptionsGrid.tsx';
|
||||
|
||||
Wasp apps are full-stack apps that consist of:
|
||||
- A Node.js server.
|
||||
- A static client.
|
||||
- A PostgreSQL database.
|
||||
|
||||
You can deploy each part **anywhere** where you can usually deploy Node.js apps or static apps. For example, you can deploy your client on [Netlify](https://www.netlify.com/), the server on [Fly.io](https://fly.io/), and the database on [Neon](https://neon.tech/).
|
||||
|
||||
To make deploying as smooth as possible, Wasp also offers a single-command deployment through the **Wasp CLI**. Read more about deploying through the CLI [here](/docs/advanced/deployment/cli).
|
||||
|
||||
<DeploymentOptionsGrid />
|
||||
|
||||
Regardless of how you choose to deploy your app (i.e., manually or using the Wasp CLI), you'll need to know about some common patterns covered below.
|
||||
|
||||
## Customizing the Dockerfile
|
||||
By default, Wasp generates a multi-stage Dockerfile.
|
||||
This file is used to build and run a Docker image with the Wasp-generated server code.
|
||||
It also runs any pending migrations.
|
||||
|
||||
You can **add extra steps to this multi-stage `Dockerfile`** by creating your own `Dockerfile` in the project's root directory.
|
||||
If Wasp finds a Dockerfile in the project's root, it appends its contents at the _bottom_ of the default multi-stage Dockerfile.
|
||||
|
||||
Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages.
|
||||
You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.
|
||||
|
||||
A few things to keep in mind:
|
||||
|
||||
- If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
|
||||
- The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
|
||||
- Make sure to supply `ENTRYPOINT` in your final build stage. Your changes won't have any effect if you don't.
|
||||
|
||||
Read more in the official Docker docs on [multi-stage builds](https://docs.docker.com/build/building/multi-stage/).
|
||||
|
||||
To see what your project's (potentially combined) Dockerfile will look like, run:
|
||||
```shell
|
||||
wasp dockerfile
|
||||
```
|
||||
|
||||
Join our [Discord](https://discord.gg/rzdnErX) if you have any questions, or if you need more customization than this hook provides.
|
366
web/docs/advanced/email.md
Normal file
366
web/docs/advanced/email.md
Normal file
@ -0,0 +1,366 @@
|
||||
---
|
||||
title: Sending Emails
|
||||
---
|
||||
|
||||
import SendingEmailsInDevelopment from '../\_sendingEmailsInDevelopment.md'
|
||||
|
||||
import { Required } from '@site/src/components/Required'
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'
|
||||
|
||||
# Sending Emails
|
||||
|
||||
With Wasp's email sending feature, you can easily integrate email functionality into your web application.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Choose from one of the providers:
|
||||
|
||||
- `Mailgun`,
|
||||
- `SendGrid`
|
||||
- or the good old `SMTP`.
|
||||
|
||||
Optionally, define the `defaultFrom` field, so you don't need to provide it whenever sending an email.
|
||||
|
||||
## Sending Emails
|
||||
|
||||
<SendingEmailsInDevelopment />
|
||||
|
||||
Before jumping into details about setting up various providers, let's see how easy it is to send emails.
|
||||
|
||||
You import the `emailSender` that is provided by the `@wasp/email` module and call the `send` method on it.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/actions/sendEmail.js"
|
||||
import { emailSender } from "@wasp/email/index.js";
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: "John Doe",
|
||||
email: "john@doe.com",
|
||||
},
|
||||
to: "user@domain.com",
|
||||
subject: "Saying hello",
|
||||
text: "Hello world",
|
||||
html: "Hello <strong>world</strong>",
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/actions/sendEmail.ts"
|
||||
import { emailSender } from "@wasp/email/index.js";
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: "John Doe",
|
||||
email: "john@doe.com",
|
||||
},
|
||||
to: "user@domain.com",
|
||||
subject: "Saying hello",
|
||||
text: "Hello world",
|
||||
html: "Hello <strong>world</strong>",
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the `send` method in the [API Reference](#javascript-api).
|
||||
|
||||
The `send` method returns an object with the status of the sent email. It varies depending on the provider you use.
|
||||
|
||||
## Providers
|
||||
|
||||
For each provider, you'll need to set up env variables in the `.env.server` file at the root of your project.
|
||||
|
||||
### Using the SMTP Provider
|
||||
|
||||
First, set the provider to `SMTP` in your `main.wasp` file.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Then, add the following env variables to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SMTP_HOST=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_PORT=
|
||||
```
|
||||
|
||||
Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.
|
||||
|
||||
### Using the Mailgun Provider
|
||||
|
||||
Set the provider to `Mailgun` in the `main.wasp` file.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: Mailgun,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: Mailgun,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Then, get the Mailgun API key and domain and add them to your `.env.server` file.
|
||||
|
||||
#### Getting the API Key and Domain
|
||||
|
||||
1. Go to [Mailgun](https://www.mailgun.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
|
||||
5. Copy the domain and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
MAILGUN_API_KEY=
|
||||
MAILGUN_DOMAIN=
|
||||
```
|
||||
|
||||
### Using the SendGrid Provider
|
||||
|
||||
Set the provider field to `SendGrid` in your `main.wasp` file.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Then, get the SendGrid API key and add it to your `.env.server` file.
|
||||
|
||||
#### Getting the API Key
|
||||
|
||||
1. Go to [SendGrid](https://sendgrid.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.sendgrid.com/settings/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SENDGRID_API_KEY=
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `emailSender` dict
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `emailSender` dict has the following fields:
|
||||
|
||||
- `provider: Provider` <Required />
|
||||
|
||||
The provider you want to use. Choose from `SMTP`, `Mailgun` or `SendGrid`.
|
||||
|
||||
- `defaultFrom: dict`
|
||||
|
||||
The default sender's details. If you set this field, you don't need to provide the `from` field when sending an email.
|
||||
|
||||
### JavaScript API
|
||||
|
||||
Using the `emailSender` in <ShowForTs>Typescript</ShowForTs><ShowForJs>JavaScript</ShowForJs>:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/actions/sendEmail.js"
|
||||
import { emailSender } from "@wasp/email/index.js";
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: "John Doe",
|
||||
email: "john@doe.com",
|
||||
},
|
||||
to: "user@domain.com",
|
||||
subject: "Saying hello",
|
||||
text: "Hello world",
|
||||
html: "Hello <strong>world</strong>",
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/actions/sendEmail.ts"
|
||||
import { emailSender } from "@wasp/email/index.js";
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: "John Doe",
|
||||
email: "john@doe.com",
|
||||
},
|
||||
to: "user@domain.com",
|
||||
subject: "Saying hello",
|
||||
text: "Hello world",
|
||||
html: "Hello <strong>world</strong>",
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `send` method accepts an object with the following fields:
|
||||
|
||||
- `from: object`
|
||||
|
||||
The sender's details. If you set up `defaultFrom` field in the `emailSender` dict in Wasp file, this field is optional.
|
||||
|
||||
- `name: string`
|
||||
|
||||
The name of the sender.
|
||||
|
||||
- `email: string`
|
||||
|
||||
The email address of the sender.
|
||||
|
||||
- `to: string` <Required />
|
||||
|
||||
The recipient's email address.
|
||||
|
||||
- `subject: string` <Required />
|
||||
|
||||
The subject of the email.
|
||||
|
||||
- `text: string` <Required />
|
||||
|
||||
The text version of the email.
|
||||
|
||||
- `html: string` <Required />
|
||||
|
||||
The HTML version of the email
|
392
web/docs/advanced/jobs.md
Normal file
392
web/docs/advanced/jobs.md
Normal file
@ -0,0 +1,392 @@
|
||||
---
|
||||
title: Recurring Jobs
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required.tsx'
|
||||
|
||||
If you have server tasks that you do not want to handle as part of the normal request-response cycle, Wasp allows you to make that function a `job` and it will gain some "superpowers."
|
||||
|
||||
Jobs will:
|
||||
* persist between server restarts
|
||||
* can be retried if they fail
|
||||
* can be delayed until the future
|
||||
* can have a recurring schedule!
|
||||
|
||||
Some examples where you may want to use a `job` on the server include sending an email, making an HTTP request to some external API, or doing some nightly calculations.
|
||||
|
||||
### Basic Job Definition and Usage
|
||||
|
||||
To declare a `job` in Wasp, simply add a declaration with a reference to an `async` function, like the following:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Then, in your [Operations](/docs/data-model/operations/overview) or [setupFn](/docs/project/server-config#setup-function) (or any other NodeJS code), you can submit work to be done:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="someAction.js"
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
|
||||
const submittedJob = await mySpecialJob.submit({ job: "args" })
|
||||
console.log(await submittedJob.pgBoss.details())
|
||||
|
||||
// Or, if you'd prefer it to execute in the future, just add a .delay().
|
||||
// It takes a number of seconds, Date, or ISO date string.
|
||||
await mySpecialJob.delay(10).submit({ job: "args" })
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="someAction.ts"
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
|
||||
const submittedJob = await mySpecialJob.submit({ job: "args" })
|
||||
console.log(await submittedJob.pgBoss.details())
|
||||
|
||||
// Or, if you'd prefer it to execute in the future, just add a .delay().
|
||||
// It takes a number of seconds, Date, or ISO date string.
|
||||
await mySpecialJob.delay(10).submit({ job: "args" })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`.
|
||||
|
||||
Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function.
|
||||
|
||||
### Recurring Jobs
|
||||
|
||||
If you have work that needs to be done on some recurring basis, you can add a `schedule` to your job declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {6-9} title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 * * * *",
|
||||
args: {=json { "job": "args" } json=} // optional
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {6-9} title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js"
|
||||
},
|
||||
schedule: {
|
||||
cron: "0 * * * *",
|
||||
args: {=json { "job": "args" } json=} // optional
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
In this example, you do _not_ need to invoke anything in JavaScript. You can imagine `foo({ job: "args" })` getting automatically scheduled and invoked for you every hour.
|
||||
|
||||
### Fully Specified Example
|
||||
Both `perform` and `schedule` accept `executorOptions`, which we pass directly to the named job executor when you submit jobs. In this example, the scheduled job will have a `retryLimit` set to 0, as `schedule` overrides any similar property from `perform`. Lastly, we add an entity to pass in via the context argument to `perform.fn`.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js",
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 1 } json=}
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
cron: "*/5 * * * *",
|
||||
args: {=json { "foo": "bar" } json=},
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 0 } json=}
|
||||
}
|
||||
},
|
||||
entities: [Task],
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js",
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 1 } json=}
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
cron: "*/5 * * * *",
|
||||
args: {=json { "foo": "bar" } json=},
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 0 } json=}
|
||||
}
|
||||
},
|
||||
entities: [Task],
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## API Reference
|
||||
|
||||
### Fields
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js",
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 1 } json=}
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
cron: "*/5 * * * *",
|
||||
args: {=json { "foo": "bar" } json=},
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 0 } json=}
|
||||
}
|
||||
},
|
||||
entities: [Task],
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
job mySpecialJob {
|
||||
executor: PgBoss,
|
||||
perform: {
|
||||
fn: import { foo } from "@server/workers/bar.js",
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 1 } json=}
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
cron: "*/5 * * * *",
|
||||
args: {=json { "foo": "bar" } json=},
|
||||
executorOptions: {
|
||||
pgBoss: {=json { "retryLimit": 0 } json=}
|
||||
}
|
||||
},
|
||||
entities: [Task],
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `job` declaration has the following fields:
|
||||
|
||||
- `executor: JobExecutor` <Required />
|
||||
|
||||
:::note Job executors
|
||||
Our jobs need job executors to handle the _scheduling, monitoring, and execution_.
|
||||
|
||||
Wasp allows you to choose which job executor will be used to execute a specific job that you define, which affects some of the finer details of how jobs will behave and how they can be further configured. Each job executor has its pros and cons, which we will explain in more detail below, so you can pick the one that best suits your needs.
|
||||
:::
|
||||
|
||||
`PgBoss` is currently our only job executor, and is recommended for low-volume production use cases. It requires your `app.db.system` to be `PostgreSQL`.
|
||||
|
||||
We have selected [pg-boss](https://github.com/timgit/pg-boss/) as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/)) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.
|
||||
|
||||
:::info
|
||||
Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.
|
||||
|
||||
<details>
|
||||
<summary>pg-boss details</summary>
|
||||
|
||||
pg-boss provides many useful features, which can be found [here](https://github.com/timgit/pg-boss/blob/8.4.2/README.md).
|
||||
|
||||
When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called `pgboss` with some internal tracking tables, including `job` and `schedule`. pg-boss tables have a `name` column in most tables that will correspond to your `job` identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.
|
||||
|
||||
If you need to customize the creation of the pg-boss instance, you can set an environment variable called `PG_BOSS_NEW_OPTIONS` to a stringified JSON object containing [these initialization parameters](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#newoptions). **NOTE**: Setting this overwrites all Wasp defaults, so you must include database connection information as well.
|
||||
|
||||
### pg-boss considerations
|
||||
- Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
|
||||
- Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
|
||||
- The job name/identifier in your `.wasp` file is the same name that will be used in the `name` column of pg-boss tables. If you change a name that had a `schedule` associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the `schedule` table in the `pgboss` schema of your database.
|
||||
- If you remove a `schedule` from a job, you will need to do the above as well.
|
||||
- If you wish to deploy to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
|
||||
- https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||
|
||||
</details>
|
||||
|
||||
:::
|
||||
|
||||
- `perform: dict` <Required />
|
||||
|
||||
- `fn: ServerImport` <Required />
|
||||
|
||||
An `async` JavaScript function of work to be performed. Since Wasp executes jobs on the server, you must import it from `@server`. The function receives a first argument which may be passed when the job is called, as well as the context containing any declared entities as the second (this is passed automatically by Wasp). Here is a sample signature:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="bar.js"
|
||||
export async function foo(args, context) {
|
||||
// Can reference context.entities.Task, for example.
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="bar.ts"
|
||||
export async function foo(args, context) {
|
||||
// Can reference context.entities.Task, for example.
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- `executorOptions: dict`
|
||||
|
||||
Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with `submit()` or in a `schedule`.
|
||||
|
||||
- `pgBoss: JSON`
|
||||
|
||||
See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options).
|
||||
|
||||
- `schedule: dict`
|
||||
|
||||
- `cron: string` <Required />
|
||||
|
||||
A 5-placeholder format cron expression string. See rationale for minute-level precision [here](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#scheduling).
|
||||
|
||||
_If you need help building cron expressions, Check out_ <em>[Crontab guru](https://crontab.guru/#0_*_*_*_*).</em>
|
||||
|
||||
- `args: JSON`
|
||||
|
||||
The arguments to pass to the `perform.fn` function when invoked.
|
||||
|
||||
- `executorOptions: dict`
|
||||
|
||||
Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The `perform.executorOptions` are the default options, and `schedule.executorOptions` can override/extend those.
|
||||
|
||||
- `pgBoss: JSON`
|
||||
|
||||
See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#sendname-data-options).
|
||||
|
||||
- `entities: [Entity]`
|
||||
|
||||
A list of entities you wish to use inside your Job (similar to [Queries and Actions](/docs/data-model/operations/queries#using-entities-in-queries)).
|
||||
|
||||
### JavaScript API
|
||||
|
||||
- `import`
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="someAction.js"
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="someAction.ts"
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- `submit(jobArgs, executorOptions)`
|
||||
- `jobArgs: JSON`
|
||||
- `executorOptions: JSON`
|
||||
|
||||
Submits a `job` to be executed by an executor, optionally passing in a JSON job argument your job handler function will receive, and executor-specific submit options.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="someAction.js"
|
||||
const submittedJob = await mySpecialJob.submit({ job: "args" })
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```js title="someAction.ts"
|
||||
const submittedJob = await mySpecialJob.submit({ job: "args" })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- `delay(startAfter)`
|
||||
- `startAfter: int | string | Date` <Required />
|
||||
|
||||
Delaying the invocation of the job handler. The delay can be one of:
|
||||
- Integer: number of seconds to delay. [Default 0]
|
||||
- String: ISO date string to run at.
|
||||
- Date: Date to run at.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="someAction.js"
|
||||
const submittedJob = await mySpecialJob.delay(10).submit({ job: "args" }, { "retryLimit": 2 })
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="someAction.ts"
|
||||
const submittedJob = await mySpecialJob.delay(10).submit({ job: "args" }, { "retryLimit": 2 })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Tracking
|
||||
The return value of `submit()` is an instance of `SubmittedJob`, which minimally contains:
|
||||
- `jobId`: A getter returning the UUID String ID for the job in that executor.
|
||||
- `jobName`: A getter returning the name of the job you used in your `.wasp` file.
|
||||
- `executorName`: A getter returning a Symbol of the name of the job executor.
|
||||
- For pg-boss, you can import a Symbol from: `import { PG_BOSS_EXECUTOR_NAME } from '@wasp/jobs/core/pgBoss/pgBossJob.js'` if you wish to compare against `executorName`.
|
||||
|
||||
There are also some namespaced, job executor-specific objects.
|
||||
|
||||
- For pg-boss, you may access: `pgBoss`
|
||||
- **NOTE**: no arguments are necessary, as we already applied the `jobId` in the available functions.
|
||||
- `details()`: pg-boss specific job detail information. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#getjobbyidid)
|
||||
- `cancel()`: attempts to cancel a job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#cancelid)
|
||||
- `resume()`: attempts to resume a canceled job. [Reference](https://github.com/timgit/pg-boss/blob/8.4.2/docs/readme.md#resumeid)
|
280
web/docs/advanced/middleware-config.md
Normal file
280
web/docs/advanced/middleware-config.md
Normal file
@ -0,0 +1,280 @@
|
||||
---
|
||||
title: Configuring Middleware
|
||||
---
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-`api`/path basis.
|
||||
|
||||
## Default Global Middleware 🌍
|
||||
|
||||
Wasp's Express server has the following middleware by default:
|
||||
|
||||
- [Helmet](https://helmetjs.github.io/): Helmet helps you secure your Express apps by setting various HTTP headers. _It's not a silver bullet, but it's a good start._
|
||||
- [CORS](https://github.com/expressjs/cors#readme): CORS is a package for providing a middleware that can be used to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options.
|
||||
|
||||
:::note
|
||||
CORS middleware is required for the frontend to communicate with the backend.
|
||||
:::
|
||||
- [Morgan](https://github.com/expressjs/morgan#readme): HTTP request logger middleware.
|
||||
- [express.json](https://expressjs.com/en/api.html#express.json) (which uses [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)): parses incoming request bodies in a middleware before your handlers, making the result available under the `req.body` property.
|
||||
|
||||
:::note
|
||||
JSON middlware is required for [Operations](/docs/data-model/operations/overview) to function properly.
|
||||
:::
|
||||
- [express.urlencoded](https://expressjs.com/en/api.html#express.urlencoded) (which uses [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions)): returns middleware that only parses urlencoded bodies and only looks at requests where the `Content-Type` header matches the type option.
|
||||
- [cookieParser](https://github.com/expressjs/cookie-parser#readme): parses Cookie header and populates `req.cookies` with an object keyed by the cookie names.
|
||||
|
||||
## Customization
|
||||
|
||||
You have three places where you can customize middleware:
|
||||
1. [global](#1-customize-global-middleware): here, any changes will apply by default *to all operations (`query` and `action`) and `api`.* This is helpful if you wanted to add support for multiple domains to CORS, for example.
|
||||
|
||||
:::caution Modifying global middleware
|
||||
Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.
|
||||
:::
|
||||
|
||||
2. [per-api](#2-customize-api-specific-middleware): you can override middleware for a specific api route (e.g. `POST /webhook/callback`). This is helpful if you want to disable JSON parsing for some callback, for example.
|
||||
3. [per-path](#3-customize-per-path-middleware): this is helpful if you need to customize middleware for all methods under a given path.
|
||||
- It's helpful for things like "complex CORS requests" which may need to apply to both `OPTIONS` and `GET`, or to apply some middleware to a _set of `api` routes_.
|
||||
|
||||
### Default Middleware Definitions
|
||||
|
||||
Below is the actual definitions of default middleware which you can override.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const defaultGlobalMiddleware = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
// Used in the examples below 👇
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 1. Customize Global Middleware
|
||||
|
||||
If you would like to modify the middleware for _all_ operations and APIs, you can do something like:
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {6} title=main.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/serverSetup.js
|
||||
import cors from 'cors'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
export const serverMiddlewareFn = (middlewareConfig) => {
|
||||
// Example of adding extra domains to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
|
||||
```wasp {6} title=main.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/serverSetup.ts
|
||||
import cors from 'cors'
|
||||
import type { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// Example of adding an extra domains to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## 2. Customize `api`-specific Middleware
|
||||
|
||||
If you would like to modify the middleware for a single API, you can do something like:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {5} title=main.wasp
|
||||
// ...
|
||||
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.js
|
||||
import express from 'express'
|
||||
|
||||
export const webhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
|
||||
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {5} title=main.wasp
|
||||
// ...
|
||||
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
import express from 'express'
|
||||
import { WebhookCallback } from '@wasp/apis/types'
|
||||
import type { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const webhookCallback: WebhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
This gets installed on a per-method basis. Behind the scenes, this results in code like:
|
||||
|
||||
```js
|
||||
router.post('/webhook/callback', webhookCallbackMiddleware, ...)
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Customize Per-Path Middleware
|
||||
|
||||
If you would like to modify the middleware for all API routes under some common path, you can define a `middlewareConfigFn` on an `apiNamespace`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {4} title=main.wasp
|
||||
// ...
|
||||
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.js
|
||||
export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
|
||||
const customMiddleware = (_req, _res, next) => {
|
||||
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.middleware', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {4} title=main.wasp
|
||||
// ...
|
||||
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
import express from 'express'
|
||||
import type { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
const customMiddleware: express.RequestHandler = (_req, _res, next) => {
|
||||
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.middleware', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
This gets installed at the router level for the path. Behind the scenes, this results in something like:
|
||||
|
||||
```js
|
||||
router.use('/foo/bar', fooBarNamespaceMiddleware)
|
||||
```
|
||||
:::
|
335
web/docs/advanced/web-sockets.md
Normal file
335
web/docs/advanced/web-sockets.md
Normal file
@ -0,0 +1,335 @@
|
||||
---
|
||||
title: Web Sockets
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
Wasp provides a fully integrated WebSocket experience by utilizing [Socket.IO](https://socket.io/) on the client and server.
|
||||
|
||||
We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful `useSocket` and `useSocketListener` abstractions for use in React components.
|
||||
|
||||
To get started, you need to:
|
||||
1. Define your WebSocket logic on the server.
|
||||
2. Enable WebSockets in your Wasp file, and connect it with your server logic.
|
||||
3. Use WebSockets on the client, in React, via `useSocket` and `useSocketListener`.
|
||||
4. Optionally, type the WebSocket events and payloads for full-stack type safety.
|
||||
|
||||
Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.
|
||||
|
||||
## Turn On WebSockets in Your Wasp File
|
||||
We specify that we are using WebSockets by adding `webSocket` to our `app` and providing the required `fn`. You can optionally change the auto-connect behavior.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/webSocket.js",
|
||||
autoConnect: true, // optional, default: true
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/webSocket.js",
|
||||
autoConnect: true, // optional, default: true
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Defining the Events Handler
|
||||
Let's define the WebSockets server with all of the events and handler functions.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
:::info Full-stack type safety
|
||||
Check this out: we'll define the event types and payloads on the server, and they will be **automatically exposed on the client**. This helps you avoid mistakes when emitting events or handling them.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
### `webSocketFn` Function
|
||||
On the server, you will get Socket.IO `io: Server` argument and `context` for your WebSocket function. The `context` object give you access to all of the entities from your Wasp app.
|
||||
|
||||
You can use this `io` object to register callbacks for all the regular [Socket.IO events](https://socket.io/docs/v4/server-api/). Also, if a user is logged in, you will have a `socket.data.user` on the server.
|
||||
|
||||
This is how we can define our `webSocketFn` function:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title=src/server/webSocket.js
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const webSocketFn = (io, context) => {
|
||||
io.on('connection', (socket) => {
|
||||
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
|
||||
console.log('a user connected: ', username)
|
||||
|
||||
socket.on('chatMessage', async (msg) => {
|
||||
console.log('message: ', msg)
|
||||
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
|
||||
// You can also use your entities here:
|
||||
// await context.entities.SomeEntity.create({ someField: msg })
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/webSocket.ts
|
||||
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const webSocketFn: WebSocketFn = (io, context) => {
|
||||
io.on('connection', (socket) => {
|
||||
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
|
||||
console.log('a user connected: ', username)
|
||||
|
||||
socket.on('chatMessage', async (msg) => {
|
||||
console.log('message: ', msg)
|
||||
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
|
||||
// You can also use your entities here:
|
||||
// await context.entities.SomeEntity.create({ someField: msg })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Typing our WebSocket function with the events and payloads
|
||||
// allows us to get type safety on the client as well
|
||||
|
||||
type WebSocketFn = WebSocketDefinition<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>
|
||||
|
||||
interface ServerToClientEvents {
|
||||
chatMessage: (msg: { id: string, username: string, text: string }) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
chatMessage: (msg: string) => void;
|
||||
}
|
||||
|
||||
interface InterServerEvents {}
|
||||
|
||||
// Data that is attached to the socket.
|
||||
// NOTE: Wasp automatically injects the JWT into the connection,
|
||||
// and if present/valid, the server adds a user to the socket.
|
||||
interface SocketData extends WaspSocketData {}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using the WebSocket On The Client
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
:::info Full-stack type safety
|
||||
All the hooks we use are typed with the events and payloads you defined on the server. VS Code will give you autocomplete for the events and payloads, and you will get type errors if you make a mistake.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
### `useSocket` Hook
|
||||
|
||||
Client access to WebSockets is provided by the `useSocket` hook. It returns:
|
||||
- `socket: Socket` for sending and receiving events.
|
||||
- `isConnected: boolean` for showing a display of the Socket.IO connection status.
|
||||
- Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly `socket.connect()` or `socket.disconnect()`.
|
||||
- If you set `autoConnect: false` in your Wasp file, then you should call these as needed.
|
||||
|
||||
All components using `useSocket` share the same underlying `socket`.
|
||||
|
||||
### `useSocketListener` Hook
|
||||
|
||||
Additionally, there is a `useSocketListener: (event, callback) => void` hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title=src/client/ChatPage.jsx
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
useSocket,
|
||||
useSocketListener,
|
||||
} from '@wasp/webSocket'
|
||||
|
||||
export const ChatPage = () => {
|
||||
const [messageText, setMessageText] = useState('')
|
||||
const [messages, setMessages] = useState([])
|
||||
const { socket, isConnected } = useSocket()
|
||||
|
||||
useSocketListener('chatMessage', logMessage)
|
||||
|
||||
function logMessage(msg) {
|
||||
setMessages((priorMessages) => [msg, ...priorMessages])
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
socket.emit('chatMessage', messageText)
|
||||
setMessageText('')
|
||||
}
|
||||
|
||||
const messageList = messages.map((msg) => (
|
||||
<li key={msg.id}>
|
||||
<em>{msg.username}</em>: {msg.text}
|
||||
</li>
|
||||
))
|
||||
const connectionIcon = isConnected ? '🟢' : '🔴'
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Chat {connectionIcon}</h2>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ul>{messageList}</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
Wasp's **full-stack type safety** kicks in here: all the event types and payloads are automatically inferred from the server and are available on the client 🔥
|
||||
|
||||
You can additonally use the `ClientToServerPayload` and `ServerToClientPayload` helper types to get the payload type for a specific event.
|
||||
|
||||
```tsx title=src/client/ChatPage.tsx
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
useSocket,
|
||||
useSocketListener,
|
||||
ServerToClientPayload,
|
||||
} from '@wasp/webSocket'
|
||||
|
||||
export const ChatPage = () => {
|
||||
const [messageText, setMessageText] = useState<
|
||||
// We are using a helper type to get the payload type for the "chatMessage" event.
|
||||
ClientToServerPayload<'chatMessage'>
|
||||
>('')
|
||||
const [messages, setMessages] = useState<
|
||||
ServerToClientPayload<'chatMessage'>[]
|
||||
>([])
|
||||
// The "socket" instance is typed with the types you defined on the server.
|
||||
const { socket, isConnected } = useSocket()
|
||||
|
||||
// This is a type-safe event handler: "chatMessage" event and its payload type
|
||||
// are defined on the server.
|
||||
useSocketListener('chatMessage', logMessage)
|
||||
|
||||
function logMessage(msg: ServerToClientPayload<'chatMessage'>) {
|
||||
setMessages((priorMessages) => [msg, ...priorMessages])
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
// This is a type-safe event emitter: "chatMessage" event and its payload type
|
||||
// are defined on the server.
|
||||
socket.emit('chatMessage', messageText)
|
||||
setMessageText('')
|
||||
}
|
||||
|
||||
const messageList = messages.map((msg) => (
|
||||
<li key={msg.id}>
|
||||
<em>{msg.username}</em>: {msg.text}
|
||||
</li>
|
||||
))
|
||||
const connectionIcon = isConnected ? '🟢' : '🔴'
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Chat {connectionIcon}</h2>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ul>{messageList}</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## API Reference
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/webSocket.js",
|
||||
autoConnect: true, // optional, default: true
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/webSocket.js",
|
||||
autoConnect: true, // optional, default: true
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `webSocket` dict has the following fields:
|
||||
|
||||
- `fn: WebSocketFn` <Required />
|
||||
|
||||
The function that defines the WebSocket events and handlers.
|
||||
|
||||
- `autoConnect: bool`
|
||||
|
||||
Whether to automatically connect to the WebSocket server. Default: `true`.
|
15
web/docs/auth/Pills.css
Normal file
15
web/docs/auth/Pills.css
Normal file
@ -0,0 +1,15 @@
|
||||
:root {
|
||||
--auth-pills-color: #333;
|
||||
--auth-pills-email: #e0f2fe;
|
||||
--auth-pills-github: #f1f5f9;
|
||||
--auth-pills-google: #ecfccb;
|
||||
--auth-pills-username-and-pass: #fce7f3;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--auth-pills-color: #fff;
|
||||
--auth-pills-email: #0c4a6e;
|
||||
--auth-pills-github: #334155;
|
||||
--auth-pills-google: #365314;
|
||||
--auth-pills-username-and-pass: #831843;
|
||||
}
|
48
web/docs/auth/Pills.jsx
Normal file
48
web/docs/auth/Pills.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import './Pills.css';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
export function Pill({ children, linkToPage, style = {} }) {
|
||||
return <Link to={linkToPage}
|
||||
style={{
|
||||
padding: "0.1rem 0.5rem",
|
||||
borderRadius: "0.375rem",
|
||||
color: "var(--auth-pills-color)",
|
||||
textDecoration: "none",
|
||||
display: "inline-block",
|
||||
...style,
|
||||
}}
|
||||
>{children}</Link>;
|
||||
}
|
||||
|
||||
/*
|
||||
:root {
|
||||
--auth-pills-email: #e0f2fe;
|
||||
--auth-pills-github: #f1f5f9;
|
||||
--auth-pills-google: #ecfccb;
|
||||
--auth-pills-username-and-pass: #fce7f3;
|
||||
}
|
||||
*/
|
||||
export function EmailPill() {
|
||||
return <Pill style={{
|
||||
backgroundColor: "var(--auth-pills-email)",
|
||||
}} linkToPage="/docs/auth/email">Email</Pill>;
|
||||
}
|
||||
|
||||
export function UsernameAndPasswordPill() {
|
||||
return <Pill style={{
|
||||
backgroundColor: "var(--auth-pills-username-and-pass)",
|
||||
}} linkToPage="/docs/auth/username-and-pass">Username & Password</Pill>;
|
||||
}
|
||||
|
||||
export function GithubPill() {
|
||||
return <Pill style={{
|
||||
backgroundColor: "var(--auth-pills-github)",
|
||||
}} linkToPage="/docs/auth/social-auth/github">Github</Pill>;
|
||||
}
|
||||
|
||||
export function GooglePill() {
|
||||
return <Pill style={{
|
||||
backgroundColor: "var(--auth-pills-google)",
|
||||
}} linkToPage="/docs/auth/social-auth/google">Google</Pill>;
|
||||
}
|
914
web/docs/auth/email.md
Normal file
914
web/docs/auth/email.md
Normal file
@ -0,0 +1,914 @@
|
||||
---
|
||||
title: Email
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.
|
||||
|
||||
![Auth UI](/img/authui/all_screens.gif)
|
||||
|
||||
:::caution Using email auth and social auth together
|
||||
If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the `User` entity), they'll be able to reset their password and login with e-mail and password ✅
|
||||
|
||||
If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that ❌
|
||||
|
||||
In the future, we will lift this limitation and enable smarter merging of accounts.
|
||||
:::
|
||||
|
||||
## Setting Up Email Authentication
|
||||
|
||||
We'll need to take the following steps to set up email authentication:
|
||||
1. Enable email authentication in the Wasp file
|
||||
1. Add the user entity
|
||||
1. Add the routes and pages
|
||||
1. Use Auth UI components in our pages
|
||||
1. Set up the email sender
|
||||
|
||||
Structure of the `main.wasp` file we will end up with:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// Configuring e-mail authentication
|
||||
app myApp {
|
||||
auth: { ... }
|
||||
}
|
||||
|
||||
// Defining User entity
|
||||
entity User { ... }
|
||||
|
||||
// Defining routes and pages
|
||||
route SignupRoute { ... }
|
||||
page SignupPage { ... }
|
||||
// ...
|
||||
```
|
||||
|
||||
### 1. Enable Email Authentication in `main.wasp`
|
||||
|
||||
Let's start with adding the following to our `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// 1. Specify the user entity (we'll define it next)
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// 2. Enable email authentication
|
||||
email: {
|
||||
// 3. Specify the email from field
|
||||
fromField: {
|
||||
name: "My App Postman",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
// 4. Specify the email verification and password reset options (we'll talk about them later)
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
},
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/"
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// 1. Specify the user entity (we'll define it next)
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// 2. Enable email authentication
|
||||
email: {
|
||||
// 3. Specify the email from field
|
||||
fromField: {
|
||||
name: "My App Postman",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
// 4. Specify the email verification and password reset options (we'll talk about them later)
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
},
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/"
|
||||
},
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the `email` auth method options [here](#fields-in-the-email-dict).
|
||||
|
||||
### 2. Add the User Entity
|
||||
|
||||
When email authentication is enabled, Wasp expects certain fields in your `userEntity`. Let's add these fields to our `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {4-8}
|
||||
// 5. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
// Add your own fields below
|
||||
// ...
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {4-8}
|
||||
// 5. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
// Add your own fields below
|
||||
// ...
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the `userEntity` fields [here](#userentity-fields).
|
||||
|
||||
### 3. Add the Routes and Pages
|
||||
|
||||
Next, we need to define the routes and pages for the authentication pages.
|
||||
|
||||
Add the following to the `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.jsx"
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { Signup } from "@client/pages/auth.jsx"
|
||||
}
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { RequestPasswordReset } from "@client/pages/auth.jsx",
|
||||
}
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth.jsx",
|
||||
}
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth.jsx",
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.tsx"
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { Signup } from "@client/pages/auth.tsx"
|
||||
}
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { RequestPasswordReset } from "@client/pages/auth.tsx",
|
||||
}
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth.tsx",
|
||||
}
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth.tsx",
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We'll define the React components for these pages in the `client/pages/auth.{jsx,tsx}` file below.
|
||||
|
||||
### 4. Create the Client Pages
|
||||
|
||||
:::info
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/pages/auth.jsx"
|
||||
import { LoginForm } from "@wasp/auth/forms/Login";
|
||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||
import { VerifyEmailForm } from "@wasp/auth/forms/VerifyEmail";
|
||||
import { ForgotPasswordForm } from "@wasp/auth/forms/ForgotPassword";
|
||||
import { ResetPasswordForm } from "@wasp/auth/forms/ResetPassword";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Forgot your password? <Link to="/request-password-reset">reset it</Link>
|
||||
.
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function Signup() {
|
||||
return (
|
||||
<Layout>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailVerification() {
|
||||
return (
|
||||
<Layout>
|
||||
<VerifyEmailForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function RequestPasswordReset() {
|
||||
return (
|
||||
<Layout>
|
||||
<ForgotPasswordForm />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function PasswordReset() {
|
||||
return (
|
||||
<Layout>
|
||||
<ResetPasswordForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
import { LoginForm } from "@wasp/auth/forms/Login";
|
||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||
import { VerifyEmailForm } from "@wasp/auth/forms/VerifyEmail";
|
||||
import { ForgotPasswordForm } from "@wasp/auth/forms/ForgotPassword";
|
||||
import { ResetPasswordForm } from "@wasp/auth/forms/ResetPassword";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Forgot your password? <Link to="/request-password-reset">reset it</Link>
|
||||
.
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function Signup() {
|
||||
return (
|
||||
<Layout>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailVerification() {
|
||||
return (
|
||||
<Layout>
|
||||
<VerifyEmailForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function RequestPasswordReset() {
|
||||
return (
|
||||
<Layout>
|
||||
<ForgotPasswordForm />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function PasswordReset() {
|
||||
return (
|
||||
<Layout>
|
||||
<ResetPasswordForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui).
|
||||
|
||||
### 5. Set up an Email Sender
|
||||
|
||||
To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.
|
||||
|
||||
We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers.
|
||||
|
||||
To set up SendGrid to send emails, we will add the following to our `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
// ...
|
||||
// 7. Set up the email sender
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
// ...
|
||||
// 7. Set up the email sender
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
... and add the following to our `.env.server` file:
|
||||
|
||||
```c title=".env.server"
|
||||
SENDGRID_API_KEY=<your key>
|
||||
```
|
||||
|
||||
If you are not sure how to get a SendGrid API key, read more [here](/docs/advanced/email#getting-the-api-key).
|
||||
|
||||
Read more about setting up email senders in the [sending emails docs](/docs/advanced/email).
|
||||
|
||||
### Conclusion
|
||||
|
||||
That's it! We have set up email authentication in our app. 🎉
|
||||
|
||||
Running `wasp db migrate-dev` and then `wasp start` should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the [using auth docs](/docs/auth/overview).
|
||||
|
||||
## Login and Signup Flows
|
||||
|
||||
### Login
|
||||
|
||||
![Auth UI](/img/authui/login.png)
|
||||
|
||||
If logging in with an unverified email is _allowed_, the user will be able to login with an unverified email address. If logging in with an unverified email is _not allowed_, the user will be shown an error message.
|
||||
|
||||
Read more about the `allowUnverifiedLogin` option [here](#allowunverifiedlogin-bool-specifies-whether-the-user-can-login-without-verifying-their-e-mail-address).
|
||||
|
||||
### Signup
|
||||
|
||||
![Auth UI](/img/authui/signup.png)
|
||||
|
||||
Some of the behavior you get out of the box:
|
||||
1. Rate limiting
|
||||
|
||||
We are limiting the rate of sign-up requests to **1 request per minute** per email address. This is done to prevent spamming.
|
||||
|
||||
2. Preventing user email leaks
|
||||
|
||||
If somebody tries to signup with an email that already exists and it's verified, we _pretend_ that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.
|
||||
|
||||
3. Allowing registration for unverified emails
|
||||
|
||||
If a user tries to register with an existing but **unverified** email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.
|
||||
|
||||
4. Password validation
|
||||
|
||||
Read more about the default password validation rules and how to override them in [using auth docs](/docs/auth/overview).
|
||||
|
||||
## Email Verification Flow
|
||||
|
||||
By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.
|
||||
|
||||
Our setup looks like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `EmailVerificationRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
The content of the e-mail can be customized, read more about it [here](#emailverification-emailverificationconfig-).
|
||||
|
||||
### Email Verification Page
|
||||
|
||||
We defined our email verification page in the `auth.{jsx,tsx}` file.
|
||||
|
||||
![Auth UI](/img/authui/email_verification.png)
|
||||
|
||||
## Password Reset Flow
|
||||
|
||||
Users can request a password and then they'll receive an e-mail with a link to reset their password.
|
||||
|
||||
Some of the behavior you get out of the box:
|
||||
1. Rate limiting
|
||||
|
||||
We are limiting the rate of sign-up requests to **1 request per minute** per email address. This is done to prevent spamming.
|
||||
|
||||
2. Preventing user email leaks
|
||||
|
||||
If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.
|
||||
|
||||
Our setup in `main.wasp` looks like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Request Password Reset Page
|
||||
|
||||
Users request their password to be reset by going to the `/request-password-reset` route. We defined our request password reset page in the `auth.{jsx,tsx}` file.
|
||||
|
||||
![Request password reset page](/img/authui/forgot_password_after.png)
|
||||
|
||||
### Password Reset Page
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `PasswordResetRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
![Request password reset page](/img/authui/reset_password_after.png)
|
||||
|
||||
Users can enter their new password there.
|
||||
|
||||
The content of the e-mail can be customized, read more about it [here](#passwordreset-passwordresetconfig-).
|
||||
|
||||
## Using The Auth
|
||||
|
||||
To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [using auth docs](/docs/auth/overview).
|
||||
|
||||
## API Reference
|
||||
|
||||
Let's go over the options we can specify when using email authentication.
|
||||
|
||||
### `userEntity` fields
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {18-25}
|
||||
app myApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
email: {
|
||||
// We'll explain these options below
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
|
||||
// Using email auth requires the `userEntity` to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {18-25}
|
||||
app myApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
email: {
|
||||
// We'll explain these options below
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
|
||||
// Using email auth requires the `userEntity` to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Email auth requires that `userEntity` specified in `auth` contains:
|
||||
|
||||
- optional `email` field of type `String`
|
||||
- optional `password` field of type `String`
|
||||
- `isEmailVerified` field of type `Boolean` with a default value of `false`
|
||||
- optional `emailVerificationSentAt` field of type `DateTime`
|
||||
- optional `passwordResetSentAt` field of type `DateTime`
|
||||
|
||||
### Fields in the `email` dict
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
email: {
|
||||
fromField: {
|
||||
name: "My App",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
email: {
|
||||
fromField: {
|
||||
name: "My App",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### `fromField: EmailFromField` <Required />
|
||||
`fromField` is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.
|
||||
|
||||
It has the following fields:
|
||||
- `name`: name of the sender
|
||||
- `email`: e-mail address of the sender <Required />
|
||||
|
||||
#### `emailVerification: EmailVerificationConfig` <Required />
|
||||
`emailVerification` is a dict that specifies the details of the e-mail verification process.
|
||||
|
||||
It has the following fields:
|
||||
- `clientRoute: Route`: a route that is used for the user to verify their e-mail address. <Required />
|
||||
|
||||
Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our `verifyEmail` action for that.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/pages/EmailVerificationPage.jsx"
|
||||
import { verifyEmail } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await verifyEmail({ token });
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/pages/EmailVerificationPage.tsx"
|
||||
import { verifyEmail } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await verifyEmail({ token });
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
We used Auth UI above to avoid doing this work of sending the token to the server manually.
|
||||
:::
|
||||
|
||||
- `getEmailContentFn: ServerImport`: a function that returns the content of the e-mail that is sent to the user.
|
||||
|
||||
Defining `getEmailContentFn` can be done by defining a file in the `server` directory.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title="server/email.js"
|
||||
export const getVerificationEmailContent = ({ verificationLink }) => ({
|
||||
subject: 'Verify your email',
|
||||
text: `Click the link below to verify your email: ${verificationLink}`,
|
||||
html: `
|
||||
<p>Click the link below to verify your email</p>
|
||||
<a href="${verificationLink}">Verify email</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="server/email.ts"
|
||||
import { GetVerificationEmailContentFn } from '@wasp/types'
|
||||
|
||||
export const getVerificationEmailContent: GetVerificationEmailContentFn = ({
|
||||
verificationLink,
|
||||
}) => ({
|
||||
subject: 'Verify your email',
|
||||
text: `Click the link below to verify your email: ${verificationLink}`,
|
||||
html: `
|
||||
<p>Click the link below to verify your email</p>
|
||||
<a href="${verificationLink}">Verify email</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>This is the default content of the e-mail, you can customize it to your liking.</small>
|
||||
|
||||
|
||||
#### `passwordReset: PasswordResetConfig` <Required />
|
||||
`passwordReset` is a dict that specifies the password reset process.
|
||||
|
||||
It has the following fields:
|
||||
- `clientRoute: Route`: a route that is used for the user to reset their password. <Required />
|
||||
|
||||
Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our `requestPasswordReset` and `resetPassword` actions to do that.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/pages/ForgotPasswordPage.jsx"
|
||||
import { requestPasswordReset } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await requestPasswordReset({ email });
|
||||
```
|
||||
|
||||
```js title="src/pages/PasswordResetPage.jsx"
|
||||
import { resetPassword } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await resetPassword({ password, token })
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/pages/ForgotPasswordPage.tsx"
|
||||
import { requestPasswordReset } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await requestPasswordReset({ email });
|
||||
```
|
||||
|
||||
```ts title="src/pages/PasswordResetPage.tsx"
|
||||
import { resetPassword } from '@wasp/auth/email/actions';
|
||||
...
|
||||
await resetPassword({ password, token })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.
|
||||
:::
|
||||
|
||||
- `getEmailContentFn: ServerImport`: a function that returns the content of the e-mail that is sent to the user.
|
||||
|
||||
Defining `getEmailContentFn` is done by defining a function that looks like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title="server/email.js"
|
||||
export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
|
||||
subject: 'Password reset',
|
||||
text: `Click the link below to reset your password: ${passwordResetLink}`,
|
||||
html: `
|
||||
<p>Click the link below to reset your password</p>
|
||||
<a href="${passwordResetLink}">Reset password</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="server/email.ts"
|
||||
import { GetPasswordResetEmailContentFn } from '@wasp/types'
|
||||
|
||||
export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({
|
||||
passwordResetLink,
|
||||
}) => ({
|
||||
subject: 'Password reset',
|
||||
text: `Click the link below to reset your password: ${passwordResetLink}`,
|
||||
html: `
|
||||
<p>Click the link below to reset your password</p>
|
||||
<a href="${passwordResetLink}">Reset password</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>This is the default content of the e-mail, you can customize it to your liking.</small>
|
||||
|
||||
#### `allowUnverifiedLogin: bool`: specifies whether the user can login without verifying their e-mail address
|
||||
|
||||
It defaults to `false`. If `allowUnverifiedLogin` is set to `true`, the user can login without verifying their e-mail address, otherwise users will receive a `401` error when trying to login without verifying their e-mail address.
|
||||
|
||||
Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the `isEmailVerified` field on the user entity to check if the user has verified their e-mail address.
|
||||
|
||||
If you have any questions, feel free to ask them on [our Discord server](https://discord.gg/rzdnErX).
|
639
web/docs/auth/overview.md
Normal file
639
web/docs/auth/overview.md
Normal file
@ -0,0 +1,639 @@
|
||||
---
|
||||
title: Using Auth
|
||||
---
|
||||
|
||||
import { AuthMethodsGrid } from "@site/src/components/AuthMethodsGrid";
|
||||
import { Required } from "@site/src/components/Required";
|
||||
|
||||
Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box 🙃.
|
||||
|
||||
Enabling auth for your app is optional and can be done by configuring the `auth` field of the `app` declaration.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
usernameAndPassword: {}, // use this or email, not both
|
||||
email: {}, // use this or usernameAndPassword, not both
|
||||
google: {},
|
||||
gitHub: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
}
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
usernameAndPassword: {}, // use this or email, not both
|
||||
email: {}, // use this or usernameAndPassword, not both
|
||||
google: {},
|
||||
gitHub: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
}
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
Read more about the `auth` field options in the [API Reference](#api-reference) section.
|
||||
|
||||
</small>
|
||||
|
||||
We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.
|
||||
|
||||
|
||||
## Available auth methods
|
||||
|
||||
Wasp supports the following auth methods:
|
||||
|
||||
<AuthMethodsGrid />
|
||||
|
||||
Let's say we enabled the [Username & password](/docs/auth/username-and-pass) authentication.
|
||||
|
||||
We get an auth backend with signup and login endpoints. We also get the `user` object in our [Operations](/docs/data-model/operations/overview) and we can decide what to do based on whether the user is logged in or not.
|
||||
|
||||
We would also get the [Auth UI](/docs/auth/ui) generated for us. We can set up our login and signup pages where our users can **create their account** and **login**. We can then protect certain pages by setting `authRequired: true` for them. This will make sure that only logged-in users can access them.
|
||||
|
||||
We will also have access to the `user` object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a **logout button** or a login button if the user is not logged in.
|
||||
|
||||
## Protecting a page with `authRequired`
|
||||
|
||||
When declaring a page, you can set the `authRequired` property.
|
||||
|
||||
If you set it to `true`, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the `app.auth.onAuthFailedRedirectTo` field.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
page MainPage {
|
||||
component: import Main from "@client/pages/Main.jsx",
|
||||
authRequired: true
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
page MainPage {
|
||||
component: import Main from "@client/pages/Main.tsx",
|
||||
authRequired: true
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::caution Requires auth method
|
||||
You can only use `authRequired` if your app uses one of the [available auth methods](#available-auth-methods).
|
||||
:::
|
||||
|
||||
If `authRequired` is set to `true`, the page's React component (specified by the `component` property) receives the `user` object as a prop. Read more about the `user` object in the [Accessing the logged-in user section](#accessing-the-logged-in-user).
|
||||
|
||||
## Logout action
|
||||
|
||||
We provide an action for logging out the user. Here's how you can use it:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="client/components/LogoutButton.jsx"
|
||||
import logout from '@wasp/auth/logout'
|
||||
|
||||
const LogoutButton = () => {
|
||||
return (
|
||||
<button onClick={logout}>Logout</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/components/LogoutButton.tsx"
|
||||
import logout from '@wasp/auth/logout'
|
||||
|
||||
const LogoutButton = () => {
|
||||
return (
|
||||
<button onClick={logout}>Logout</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Accessing the logged-in user
|
||||
|
||||
You can get access to the `user` object both in the backend and on the frontend.
|
||||
|
||||
### On the client
|
||||
|
||||
There are two ways to access the `user` object on the client:
|
||||
- the `user` prop
|
||||
- the `useAuth` hook
|
||||
|
||||
#### Using the `user` prop
|
||||
|
||||
If the page's declaration sets `authRequired` to `true`, the page's React component receives the `user` object as a prop:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
page AccountPage {
|
||||
component: import Account from "@client/pages/Account.jsx",
|
||||
authRequired: true
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="client/pages/Account.jsx"
|
||||
import Button from './Button';
|
||||
import logout from '@wasp/auth/logout';
|
||||
|
||||
const AccountPage = ({ user }) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={logout}>Logout</Button>
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountPage;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
page AccountPage {
|
||||
component: import Account from "@client/pages/Account.tsx",
|
||||
authRequired: true
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/pages/Account.tsx"
|
||||
import type { User } from '@wasp/entities';
|
||||
import Button from './Button';
|
||||
import logout from '@wasp/auth/logout';
|
||||
|
||||
const AccountPage = ({ user }: { user: User }) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={logout}>Logout</Button>
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountPage;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Using the `useAuth` hook
|
||||
|
||||
Wasp provides a React hook you can use in the client components - `useAuth`.
|
||||
|
||||
This hook is a thin wrapper over Wasp's `useQuery` hook and returns data in the same format.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/pages/MainPage.jsx"
|
||||
import useAuth from '@wasp/auth/useAuth'
|
||||
import { Link } from 'react-router-dom'
|
||||
import logout from '@wasp/auth/logout'
|
||||
import Todo from '../Todo'
|
||||
|
||||
export function Main() {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<span>
|
||||
Please <Link to='/login'>login</Link> or <Link to='/signup'>sign up</Link>.
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<button onClick={logout}>Logout</button>
|
||||
<Todo />
|
||||
< />
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/pages/MainPage.tsx"
|
||||
import useAuth from '@wasp/auth/useAuth'
|
||||
import { Link } from 'react-router-dom'
|
||||
import logout from '@wasp/auth/logout'
|
||||
import Todo from '../Todo'
|
||||
|
||||
export function Main() {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<span>
|
||||
Please <Link to='/login'>login</Link> or <Link to='/signup'>sign up</Link>.
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<button onClick={logout}>Logout</button>
|
||||
<Todo />
|
||||
< />
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip
|
||||
Since the `user` prop is only available in a page's React component: use the `user` prop in the page's React component and the `useAuth` hook in any other React component.
|
||||
:::
|
||||
|
||||
### On the server
|
||||
|
||||
#### Using the `context.user` object
|
||||
|
||||
When authentication is enabled, all [queries and actions](/docs/data-model/operations/overview) have access to the `user` object through the `context` argument. `context.user` contains all User entity's fields, except for the password.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (task, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(403)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
return Task.create({
|
||||
data: {
|
||||
description: task.description,
|
||||
user: {
|
||||
connect: { id: context.user.id }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import type { Task } from "@wasp/entities"
|
||||
import type { CreateTask } from "@wasp/actions/types"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
type CreateTaskPayload = Pick<Task, "description">
|
||||
|
||||
export const createTask: CreateTask<CreateTaskPayload, Task> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(403)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
return Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
user: {
|
||||
connect: { id: context.user.id }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
To implement access control in your app, each operation must check `context.user` and decide what to do. For example, if `context.user` is `undefined` inside a private operation, the user's access should be denied.
|
||||
|
||||
When using WebSockets, the `user` object is also available on the `socket.data` object. Read more in the [WebSockets section](/docs/advanced/web-sockets#websocketfn-function).
|
||||
|
||||
## User entity
|
||||
|
||||
### Password hashing
|
||||
|
||||
You don't need to worry about hashing the password yourself. Even when directly using the Prisma client and calling `create()` with a plain-text password, Wasp's middleware makes sure to hash the password before storing it in the database.
|
||||
For example, if you need to update a user's password, you can safely use the Prisma client to do so, e.g., inside an Action:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
export const updatePassword = async (args, context) => {
|
||||
return context.entities.User.update({
|
||||
where: { id: args.userId },
|
||||
data: {
|
||||
password: 'New pwd which will be hashed automatically!'
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import type { UpdatePassword } from "@wasp/actions/types"
|
||||
import type { User } from "@wasp/entities"
|
||||
|
||||
type UpdatePasswordPayload = {
|
||||
userId: User["id"]
|
||||
}
|
||||
|
||||
export const updatePassword: UpdatePassword<UpdatePasswordPayload, User> = async (args, context) => {
|
||||
return context.entities.User.update({
|
||||
where: { id: args.userId },
|
||||
data: {
|
||||
password: 'New pwd which will be hashed automatically!'
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Default validations
|
||||
|
||||
Wasp includes several basic validation mechanisms. If you need something extra, the [next section](#customizing-validations) shows how to customize them.
|
||||
|
||||
Default validations depend on the auth method you use.
|
||||
|
||||
#### Username & password
|
||||
|
||||
If you use [Username & password](/docs/auth/username-and-pass) authentication, the default validations are:
|
||||
- The `username` must not be empty
|
||||
- The `password` must not be empty, have at least 8 characters, and contain a number
|
||||
|
||||
Note that `username`s are stored in a **case-sensitive** manner.
|
||||
|
||||
#### Email
|
||||
|
||||
If you use [Email](/docs/auth/email) authentication, the default validations are:
|
||||
- The `email` must not be empty and a valid email address
|
||||
- The `password` must not be empty, have at least 8 characters, and contain a number
|
||||
|
||||
Note that `email`s are stored in a **case-insensitive** manner.
|
||||
|
||||
### Customizing validations
|
||||
|
||||
:::note
|
||||
You can only disable the default validation for **Username & password** authentication, but you can add custom validations can to both **Username & password** and **Email** auth methods.
|
||||
|
||||
This is a bug in Wasp that is being tracked [here](https://github.com/wasp-lang/wasp/issues/1358)
|
||||
:::
|
||||
|
||||
To disable/enable default validations, or add your own, modify your custom signup function:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
},
|
||||
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
|
||||
_waspCustomValidations: [
|
||||
{
|
||||
validates: 'password',
|
||||
message: 'password must contain an uppercase letter',
|
||||
validator: password => /[A-Z]/.test(password)
|
||||
},
|
||||
]
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
},
|
||||
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
|
||||
_waspCustomValidations: [
|
||||
{
|
||||
validates: 'password',
|
||||
message: 'password must contain an uppercase letter',
|
||||
validator: password => /[A-Z]/.test(password)
|
||||
},
|
||||
]
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
Validations always run on `create()`.
|
||||
For `update()`, they only run when the field mentioned in `validates` is present.
|
||||
|
||||
The validation process stops on the first `validator` to return false. If enabled, default validations run first and then custom validations.
|
||||
:::
|
||||
|
||||
### Validation Error Handling
|
||||
When creating, updating, or deleting entities, you may wish to handle validation errors. Wasp exposes a class called `AuthError` for this purpose.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
try {
|
||||
await context.entities.User.update(...)
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
throw new HttpError(422, 'Validation failed', { message: e.message })
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
try {
|
||||
await context.entities.User.update(...)
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
throw new HttpError(422, 'Validation failed', { message: e.message })
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## API Reference
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
usernameAndPassword: {}, // use this or email, not both
|
||||
email: {}, // use this or usernameAndPassword, not both
|
||||
google: {},
|
||||
gitHub: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
}
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
usernameAndPassword: {}, // use this or email, not both
|
||||
email: {}, // use this or usernameAndPassword, not both
|
||||
google: {},
|
||||
gitHub: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/someRoute"
|
||||
}
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`app.auth` is a dictionary with the following fields:
|
||||
|
||||
#### `userEntity: entity` <Required />
|
||||
The entity representing the user. Its mandatory fields depend on your chosen auth method.
|
||||
|
||||
#### `externalAuthEntity: entity`
|
||||
Wasp requires you to set the field `auth.externalAuthEntity` for all authentication methods relying on an external authorizatino provider (e.g., Google). You also need to tweak the Entity referenced by `auth.userEntity`, as shown below.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {4,14} title="main.wasp"
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
//...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {4,14} title="main.wasp"
|
||||
//...
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
//...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity).
|
||||
:::
|
||||
|
||||
See [Google docs](/docs/auth/social-auth/google) and [GitHub docs](/docs/auth/social-auth/github) for more details.
|
||||
|
||||
#### `methods: dict` <Required />
|
||||
A dictionary of auth methods enabled for the app.
|
||||
|
||||
<AuthMethodsGrid />
|
||||
|
||||
#### `onAuthFailedRedirectTo: String` <Required />
|
||||
The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has `authRequired: true`).
|
||||
Check out these [essentials docs on auth](/docs/tutorial/auth#adding-auth-to-the-project) to see an example of usage.
|
||||
|
||||
#### `onAuthSucceededRedirectTo: String`
|
||||
The route to which Wasp will send a successfully authenticated after a successful login/signup.
|
||||
The default value is `"/"`.
|
||||
|
||||
:::note
|
||||
Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](/docs/auth/ui).
|
||||
:::
|
29
web/docs/auth/social-auth/SocialAuthGrid.css
Normal file
29
web/docs/auth/social-auth/SocialAuthGrid.css
Normal file
@ -0,0 +1,29 @@
|
||||
.social-auth-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.auth-method-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-pagination-nav-border-radius);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
.auth-method-box:hover {
|
||||
border-color: var(--ifm-pagination-nav-color-hover);
|
||||
}
|
||||
.auth-method-box h3 {
|
||||
margin: 0;
|
||||
color: var(--ifm-link-color);
|
||||
}
|
||||
.auth-method-box p {
|
||||
margin: 0;
|
||||
color: var(--ifm-color-secondary-contrast-foreground);
|
||||
}
|
||||
.social-auth-info {
|
||||
color: var(--ifm-color-secondary-contrast-foreground);
|
||||
}
|
53
web/docs/auth/social-auth/SocialAuthGrid.tsx
Normal file
53
web/docs/auth/social-auth/SocialAuthGrid.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import Link from '@docusaurus/Link';
|
||||
import "./SocialAuthGrid.css";
|
||||
|
||||
export function SocialAuthGrid({
|
||||
pagePart = "", // e.g. #overrides
|
||||
}) {
|
||||
const authMethods = [
|
||||
{
|
||||
title: "Google",
|
||||
description: "Users sign in with their Google account.",
|
||||
linkToDocs: "/docs/auth/social-auth/google" + pagePart,
|
||||
},
|
||||
{
|
||||
title: "Github",
|
||||
description: "Users sign in with their Github account.",
|
||||
linkToDocs: "/docs/auth/social-auth/github" + pagePart,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="social-auth-grid">
|
||||
{authMethods.map((authMethod) => (
|
||||
<AuthMethodBox
|
||||
title={authMethod.title}
|
||||
description={authMethod.description}
|
||||
linkToDocs={authMethod.linkToDocs}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="social-auth-info">
|
||||
<small>Click on each provider for more details.</small>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthMethodBox({
|
||||
linkToDocs,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
linkToDocs: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<Link to={linkToDocs} className="auth-method-box">
|
||||
<h3>{title} »</h3>
|
||||
<p>{description}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
10
web/docs/auth/social-auth/_api-reference-intro.md
Normal file
10
web/docs/auth/social-auth/_api-reference-intro.md
Normal file
@ -0,0 +1,10 @@
|
||||
Provider-specific behavior comes down to implementing two functions.
|
||||
|
||||
- `configFn`
|
||||
- `getUserFieldsFn`
|
||||
|
||||
The reference shows how to define both.
|
||||
|
||||
For behavior common to all providers, check the general [API Reference](/docs/auth/overview.md#api-reference).
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
10
web/docs/auth/social-auth/_default-behaviour.md
Normal file
10
web/docs/auth/social-auth/_default-behaviour.md
Normal file
@ -0,0 +1,10 @@
|
||||
When a user **signs in for the first time**, Wasp creates a new user account and links it to the chosen auth provider account for future logins.
|
||||
|
||||
Also, if the `userEntity` has:
|
||||
|
||||
- A `username` field: Wasp sets it to a random username (e.g. `nice-blue-horse-14357`).
|
||||
- A `password` field: Wasp sets it to a random string.
|
||||
|
||||
This is a historical coupling between `auth` methods we plan to remove in the future.
|
||||
|
||||
<!-- This snippet is used in overview.md, google.md and github.md -->
|
3
web/docs/auth/social-auth/_getuserfields-type.md
Normal file
3
web/docs/auth/social-auth/_getuserfields-type.md
Normal file
@ -0,0 +1,3 @@
|
||||
Wasp automatically generates the type `GetUserFieldsFn` to help you correctly type your `getUserFields` function.
|
||||
|
||||
<!-- This snippet is used in overview.md, google.md and github.md -->
|
10
web/docs/auth/social-auth/_override-example-intro.md
Normal file
10
web/docs/auth/social-auth/_override-example-intro.md
Normal file
@ -0,0 +1,10 @@
|
||||
When a user logs in using a social login provider, the backend receives some data about the user.
|
||||
Wasp lets you access this data inside the `getUserFieldsFn` function.
|
||||
|
||||
For example, the User entity can include a `displayName` field which you can set based on the details received from the provider.
|
||||
|
||||
Wasp also lets you customize the configuration of the providers' settings using the `configFn` function.
|
||||
|
||||
Let's use this example to show both functions in action:
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
10
web/docs/auth/social-auth/_override-intro.md
Normal file
10
web/docs/auth/social-auth/_override-intro.md
Normal file
@ -0,0 +1,10 @@
|
||||
Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.
|
||||
|
||||
There are two mechanisms (functions) used for overriding the default behavior:
|
||||
|
||||
- `getUserFieldsFn`
|
||||
- `configFn`
|
||||
|
||||
Let's explore them in more detail.
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
@ -0,0 +1,4 @@
|
||||
Wasp exposes two functions that can help you generate usernames. Import them from `@wasp/core/auth.js`:
|
||||
|
||||
- `generateAvailableUsername` takes an array of strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Github user Jim Smith.
|
||||
- `generateAvailableDictionaryUsername` generates a random dictionary phrase that is not yet in the database. For example, `nice-blue-horse-27160`.
|
3
web/docs/auth/social-auth/_using-auth-note.md
Normal file
3
web/docs/auth/social-auth/_using-auth-note.md
Normal file
@ -0,0 +1,3 @@
|
||||
To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on [using auth](/docs/auth/overview).
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
16
web/docs/auth/social-auth/_wasp-file-structure-note.md
Normal file
16
web/docs/auth/social-auth/_wasp-file-structure-note.md
Normal file
@ -0,0 +1,16 @@
|
||||
Here's a skeleton of how our `main.wasp` should look like after we're done:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// Configuring the social authentication
|
||||
app myApp {
|
||||
auth: { ... }
|
||||
}
|
||||
|
||||
// Defining entities
|
||||
entity User { ... }
|
||||
entity SocialLogin { ... }
|
||||
|
||||
// Defining routes and pages
|
||||
route LoginRoute { ... }
|
||||
page LoginPage { ... }
|
||||
```
|
606
web/docs/auth/social-auth/github.md
Normal file
606
web/docs/auth/social-auth/github.md
Normal file
@ -0,0 +1,606 @@
|
||||
---
|
||||
title: GitHub
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import DefaultBehaviour from './\_default-behaviour.md';
|
||||
import OverrideIntro from './\_override-intro.md';
|
||||
import OverrideExampleIntro from './\_override-example-intro.md';
|
||||
import UsingAuthNote from './\_using-auth-note.md';
|
||||
import WaspFileStructureNote from './\_wasp-file-structure-note.md';
|
||||
import UsernameGenerateExplanation from './\_username-generate-explanation.md';
|
||||
import GetUserFieldsType from './\_getuserfields-type.md';
|
||||
import ApiReferenceIntro from './\_api-reference-intro.md';
|
||||
|
||||
Wasp supports Github Authentication out of the box.
|
||||
GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.
|
||||
|
||||
Letting your users log in using their GitHub accounts turns the signup process into a breeze.
|
||||
|
||||
Let's walk through enabling Github Authentication, explain some of the default settings, and show how to override them.
|
||||
|
||||
## Setting up Github Auth
|
||||
|
||||
Enabling GitHub Authentication comes down to a series of steps:
|
||||
|
||||
1. Enabling GitHub authentication in the Wasp file.
|
||||
1. Adding the necessary Entities.
|
||||
1. Creating a GitHub OAuth app.
|
||||
1. Adding the neccessary Routes and Pages
|
||||
1. Using Auth UI components in our Pages.
|
||||
|
||||
<WaspFileStructureNote />
|
||||
|
||||
### 1. Adding Github Auth to Your Wasp File
|
||||
|
||||
Let's start by properly configuring the Auth object:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
// 1. Specify the User entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
// 2. Specify the SocialLogin entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
// highlight-next-line
|
||||
// 3. Enable Github Auth
|
||||
// highlight-next-line
|
||||
gitHub: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
// 1. Specify the User entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
// 2. Specify the SocialLogin entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
// highlight-next-line
|
||||
// 3. Enable Github Auth
|
||||
// highlight-next-line
|
||||
gitHub: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 2. Add the Entities
|
||||
|
||||
Let's now define the entities acting as `app.auth.userEntity` and `app.auth.externalAuthEntity`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// highlight-next-line
|
||||
// 4. Define the User entity
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
// 5. Define the SocialLogin entity
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// highlight-next-line
|
||||
// 4. Define the User entity
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
// 5. Define the SocialLogin entity
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity).
|
||||
|
||||
### 3. Creating a GitHub OAuth App
|
||||
|
||||
To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it:
|
||||
|
||||
1. Log into your GitHub account and navigate to: https://github.com/settings/developers.
|
||||
2. Select **New OAuth App**.
|
||||
3. Supply required information.
|
||||
|
||||
<img alt="GitHub Applications Screenshot"
|
||||
src={useBaseUrl('img/integrations-github-1.png')}
|
||||
width="400px"
|
||||
/>
|
||||
|
||||
- For **Authorization callback URL**:
|
||||
- For development, put: `http://localhost:3000/auth/login/github`.
|
||||
- Once you know on which URL your app will be deployed, you can create a new app with that URL instead e.g. `https://someotherhost.com/auth/login/github`.
|
||||
|
||||
4. Hit **Register application**.
|
||||
5. Hit **Generate a new client secret** on the next page.
|
||||
6. Copy your Client ID and Client secret as you'll need them in the next step.
|
||||
|
||||
### 4. Adding Environment Variables
|
||||
|
||||
Add these environment variables to the `.env.server` file at the root of your project (take their values from the previous step):
|
||||
|
||||
```bash title=".env.server"
|
||||
GITHUB_CLIENT_ID=your-github-client-id
|
||||
GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
```
|
||||
|
||||
### 5. Adding the Necessary Routes and Pages
|
||||
|
||||
Let's define the necessary authentication Routes and Pages.
|
||||
|
||||
Add the following code to your `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We'll define the React components for these pages in the `client/pages/auth.{jsx,tsx}` file below.
|
||||
|
||||
### 6. Creating the Client Pages
|
||||
|
||||
:::info
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/pages/auth.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui).
|
||||
|
||||
### Conclusion
|
||||
|
||||
Yay, we've successfully set up Github Auth! 🎉
|
||||
|
||||
![Github Auth](/img/auth/github.png)
|
||||
|
||||
Running `wasp db migrate-dev` and `wasp start` should now give you a working app with authentication.
|
||||
To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](/docs/auth/overview).
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
Add `gitHub: {}` to the `auth.methods` dictionary to use it with default settings.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp {10}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp {10}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<DefaultBehaviour />
|
||||
|
||||
## Overrides
|
||||
|
||||
<OverrideIntro />
|
||||
|
||||
### Using the User's Provider Account Details
|
||||
|
||||
<OverrideExampleIntro />
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12,22}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {
|
||||
configFn: import { getConfig } from "@server/auth/github.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
displayName String
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
```js title=src/server/auth/github.js
|
||||
import { generateAvailableDictionaryUsername } from "@wasp/core/auth.js";
|
||||
|
||||
export const getUserFields = async (_context, args) => {
|
||||
const username = await generateAvailableDictionaryUsername();
|
||||
const displayName = args.profile.displayName;
|
||||
return { username, displayName };
|
||||
};
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID // look up from env or elsewhere
|
||||
clientSecret // look up from env or elsewhere
|
||||
scope: [],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12,22}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {
|
||||
configFn: import { getConfig } from "@server/auth/github.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
displayName String
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
```ts title=src/server/auth/github.ts
|
||||
import type { GetUserFieldsFn } from '@wasp/types'
|
||||
import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields: GetUserFieldsFn = async (_context, args) => {
|
||||
const username = await generateAvailableDictionaryUsername()
|
||||
const displayName = args.profile.displayName
|
||||
return { username, displayName }
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: [],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<GetUserFieldsType />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Auth
|
||||
|
||||
<UsingAuthNote />
|
||||
|
||||
## API Reference
|
||||
|
||||
<ApiReferenceIntro />
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {
|
||||
configFn: import { getConfig } from "@server/auth/github.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {
|
||||
configFn: import { getConfig } from "@server/auth/github.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `gitHub` dict has the following properties:
|
||||
|
||||
- #### `configFn: ServerImport`
|
||||
|
||||
This function should return an object with the Client ID, Client Secret, and scope for the OAuth provider.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/auth/github.js
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: [],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/auth/github.ts
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: [],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- #### `getUserFieldsFn: ServerImport`
|
||||
|
||||
This function should return the user fields to use when creating a new user.
|
||||
|
||||
The `context` contains the `User` entity, and the `args` object contains GitHub profile information.
|
||||
You can do whatever you want with this information (e.g., generate a username).
|
||||
|
||||
Here is how you could generate a username based on the Github display name:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/auth/github.js
|
||||
import { generateAvailableUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields = async (_context, args) => {
|
||||
const username = await generateAvailableUsername(
|
||||
args.profile.displayName.split(' '),
|
||||
{ separator: '.' }
|
||||
)
|
||||
return { username }
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/auth/github.ts
|
||||
import type { GetUserFieldsFn } from '@wasp/types'
|
||||
import { generateAvailableUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields: GetUserFieldsFn = async (_context, args) => {
|
||||
const username = await generateAvailableUsername(
|
||||
args.profile.displayName.split(' '),
|
||||
{ separator: '.' }
|
||||
)
|
||||
return { username }
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<UsernameGenerateExplanation />
|
651
web/docs/auth/social-auth/google.md
Normal file
651
web/docs/auth/social-auth/google.md
Normal file
@ -0,0 +1,651 @@
|
||||
---
|
||||
title: Google
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import DefaultBehaviour from './\_default-behaviour.md';
|
||||
import OverrideIntro from './\_override-intro.md';
|
||||
import OverrideExampleIntro from './\_override-example-intro.md';
|
||||
import UsingAuthNote from './\_using-auth-note.md';
|
||||
import WaspFileStructureNote from './\_wasp-file-structure-note.md';
|
||||
import UsernameGenerateExplanation from './\_username-generate-explanation.md';
|
||||
import GetUserFieldsType from './\_getuserfields-type.md';
|
||||
import ApiReferenceIntro from './\_api-reference-intro.md';
|
||||
|
||||
Wasp supports Google Authentication out of the box.
|
||||
Google Auth is arguably the best external auth option, as most users on the web already have Google accounts.
|
||||
|
||||
Enabling it lets your users log in using their existing Google accounts, greatly simplifying the process and enhancing the user experience.
|
||||
|
||||
Let's walk through enabling Google authentication, explain some of the default settings, and show how to override them.
|
||||
|
||||
## Setting up Google Auth
|
||||
|
||||
Enabling Google Authentication comes down to a series of steps:
|
||||
|
||||
1. Enabling Google authentication in the Wasp file.
|
||||
1. Adding the necessary Entities.
|
||||
1. Creating a Google OAuth app.
|
||||
1. Adding the neccessary Routes and Pages
|
||||
1. Using Auth UI components in our Pages.
|
||||
|
||||
<WaspFileStructureNote />
|
||||
|
||||
### 1. Adding Google Auth to Your Wasp File
|
||||
|
||||
Let's start by properly configuring the Auth object:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
// 1. Specify the User entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
// 2. Specify the SocialLogin entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
// highlight-next-line
|
||||
// 3. Enable Google Auth
|
||||
// highlight-next-line
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
// 1. Specify the User entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
// 2. Specify the SocialLogin entity (we'll define it next)
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
// highlight-next-line
|
||||
// 3. Enable Google Auth
|
||||
// highlight-next-line
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity).
|
||||
|
||||
### 2. Adding the Entities
|
||||
|
||||
Let's now define the entities acting as `app.auth.userEntity` and `app.auth.externalAuthEntity`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// highlight-next-line
|
||||
// 4. Define the User entity
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
// 5. Define the SocialLogin entity
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// highlight-next-line
|
||||
// 4. Define the User entity
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
// 5. Define the SocialLogin entity
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 3. Creating a Google OAuth App
|
||||
|
||||
To use Google as an authentication method, you'll first need to create a Google project and provide Wasp with your client key and secret. Here's how you do it:
|
||||
|
||||
1. Create a Google Cloud Platform account if you do not already have one: https://cloud.google.com/
|
||||
2. Create and configure a new Google project here: https://console.cloud.google.com/home/dashboard
|
||||
|
||||
![Google Console Screenshot 1](/img/integrations-google-1.jpg)
|
||||
|
||||
![Google Console Screenshot 2](/img/integrations-google-2.jpg)
|
||||
|
||||
3. Search for **OAuth** in the top bar, click on **OAuth consent screen**.
|
||||
|
||||
![Google Console Screenshot 3](/img/integrations-google-3.jpg)
|
||||
|
||||
- Select what type of app you want, we will go with **External**.
|
||||
|
||||
![Google Console Screenshot 4](/img/integrations-google-4.jpg)
|
||||
|
||||
- Fill out applicable information on Page 1.
|
||||
|
||||
![Google Console Screenshot 5](/img/integrations-google-5.jpg)
|
||||
|
||||
- On Page 2, Scopes, you should select `userinfo.profile`. You can optionally search for other things, like `email`.
|
||||
|
||||
![Google Console Screenshot 6](/img/integrations-google-6.jpg)
|
||||
|
||||
![Google Console Screenshot 7](/img/integrations-google-7.jpg)
|
||||
|
||||
![Google Console Screenshot 8](/img/integrations-google-8.jpg)
|
||||
|
||||
- Add any test users you want on Page 3.
|
||||
|
||||
![Google Console Screenshot 9](/img/integrations-google-9.jpg)
|
||||
|
||||
4. Next, click **Credentials**.
|
||||
|
||||
![Google Console Screenshot 10](/img/integrations-google-10.jpg)
|
||||
|
||||
- Select **Create Credentials**.
|
||||
- Select **OAuth client ID**.
|
||||
|
||||
![Google Console Screenshot 11](/img/integrations-google-11.jpg)
|
||||
|
||||
- Complete the form
|
||||
|
||||
![Google Console Screenshot 12](/img/integrations-google-12.jpg)
|
||||
|
||||
- Under Authorized redirect URIs, put in: `http://localhost:3000/auth/login/google`
|
||||
|
||||
![Google Console Screenshot 13](/img/integrations-google-13.jpg)
|
||||
|
||||
- Once you know on which URL(s) your API server will be deployed, also add those URL(s).
|
||||
- For example: `https://someotherhost.com/auth/login/google`
|
||||
|
||||
- When you save, you can click the Edit icon and your credentials will be shown.
|
||||
|
||||
![Google Console Screenshot 14](/img/integrations-google-14.jpg)
|
||||
|
||||
5. Copy your Client ID and Client secret as you will need them in the next step.
|
||||
|
||||
### 4. Adding Environment Variables
|
||||
|
||||
Add these environment variables to the `.env.server` file at the root of your project (take their values from the previous step):
|
||||
|
||||
```bash title=".env.server"
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
```
|
||||
|
||||
### 5. Adding the Necessary Routes and Pages
|
||||
|
||||
Let's define the necessary authentication Routes and Pages.
|
||||
|
||||
Add the following code to your `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
// 6. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We'll define the React components for these pages in the `client/pages/auth.{jsx,tsx}` file below.
|
||||
|
||||
### 6. Create the Client Pages
|
||||
|
||||
:::info
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's now create a `auth.{jsx,tsx}` file in the `client/pages`.
|
||||
It should have the following code:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/pages/auth.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info Auth UI
|
||||
Our pages use an automatically-generated Auth UI component. Read more about Auth UI components [here](/docs/auth/ui).
|
||||
:::
|
||||
|
||||
### Conclusion
|
||||
|
||||
Yay, we've successfully set up Google Auth! 🎉
|
||||
|
||||
![Google Auth](/img/auth/google.png)
|
||||
|
||||
Running `wasp db migrate-dev` and `wasp start` should now give you a working app with authentication.
|
||||
To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](/docs/auth/overview).
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
Add `google: {}` to the `auth.methods` dictionary to use it with default settings:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp {10}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp {10}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<DefaultBehaviour />
|
||||
|
||||
## Overrides
|
||||
|
||||
<OverrideIntro />
|
||||
|
||||
### Using the User's Provider Account Details
|
||||
|
||||
<OverrideExampleIntro />
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12,22}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
configFn: import { getConfig } from "@server/auth/google.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
displayName String
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
```js title=src/server/auth/google.js
|
||||
import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields = async (_context, args) => {
|
||||
const username = await generateAvailableDictionaryUsername()
|
||||
const displayName = args.profile.displayName
|
||||
return { username, displayName }
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: ['profile', 'email'],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12,22}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
configFn: import { getConfig } from "@server/auth/google.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
displayName String
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
```ts title=src/server/auth/google.ts
|
||||
import type { GetUserFieldsFn } from '@wasp/types'
|
||||
import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields: GetUserFieldsFn = async (_context, args) => {
|
||||
const username = await generateAvailableDictionaryUsername()
|
||||
const displayName = args.profile.displayName
|
||||
return { username, displayName }
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: ['profile', 'email'],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<GetUserFieldsType />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Auth
|
||||
|
||||
<UsingAuthNote />
|
||||
|
||||
## API Reference
|
||||
|
||||
<ApiReferenceIntro />
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
configFn: import { getConfig } from "@server/auth/google.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {11-12}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
configFn: import { getConfig } from "@server/auth/google.js",
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `google` dict has the following properties:
|
||||
|
||||
- #### `configFn: ServerImport`
|
||||
|
||||
This function must return an object with the Client ID, the Client Secret, and the scope for the OAuth provider.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/auth/google.js
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: ['profile', 'email'],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/auth/google.ts
|
||||
export function getConfig() {
|
||||
return {
|
||||
clientID, // look up from env or elsewhere
|
||||
clientSecret, // look up from env or elsewhere
|
||||
scope: ['profile', 'email'],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- #### `getUserFieldsFn: ServerImport`
|
||||
|
||||
This function must return the user fields to use when creating a new user.
|
||||
|
||||
The `context` contains the `User` entity, and the `args` object contains Google profile information.
|
||||
You can do whatever you want with this information (e.g., generate a username).
|
||||
|
||||
Here is how to generate a username based on the Google display name:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/auth/google.js
|
||||
import { generateAvailableUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields = async (_context, args) => {
|
||||
const username = await generateAvailableUsername(
|
||||
args.profile.displayName.split(' '),
|
||||
{ separator: '.' }
|
||||
)
|
||||
return { username }
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/auth/google.ts
|
||||
import type { GetUserFieldsFn } from '@wasp/types'
|
||||
import { generateAvailableUsername } from '@wasp/core/auth.js'
|
||||
|
||||
export const getUserFields: GetUserFieldsFn = async (_context, args) => {
|
||||
const username = await generateAvailableUsername(
|
||||
args.profile.displayName.split(' '),
|
||||
{ separator: '.' }
|
||||
)
|
||||
return { username }
|
||||
}
|
||||
```
|
||||
|
||||
<GetUserFieldsType />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<UsernameGenerateExplanation />
|
501
web/docs/auth/social-auth/overview.md
Normal file
501
web/docs/auth/social-auth/overview.md
Normal file
@ -0,0 +1,501 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
import { SocialAuthGrid } from './SocialAuthGrid';
|
||||
import DefaultBehaviour from './\_default-behaviour.md';
|
||||
import OverrideIntro from './\_override-intro.md';
|
||||
import GetUserFieldsType from './\_getuserfields-type.md';
|
||||
|
||||
Social login options (e.g., _Log in with Google_) are a great (maybe even the best) solution for handling user accounts.
|
||||
A famous old developer joke tells us _"The best auth system is the one you never have to make."_
|
||||
|
||||
Wasp wants to make adding social login options to your app as painless as possible.
|
||||
|
||||
Using different social providers gives users a chance to sign into your app via their existing accounts on other platforms (Google, GitHub, etc.).
|
||||
|
||||
This page goes through the common behaviors between all supported social login providers and shows you how to customize them.
|
||||
It also gives an overview of Wasp's UI helpers - the quickest possible way to get started with social auth.
|
||||
|
||||
## Available Providers
|
||||
|
||||
Wasp currently supports the following social login providers:
|
||||
|
||||
<SocialAuthGrid />
|
||||
|
||||
## Social Login Entity
|
||||
|
||||
Wasp requires you to declare a `userEntity` for all `auth` methods (social or otherwise).
|
||||
This field tells Wasp which Entity represents the user.
|
||||
|
||||
Additionally, when using `auth` methods that rely on external providers(e.g., _Google_), you must also declare an `externalAuthEntity`.
|
||||
This tells Wasp which Entity represents the user's link with the social provider.
|
||||
|
||||
Both fields fall under `app.auth`. Here's what the full setup looks like:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// highlight-next-line
|
||||
userEntity: User,
|
||||
// highlight-next-line
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
// highlight-next-line
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// highlight-next-line
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
To learn more about what the fields on these entities represent, look at the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
:::note
|
||||
Wasp uses the same `externalAuthEntity` for all social login providers (e.g. both GitHub and Google use the same entity).
|
||||
:::
|
||||
|
||||
## Default Behavior
|
||||
|
||||
<DefaultBehaviour />
|
||||
|
||||
## Overrides
|
||||
|
||||
Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.
|
||||
|
||||
### Allowing User to Set Their Username
|
||||
|
||||
If you want to modify the signup flow (e.g., let users choose their own usernames), you will need to go through three steps:
|
||||
|
||||
1. The first step is adding a `isSignupComplete` property to your `User` Entity. This field will signals whether the user has completed the signup process.
|
||||
2. The second step is overriding the default signup behavior.
|
||||
3. The third step is implementing the rest of your signup flow and redirecting users where appropriate.
|
||||
|
||||
Let's go through both steps in more detail.
|
||||
|
||||
#### 1. Adding the `isSignupComplete` Field to the `User` Entity
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
// highlight-next-line
|
||||
isSignupComplete Boolean @default(false)
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
// highlight-next-line
|
||||
isSignupComplete Boolean @default(false)
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### 2. Overriding the Default Behavior
|
||||
|
||||
Declare an import under `app.auth.methods.google.getUserFieldsFn` (the example assumes you're using Google):
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
// highlight-next-line
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
And implement the imported function.
|
||||
|
||||
```js title=src/server/auth/google.js
|
||||
export const getUserFields = async (_context, _args) => {
|
||||
return {
|
||||
isSignupComplete: false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {
|
||||
// highlight-next-line
|
||||
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
And implement the imported function:
|
||||
|
||||
```ts title=src/server/auth/google.ts
|
||||
import { GetUserFieldsFn } from '@wasp/types'
|
||||
|
||||
export const getUserFields: GetUserFieldsFn = async (_context, _args) => {
|
||||
return {
|
||||
isSignupComplete: false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<GetUserFieldsType />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### 3. Showing the Correct State on the Client
|
||||
|
||||
You can query the user's `isSignupComplete` flag on the client with the [`useAuth()`](/docs/auth/overview) hook.
|
||||
Depending on the flag's value, you can redirect users to the appropriate signup step.
|
||||
|
||||
For example:
|
||||
|
||||
1. When the user lands on the homepage, check the value of `user.isSignupComplete`.
|
||||
2. If it's `false`, it means the user has started the signup process but hasn't yet chosen their username. Therefore, you can redirect them to `EditUserDetailsPage` where they can edit the `username` property.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title=client/HomePage.jsx
|
||||
import useAuth from '@wasp/auth/useAuth'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
export function HomePage() {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
if (user.isSignupComplete === false) {
|
||||
return <Redirect to="/edit-user-details" />
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title=client/HomePage.tsx
|
||||
import useAuth from '@wasp/auth/useAuth'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
export function HomePage() {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
if (user.isSignupComplete === false) {
|
||||
return <Redirect to="/edit-user-details" />
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The same general principle applies to more complex signup procedures, just change the boolean `isSignupComplete` property to a property like `currentSignupStep` that can hold more values.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Using the User's Provider Account Details
|
||||
|
||||
Account details are provider-specific.
|
||||
Each provider has their own rules for defining the `getUserFieldsFn` and `configFn` functions:
|
||||
|
||||
<SocialAuthGrid pagePart="#overrides" />
|
||||
|
||||
## UI Helpers
|
||||
|
||||
:::tip Use Auth UI
|
||||
[Auth UI](/docs/auth/ui) is a common name for all high-level auth forms that come with Wasp.
|
||||
|
||||
These include fully functional auto-generated login and signup forms with working social login buttons.
|
||||
If you're looking for the fastest way to get your auth up and running, that's where you should look.
|
||||
|
||||
The UI helpers described below are lower-level and are useful for creating your custom forms.
|
||||
:::
|
||||
|
||||
Wasp provides sign-in buttons, logos and URLs for each of the supported social login providers.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title=client/LoginPage.jsx
|
||||
import {
|
||||
SignInButton as GoogleSignInButton,
|
||||
signInUrl as googleSignInUrl,
|
||||
logoUrl as googleLogoUrl,
|
||||
} from '@wasp/auth/helpers/Google'
|
||||
import {
|
||||
SignInButton as GitHubSignInButton,
|
||||
signInUrl as gitHubSignInUrl,
|
||||
logoUrl as gitHubLogoUrl,
|
||||
} from '@wasp/auth/helpers/GitHub'
|
||||
|
||||
export const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<GoogleSignInButton />
|
||||
<GitHubSignInButton />
|
||||
{/* or */}
|
||||
<a href={googleSignInUrl}>Sign in with Google</a>
|
||||
<a href={gitHubSignInUrl}>Sign in with GitHub</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title=client/LoginPage.tsx
|
||||
import {
|
||||
SignInButton as GoogleSignInButton,
|
||||
signInUrl as googleSignInUrl,
|
||||
logoUrl as googleLogoUrl,
|
||||
} from '@wasp/auth/helpers/Google'
|
||||
import {
|
||||
SignInButton as GitHubSignInButton,
|
||||
signInUrl as gitHubSignInUrl,
|
||||
logoUrl as gitHubLogoUrl,
|
||||
} from '@wasp/auth/helpers/GitHub'
|
||||
|
||||
export const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<GoogleSignInButton />
|
||||
<GitHubSignInButton />
|
||||
{/* or */}
|
||||
<a href={googleSignInUrl}>Sign in with Google</a>
|
||||
<a href={gitHubSignInUrl}>Sign in with GitHub</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
If you need even more customization, you can create your custom components using `signInUrl`s.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Fields in the `app.auth` Dictionary and Overrides
|
||||
|
||||
For more information on:
|
||||
|
||||
- Allowed fields in `app.auth`
|
||||
- `getUserFields` and `configFn` functions
|
||||
|
||||
Check the provider-specific API References:
|
||||
|
||||
<SocialAuthGrid pagePart="#api-reference" />
|
||||
|
||||
### The `externalAuthEntity` and Its Fields
|
||||
|
||||
Using social login providers requires you to define _an External Auth Entity_ and declare it with the `app.auth.externalAuthEntity` field.
|
||||
This Entity holds the data relevant to the social provider.
|
||||
All social providers share the same Entity.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp {4-10}
|
||||
// ...
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp {4-10}
|
||||
// ...
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
You don't need to know these details, you can just copy and paste the entity definition above and you are good to go.
|
||||
:::
|
||||
|
||||
The Entity acting as `app.auth.externalAuthEntity` must include the following fields:
|
||||
|
||||
- `provider` - The provider's name (e.g. `google`, `github`, etc.).
|
||||
- `providerId` - The user's ID on the provider's platform.
|
||||
- `userId` - The user's ID on your platform (this references the `id` field from the Entity acting as `app.auth.userEntity`).
|
||||
- `user` - A relation to the `userEntity` (see [the `userEntity` section](#expected-fields-on-the-userentity)) for more details.
|
||||
- `createdAt` - A timestamp of when the association was created.
|
||||
- `@@unique([provider, providerId, userId])` - A unique constraint on the combination of `provider`, `providerId` and `userId`.
|
||||
|
||||
### Expected Fields on the `userEntity`
|
||||
|
||||
Using Social login providers requires you to add one extra field to the Entity acting as `app.auth.userEntity`:
|
||||
|
||||
- `externalAuthAssociations` - A relation to the `externalAuthEntity` (see [the `externalAuthEntity` section](#the-externalauthentity-and-its-fields) for more details).
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp {6}
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp {6}
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
614
web/docs/auth/ui.md
Normal file
614
web/docs/auth/ui.md
Normal file
@ -0,0 +1,614 @@
|
||||
---
|
||||
title: Auth UI
|
||||
---
|
||||
|
||||
import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill } from "./Pills";
|
||||
|
||||
To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.
|
||||
|
||||
Below we cover all of the available UI components and how to use them.
|
||||
|
||||
![Auth UI](/img/authui/all_screens.gif)
|
||||
|
||||
## Overview
|
||||
|
||||
After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.
|
||||
|
||||
Based on the authentication providers you enabled in your `main.wasp` file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {5} title="main.wasp"
|
||||
app MyApp {
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {5} title="main.wasp"
|
||||
app MyApp {
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You'll get the following UI:
|
||||
|
||||
![Auth UI](/img/authui/login.png)
|
||||
|
||||
And then if you enable Google and Github:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {6-7}
|
||||
app MyApp {
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
google: {},
|
||||
github: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {6-7}
|
||||
app MyApp {
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
google: {},
|
||||
github: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The form will automatically update to look like this:
|
||||
|
||||
![Auth UI](/img/authui/multiple_providers.png)
|
||||
|
||||
Let's go through all of the available components and how to use them.
|
||||
|
||||
## Auth Components
|
||||
|
||||
The following components are available for you to use in your app:
|
||||
|
||||
- [Login form](#login-form)
|
||||
- [Signup form](#signup-form)
|
||||
- [Forgot password form](#forgot-password-form)
|
||||
- [Reset password form](#reset-password-form)
|
||||
- [Verify email form](#verify-email-form)
|
||||
|
||||
### Login Form
|
||||
|
||||
Used with <UsernameAndPasswordPill />, <EmailPill />, <GithubPill /> and <GooglePill /> authentication.
|
||||
|
||||
![Login form](/img/authui/login.png)
|
||||
|
||||
You can use the `LoginForm` component to build your login page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { LoginPage } from "@client/LoginPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Use it like this
|
||||
export function LoginPage() {
|
||||
return <LoginForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { LoginPage } from "@client/LoginPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Use it like this
|
||||
export function LoginPage() {
|
||||
return <LoginForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
It will automatically show the correct authentication providers based on your `main.wasp` file.
|
||||
|
||||
### Signup Form
|
||||
|
||||
Used with <UsernameAndPasswordPill />, <EmailPill />, <GithubPill /> and <GooglePill /> authentication.
|
||||
|
||||
![Signup form](/img/authui/signup.png)
|
||||
|
||||
You can use the `SignupForm` component to build your signup page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { SignupPage } from "@client/SignupPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/SignupPage.jsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
// Use it like this
|
||||
export function SignupPage() {
|
||||
return <SignupForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { SignupPage } from "@client/SignupPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/SignupPage.tsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
// Use it like this
|
||||
export function SignupPage() {
|
||||
return <SignupForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
It will automatically show the correct authentication providers based on your `main.wasp` file.
|
||||
|
||||
### Forgot Password Form
|
||||
|
||||
Used with <EmailPill /> authentication.
|
||||
|
||||
If users forget their password, they can use this form to reset it.
|
||||
|
||||
![Forgot password form](/img/authui/forgot_password.png)
|
||||
|
||||
You can use the `ForgotPasswordForm` component to build your own forgot password page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { ForgotPasswordPage } from "@client/ForgotPasswordPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/ForgotPasswordPage.jsx"
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
|
||||
// Use it like this
|
||||
export function ForgotPasswordPage() {
|
||||
return <ForgotPasswordForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { ForgotPasswordPage } from "@client/ForgotPasswordPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/ForgotPasswordPage.tsx"
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
|
||||
// Use it like this
|
||||
export function ForgotPasswordPage() {
|
||||
return <ForgotPasswordForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Reset Password Form
|
||||
|
||||
Used with <EmailPill /> authentication.
|
||||
|
||||
After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.
|
||||
|
||||
![Reset password form](/img/authui/reset_password.png)
|
||||
|
||||
You can use the `ResetPasswordForm` component to build your reset password page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { ResetPasswordPage } from "@client/ResetPasswordPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/ResetPasswordPage.jsx"
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
|
||||
// Use it like this
|
||||
export function ResetPasswordPage() {
|
||||
return <ResetPasswordForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { ResetPasswordPage } from "@client/ResetPasswordPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/ResetPasswordPage.tsx"
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
|
||||
// Use it like this
|
||||
export function ResetPasswordPage() {
|
||||
return <ResetPasswordForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Verify Email Form
|
||||
|
||||
Used with <EmailPill /> authentication.
|
||||
|
||||
After users sign up, they will receive an email with a link to this form where they can verify their email.
|
||||
|
||||
![Verify email form](/img/authui/email_verification.png)
|
||||
|
||||
You can use the `VerifyEmailForm` component to build your email verification page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { VerifyEmailPage } from "@client/VerifyEmailPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/VerifyEmailPage.jsx"
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
|
||||
// Use it like this
|
||||
export function VerifyEmailPage() {
|
||||
return <VerifyEmailForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { VerifyEmailPage } from "@client/VerifyEmailPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/VerifyEmailPage.tsx"
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
|
||||
// Use it like this
|
||||
export function VerifyEmailPage() {
|
||||
return <VerifyEmailForm />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Customization 💅🏻
|
||||
|
||||
You customize all of the available forms by passing props to them.
|
||||
|
||||
Props you can pass to all of the forms:
|
||||
|
||||
1. `appearance` - customize the form colors (via design tokens)
|
||||
2. `logo` - path to your logo
|
||||
3. `socialLayout` - layout of the social buttons, which can be `vertical` or `horizontal`
|
||||
|
||||
### 1. Customizing the Colors
|
||||
|
||||
We use [Stitches](https://stitches.dev/) to style the Auth UI. You can customize the styles by overriding the default theme tokens.
|
||||
|
||||
:::info List of all available tokens
|
||||
|
||||
See the [list of all available tokens](https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/react-app/src/stitches.config.js) which you can override.
|
||||
|
||||
:::
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="client/appearance.js"
|
||||
export const authAppearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import { authAppearance } from './appearance'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass the appearance object to the form
|
||||
appearance={authAppearance}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="client/appearance.ts"
|
||||
import type { CustomizationOptions } from '@wasp/auth/forms/types'
|
||||
|
||||
export const authAppearance: CustomizationOptions['appearance'] = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import { authAppearance } from './appearance'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass the appearance object to the form
|
||||
appearance={authAppearance}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We recommend defining your appearance in a separate file and importing it into your components.
|
||||
|
||||
### 2. Using Your Logo
|
||||
|
||||
You can add your logo to the Auth UI by passing the `logo` prop to any of the components.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import Logo from './logo.png'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass in the path to your logo
|
||||
logo={Logo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import Logo from './logo.png'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass in the path to your logo
|
||||
logo={Logo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 3. Social Buttons Layout
|
||||
|
||||
You can change the layout of the social buttons by passing the `socialLayout` prop to any of the components. It can be either `vertical` or `horizontal` (default).
|
||||
|
||||
If we pass in `vertical`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass in the socialLayout prop
|
||||
socialLayout="vertical"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<LoginForm
|
||||
// Pass in the socialLayout prop
|
||||
socialLayout="vertical"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We get this:
|
||||
|
||||
![Vertical social buttons](/img/authui/vertical_social_buttons.png)
|
||||
|
||||
### Let's Put Everything Together 🪄
|
||||
|
||||
If we provide the logo and custom colors:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```ts title="client/appearance.js"
|
||||
export const appearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
import { authAppearance } from './appearance'
|
||||
import todoLogo from './todoLogo.png'
|
||||
|
||||
export function LoginPage() {
|
||||
return <LoginForm appearance={appearance} logo={todoLogo} />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="client/appearance.ts"
|
||||
import type { CustomizationOptions } from '@wasp/auth/forms/types'
|
||||
|
||||
export const appearance: CustomizationOptions['appearance'] = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
import { authAppearance } from './appearance'
|
||||
import todoLogo from './todoLogo.png'
|
||||
|
||||
export function LoginPage() {
|
||||
return <LoginForm appearance={appearance} logo={todoLogo} />
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We get a form looking like this:
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<img src="/img/authui/custom_login.gif" alt="Custom login form" />
|
||||
</div>
|
652
web/docs/auth/username-and-pass.md
Normal file
652
web/docs/auth/username-and-pass.md
Normal file
@ -0,0 +1,652 @@
|
||||
---
|
||||
title: Username & Password
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.
|
||||
|
||||
## Setting Up Username & Password Authentication
|
||||
|
||||
To set up username authentication we need to:
|
||||
1. Enable username authentication in the Wasp file
|
||||
1. Add the user entity
|
||||
1. Add the routes and pages
|
||||
1. Use Auth UI components in our pages
|
||||
|
||||
Structure of the `main.wasp` file we will end up with:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// Configuring e-mail authentication
|
||||
app myApp {
|
||||
auth: { ... }
|
||||
}
|
||||
// Defining User entity
|
||||
entity User { ... }
|
||||
// Defining routes and pages
|
||||
route SignupRoute { ... }
|
||||
page SignupPage { ... }
|
||||
// ...
|
||||
```
|
||||
|
||||
### 1. Enable Username Authentication
|
||||
|
||||
Let's start with adding the following to our `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {11}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// 1. Specify the user entity (we'll define it next)
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// 2. Enable username authentication
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {11}
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// 1. Specify the user entity (we'll define it next)
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// 2. Enable username authentication
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the `usernameAndPassword` auth method options [here](#fields-in-the-usernameandpassword-dict).
|
||||
|
||||
### 2. Add the User Entity
|
||||
|
||||
When username authentication is enabled, Wasp expects certain fields in your `userEntity`. Let's add these fields to our `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp" {4-5}
|
||||
// 3. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
// Add your own fields below
|
||||
// ...
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp" {4-5}
|
||||
// 3. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
// Add your own fields below
|
||||
// ...
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the `userEntity` fields [here](#userentity-fields).
|
||||
|
||||
### 3. Add the Routes and Pages
|
||||
|
||||
Next, we need to define the routes and pages for the authentication pages.
|
||||
|
||||
Add the following to the `main.wasp` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// 4. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.jsx"
|
||||
}
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { Signup } from "@client/pages/auth.jsx"
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
// 4. Define the routes
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth.tsx"
|
||||
}
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { Signup } from "@client/pages/auth.tsx"
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We'll define the React components for these pages in the `client/pages/auth.{jsx,tsx}` file below.
|
||||
|
||||
### 4. Create the Client Pages
|
||||
|
||||
:::info
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](/docs/project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```tsx title="client/pages/auth.jsx"
|
||||
import { LoginForm } from "@wasp/auth/forms/Login";
|
||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function Signup() {
|
||||
return (
|
||||
<Layout>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
import { LoginForm } from "@wasp/auth/forms/Login";
|
||||
import { SignupForm } from "@wasp/auth/forms/Signup";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Layout>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export function Signup() {
|
||||
return (
|
||||
<Layout>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
// A layout component to center the content
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](/docs/auth/ui).
|
||||
|
||||
### Conclusion
|
||||
|
||||
That's it! We have set up username authentication in our app. 🎉
|
||||
|
||||
Running `wasp db migrate-dev` and then `wasp start` should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the [using auth docs](/docs/auth/overview).
|
||||
|
||||
## Customizing the Auth Flow
|
||||
|
||||
The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.
|
||||
|
||||
Read more about the default username and password validation rules and how to override them in the [using auth docs](/docs/auth/overview).
|
||||
|
||||
If you require more control in your authentication flow, you can achieve that in the following ways:
|
||||
1. Create your UI and use `signup` and `login` actions.
|
||||
1. Create your custom sign-up and login [actions](#) which uses the Prisma client, along with your custom code.
|
||||
|
||||
### 1. Using the `signup` and `login` actions
|
||||
|
||||
#### `login()`
|
||||
An action for logging in the user.
|
||||
|
||||
It takes two arguments:
|
||||
|
||||
- `username: string` <Required />
|
||||
|
||||
Username of the user logging in.
|
||||
|
||||
- `password: string` <Required />
|
||||
|
||||
Password of the user logging in.
|
||||
|
||||
You can use it like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="client/pages/auth.jsx"
|
||||
// Importing the login action 👇
|
||||
import login from '@wasp/auth/login'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function LoginPage() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
const history = useHistory()
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await login(username, password)
|
||||
history.push('/')
|
||||
} catch (error) {
|
||||
setError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
// Importing the login action 👇
|
||||
import login from '@wasp/auth/login'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function LoginPage() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const history = useHistory()
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await login(username, password)
|
||||
history.push('/')
|
||||
} catch (error: unknown) {
|
||||
setError(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
When using the exposed `login()` function, make sure to implement your redirect on success login logic (e.g. redirecting to home).
|
||||
:::
|
||||
|
||||
#### `signup()`
|
||||
An action for signing up the user. This action does not log in the user, you still need to call `login()`.
|
||||
|
||||
It takes one argument:
|
||||
- `userFields: object` <Required />
|
||||
|
||||
It has the following fields:
|
||||
- `username: string` <Required />
|
||||
|
||||
- `password: string` <Required />
|
||||
|
||||
:::info
|
||||
Wasp only stores the auth-related fields of the user entity. Adding extra fields to `userFields` will not have any effect.
|
||||
|
||||
If you need to add extra fields to the user entity, we suggest doing it in a separate step after the user logs in for the first time.
|
||||
:::
|
||||
|
||||
You can use it like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="client/pages/auth.jsx"
|
||||
// Importing the signup and login actions 👇
|
||||
import signup from '@wasp/auth/signup'
|
||||
import login from '@wasp/auth/login'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function Signup() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(null)
|
||||
const history = useHistory()
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await signup({
|
||||
username,
|
||||
password,
|
||||
})
|
||||
await login(username, password)
|
||||
history.push("/")
|
||||
} catch (error) {
|
||||
setError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="client/pages/auth.tsx"
|
||||
// Importing the signup and login actions 👇
|
||||
import signup from '@wasp/auth/signup'
|
||||
import login from '@wasp/auth/login'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function Signup() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const history = useHistory()
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await signup({
|
||||
username,
|
||||
password,
|
||||
})
|
||||
await login(username, password)
|
||||
history.push("/")
|
||||
} catch (error: unknown) {
|
||||
setError(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 2. Creating your custom actions
|
||||
|
||||
The code of your custom sign-up action can look like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action signupUser {
|
||||
fn: import { signUp } from "@server/auth/signup.js",
|
||||
entities: [User]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```js title="src/server/auth/signup.js"
|
||||
export const signUp = async (args, context) => {
|
||||
// Your custom code before sign-up.
|
||||
// ...
|
||||
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
}
|
||||
})
|
||||
|
||||
// Your custom code after sign-up.
|
||||
// ...
|
||||
return newUser
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action signupUser {
|
||||
fn: import { signUp } from "@server/auth/signup.js",
|
||||
entities: [User]
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="src/server/auth/signup.ts"
|
||||
import type { User } from '@wasp/entities'
|
||||
import type { SignupUser } from '@wasp/actions/types'
|
||||
|
||||
type SignupPayload = Pick<User, 'username' | 'password'>
|
||||
|
||||
export const signUp: SignupUser<SignupPayload, User> = async (args, context) => {
|
||||
// Your custom code before sign-up.
|
||||
// ...
|
||||
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
}
|
||||
})
|
||||
|
||||
// Your custom code after sign-up.
|
||||
// ...
|
||||
return newUser
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Auth
|
||||
|
||||
To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the [using auth docs](/docs/auth/overview).
|
||||
|
||||
## API Reference
|
||||
|
||||
### `userEntity` fields
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
|
||||
// Wasp requires the `userEntity` to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
|
||||
// Wasp requires the `userEntity` to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Username & password auth requires that `userEntity` specified in `auth` contains:
|
||||
|
||||
- `username` field of type `String`
|
||||
- `password` field of type `String`
|
||||
|
||||
### Fields in the `usernameAndPassword` dict
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
// ...
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
// ...
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
`usernameAndPassword` dict doesn't have any options at the moment.
|
||||
:::
|
||||
|
||||
You can read about the rest of the `auth` options in the [using auth](/docs/auth/overview) section of the docs.
|
336
web/docs/data-model/backends.md
Normal file
336
web/docs/data-model/backends.md
Normal file
@ -0,0 +1,336 @@
|
||||
---
|
||||
title: Databases
|
||||
---
|
||||
|
||||
[Entities](/docs/data-model/entities.md), [Operations](/docs/data-model/operations/overview) and [Automatic CRUD](/docs/data-model/crud.md) together make a high-level interface for working with your app's data. Still, all that data has to live somewhere, so let's see how Wasp deals with databases.
|
||||
|
||||
## Supported Database Backends
|
||||
|
||||
Wasp supports multiple database backends. We'll list and explain each one.
|
||||
|
||||
### SQLite
|
||||
|
||||
The default database Wasp uses is [SQLite](https://www.sqlite.org/index.html).
|
||||
|
||||
SQLite is a great way for getting started with a new project because it doesn't require any configuration, but Wasp can only use it in development. Once you want to deploy your Wasp app to production, you'll need to switch to PostgreSQL and stick with it.
|
||||
|
||||
Fortunately, migrating from SQLite to PostgreSQL is pretty simple, and we have [a guide](#migrating-from-sqlite-to-postgresql) to help you.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
[PostgreSQL](https://www.postgresql.org/) is the most advanced open source database and the fourth most popular database overall.
|
||||
It's been in active development for 20 years.
|
||||
Therefore, if you're looking for a battle-tested database, look no further.
|
||||
|
||||
To use Wasp with PostgreSQL, you'll have to ensure a database instance is running during development. Wasp needs access to your database for commands such as `wasp start` or `wasp db migrate-dev` and expects to find a connection string in the `DATABASE_URL` environment variable.
|
||||
|
||||
We cover all supported ways of connecting to a database in [the next section](#connecting-to-a-database).
|
||||
|
||||
### Migrating from SQLite to PostgreSQL
|
||||
|
||||
To run your Wasp app in production, you'll need to switch from SQLite to PostgreSQL.
|
||||
|
||||
1. Set the `app.db.system` fild to PostgreSQL.
|
||||
2. Delete all the old migrations, since they are SQLite migrations and can't be used with PostgreSQL:
|
||||
|
||||
```bash
|
||||
rm -r migrations/
|
||||
```
|
||||
|
||||
3. Ensure your new database is running (check the [section on connecing to a database](#connecting-to-a-database) to see how). Leave it running, since we need it for the next step.
|
||||
4. In a different terminal, run `wasp db migrate-dev` to apply the changes and create a new initial migration.
|
||||
5. That is it, you are all done!
|
||||
|
||||
## Connecting to a Database
|
||||
|
||||
Assuming you're not using SQLite, Wasp offers two ways of connecting your app to a database instance:
|
||||
|
||||
1. A ready-made dev database that requires minimal setup and is great for quick prototyping.
|
||||
2. A "real" database Wasp can connect to and use in production.
|
||||
|
||||
### Using the Dev Database Provided by Wasp
|
||||
|
||||
The command `wasp start db` will start a default PostgreSQL dev database for you.
|
||||
|
||||
Your Wasp app will automatically connect to it, just keep `wasp start db` running in the background.
|
||||
Also, make sure that:
|
||||
|
||||
- You have [Docker installed](https://www.docker.com/get-started/) and in `PATH`.
|
||||
- The port `5432` isn't taken.
|
||||
|
||||
### Connecting to an existing database
|
||||
|
||||
If you want to spin up your own dev database (or connect to an external one), you can tell Wasp about it using the `DATABASE_URL` environment variable. Wasp will use the value of `DATABASE_URL` as a connection string.
|
||||
|
||||
The easiest way to set the necessary `DATABASE_URL` environment variable is by adding it to the [.env.server](/docs/project/env-vars) file in the root dir of your Wasp project (if that file doesn't yet exist, create it).
|
||||
|
||||
Alternatively, you can set it inline when running `wasp` (this applies to all environment variables):
|
||||
|
||||
```bash
|
||||
DATABASE_URL=<my-db-url> wasp ...
|
||||
```
|
||||
|
||||
This trick is useful for running a certain `wasp` command on a specific database.
|
||||
For example, you could do:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=<production-db-url> wasp db seed myProductionSeed
|
||||
```
|
||||
|
||||
This command seeds the data for a fresh staging or production database.
|
||||
To more precisely understand how seeding works, keep reading.
|
||||
|
||||
## Seeding the Database
|
||||
|
||||
**Database seeding** is a term used for populating the database with some initial data.
|
||||
|
||||
Seeding is most commonly used for two following scenarios:
|
||||
|
||||
1. To put the development database into a state convenient for working and testing.
|
||||
2. To initialize any database (`dev`, `staging`, or `prod`) with essential data it requires to operate.
|
||||
For example, populating the Currency table with default currencies, or the Country table with all available countries.
|
||||
|
||||
### Writing a Seed Function
|
||||
|
||||
You can define as many **seed functions** as you want in an array under the `app.db.seeds` field:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
// ...
|
||||
db: {
|
||||
// ...
|
||||
seeds: [
|
||||
import { devSeedSimple } from "@server/dbSeeds.js",
|
||||
import { prodSeed } from "@server/dbSeeds.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
// ...
|
||||
db: {
|
||||
// ...
|
||||
seeds: [
|
||||
import { devSeedSimple } from "@server/dbSeeds.js",
|
||||
import { prodSeed } from "@server/dbSeeds.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Each seed function must be an async function that takes one argument, `prismaClient`, which is a [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client/crud) instance used to interact with the database.
|
||||
This is the same Prisma Client instance that Wasp uses internally and thus includes all of the usual features (e.g., password hashing).
|
||||
|
||||
Since a seed function falls under server-side code, it can import other server-side functions. This is convenient because you might want to seed the database using Actions.
|
||||
|
||||
Here's an example of a seed function that imports an Action:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
import { createTask } from './actions.js'
|
||||
|
||||
export const devSeedSimple = async (prismaClient) => {
|
||||
const user = await createUser(prismaClient, {
|
||||
username: 'RiuTheDog',
|
||||
password: 'bark1234',
|
||||
})
|
||||
|
||||
await createTask(
|
||||
{ description: 'Chase the cat' },
|
||||
{ user, entities: { Task: prismaClient.task } }
|
||||
)
|
||||
}
|
||||
|
||||
async function createUser(prismaClient, data) {
|
||||
const { password, ...newUser } = await prismaClient.user.create({ data })
|
||||
return newUser
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
import { createTask } from './actions.js'
|
||||
import { User } from '@wasp/entities'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
type SanitizedUser = Omit<User, 'password'>
|
||||
|
||||
export const devSeedSimple = async (prismaClient: PrismaClient) => {
|
||||
const user = await createUser(prismaClient, {
|
||||
username: 'RiuTheDog',
|
||||
password: 'bark1234',
|
||||
})
|
||||
|
||||
await createTask(
|
||||
{ description: 'Chase the cat', isDone: false },
|
||||
{ user, entities: { Task: prismaClient.task } }
|
||||
)
|
||||
}
|
||||
|
||||
async function createUser(
|
||||
prismaClient: PrismaClient,
|
||||
data: Pick<User, 'username' | 'password'>
|
||||
): Promise<SanitizedUser> {
|
||||
const { password, ...newUser } = await prismaClient.user.create({ data })
|
||||
return newUser
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Running seed functions
|
||||
|
||||
Run the command `wasp db seed` and Wasp will ask you which seed function you'd like to run (if you've defined more than one).
|
||||
|
||||
Alternatively, run the command `wasp db seed <seed-name>` to choose a specific seed function right away, for example:
|
||||
|
||||
```
|
||||
wasp db seed devSeedSimple
|
||||
```
|
||||
|
||||
Check the [API Reference](#cli-commands-for-seeding-the-database) for more details on these commands.
|
||||
|
||||
:::tip
|
||||
You'll often want to call `wasp db seed` right after you run `wasp db reset`, as it makes sense to fill the database with initial data after clearing it.
|
||||
:::
|
||||
|
||||
## API Reference
|
||||
|
||||
You can tell Wasp which database to use in the `app` declaration's `db` field:
|
||||
|
||||
### The `app.db` Field
|
||||
|
||||
Here's an example that uses the `app.db` field to its full potential:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
db: {
|
||||
system: PostgreSQL,
|
||||
seeds: [
|
||||
import devSeed from "@server/dbSeeds.js"
|
||||
],
|
||||
prisma: {
|
||||
clientPreviewFeatures: ["extendedWhereUnique"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
db: {
|
||||
system: PostgreSQL,
|
||||
seeds: [
|
||||
import devSeed from "@server/dbSeeds.js"
|
||||
],
|
||||
prisma: {
|
||||
clientPreviewFeatures: ["extendedWhereUnique"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`app.db` is a dictionary with the following fields (all fields are optional):
|
||||
|
||||
- `system: DbSystem`
|
||||
|
||||
The database system Wasp should use. It can be either PostgreSQL or SQLite.
|
||||
The default value for the field is SQLite (this default value also applies if the entire `db` field is left unset).
|
||||
Whenever you modify the `db.system` field, make sure to run `wasp db migrate-dev` to apply the changes.
|
||||
|
||||
- `seeds: [ServerImport]`
|
||||
|
||||
Defines the seed functions you can use with the `wasp db seed` command to seed your database with initial data.
|
||||
Read the [Seeding section](#seeding-the-database) for more details.
|
||||
|
||||
- `prisma: PrismaOptions`
|
||||
|
||||
Additional configuration for Prisma.
|
||||
It currently only supports a single field:
|
||||
|
||||
- `clientPreviewFeatures : [string]`
|
||||
|
||||
Allows you to define [Prisma client preview features](https://www.prisma.io/docs/concepts/components/preview-features/client-preview-features).
|
||||
|
||||
### CLI Commands for Seeding the Database
|
||||
|
||||
Use one of the following commands to run the seed functions:
|
||||
|
||||
- `wasp db seed`
|
||||
|
||||
If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.
|
||||
|
||||
- `wasp db seed <seed-name>`
|
||||
|
||||
This command runs the seed function with the specified name. The name is the identifier used in its `import` expression in the `app.db.seeds` list.
|
||||
For example, to run the seed function `devSeedSimple` which was defined like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
// ...
|
||||
db: {
|
||||
// ...
|
||||
seeds: [
|
||||
// ...
|
||||
import { devSeedSimple } from "@server/dbSeeds.js",
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title=main.wasp
|
||||
app MyApp {
|
||||
// ...
|
||||
db: {
|
||||
// ...
|
||||
seeds: [
|
||||
// ...
|
||||
import { devSeedSimple } from "@server/dbSeeds.js",
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Use the following command:
|
||||
|
||||
```
|
||||
wasp db seed devSeedSimple
|
||||
```
|
749
web/docs/data-model/crud.md
Normal file
749
web/docs/data-model/crud.md
Normal file
@ -0,0 +1,749 @@
|
||||
---
|
||||
title: Automatic CRUD
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
import ImgWithCaption from '../../blog/components/ImgWithCaption'
|
||||
|
||||
If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.
|
||||
|
||||
Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.
|
||||
|
||||
With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting [Entities](/docs/data-model/entities). As you update definitions for your Entities, Wasp automatically regenerates the backend logic.
|
||||
|
||||
:::caution Early preview
|
||||
This feature is currently in early preview and we are actively working on it. Read more about [our plans](#future-of-crud-operations-in-wasp) for CRUD operations.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Imagine we have a `Task` entity and we want to enable CRUD operations for it.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean
|
||||
psl=}
|
||||
```
|
||||
|
||||
We can then define a new `crud` called `Tasks`.
|
||||
|
||||
We specify to use the `Task` entity and we enable the `getAll`, `get`, `create` and `update` operations (let's say we don't need the `delete` operation).
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks {
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {
|
||||
isPublic: true, // by default only logged in users can perform operations
|
||||
},
|
||||
get: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js",
|
||||
},
|
||||
update: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
1. It uses default implementation for `getAll`, `get`, and `update`,
|
||||
2. ... while specifying a custom implementation for `create`.
|
||||
3. `getAll` will be public (no auth needed), while the rest of the operations will be private.
|
||||
|
||||
Here's what it looks like when visualized:
|
||||
|
||||
<ImgWithCaption alt="Automatic CRUD with Wasp" source="img/crud_diagram.png" caption="Visualization of the Tasks crud declaration"/>
|
||||
|
||||
We can now use the CRUD queries and actions we just specified in our client code.
|
||||
|
||||
Keep reading for an example of Automatic CRUD in action, or skip ahead for the [API Reference](#api-reference)
|
||||
|
||||
## Example: A Simple TODO App
|
||||
|
||||
Let's create a full-app example that uses automatic CRUD. We'll stick to using the `Task` entity from the previous example, but we'll add a `User` entity and enable [username and password](/docs/auth/username-and-pass) based auth.
|
||||
|
||||
<ImgWithCaption alt="Automatic CRUD with Wasp" source="img/crud-guide.gif" caption="We are building a simple tasks app with username based auth"/>
|
||||
|
||||
### Creating the App
|
||||
|
||||
We can start by running `wasp new tasksCrudApp` and then adding the following to the `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app tasksCrudApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Tasks Crud App",
|
||||
|
||||
// We enabled auth and set the auth method to username and password
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
tasks Task[]
|
||||
psl=}
|
||||
|
||||
// We defined a Task entity on which we'll enable CRUD later on
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
psl=}
|
||||
|
||||
// Tasks app routes
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import { MainPage } from "@client/MainPage.jsx",
|
||||
authRequired: true,
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { LoginPage } from "@client/LoginPage.jsx",
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { SignupPage } from "@client/SignupPage.jsx",
|
||||
}
|
||||
```
|
||||
|
||||
We can then run `wasp db migrate-dev` to create the database and run the migrations.
|
||||
|
||||
### Adding CRUD to the `Task` Entity ✨
|
||||
|
||||
Let's add the following `crud` declaration to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
crud Tasks {
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
You'll notice that we enabled only `getAll` and `create` operations. This means that only these operations will be available.
|
||||
|
||||
We also overrode the `create` operation with a custom implementation. This means that the `create` operation will not be generated, but instead, the `createTask` function from `@server/tasks.js` will be used.
|
||||
|
||||
### Our Custom `create` Operation
|
||||
|
||||
Here's the `src/server/tasks.{js,ts}` file:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/tasks.js {15-20}
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401, 'User not authenticated.')
|
||||
}
|
||||
|
||||
const { description, isDone } = args
|
||||
const { Task } = context.entities
|
||||
|
||||
return await Task.create({
|
||||
data: {
|
||||
description,
|
||||
isDone,
|
||||
// Connect the task to the user that is creating it
|
||||
user: {
|
||||
connect: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/tasks.ts {23-28}
|
||||
import type { CreateAction } from '@wasp/crud/Tasks'
|
||||
import type { Task } from '@wasp/entities'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
type CreateTaskInput = { description: string; isDone: boolean }
|
||||
|
||||
export const createTask: CreateAction<CreateTaskInput, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401, 'User not authenticated.')
|
||||
}
|
||||
|
||||
const { description, isDone } = args
|
||||
const { Task } = context.entities
|
||||
|
||||
return await Task.create({
|
||||
data: {
|
||||
description,
|
||||
isDone,
|
||||
// Connect the task to the user that is creating it
|
||||
user: {
|
||||
connect: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We made a custom `create` operation because we want to make sure that the task is connected to the user that is creating it.
|
||||
Automatic CRUD doesn't support this by default (yet!).
|
||||
Read more about the default implementations [here](#declaring-a-crud-with-default-options).
|
||||
|
||||
### Using the Generated CRUD Operations on the Client
|
||||
|
||||
And let's use the generated operations in our client code:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="pages/MainPage.jsx"
|
||||
// highlight-next-line
|
||||
import { Tasks } from '@wasp/crud/Tasks'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MainPage = () => {
|
||||
// highlight-next-line
|
||||
const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
|
||||
// highlight-next-line
|
||||
const createTask = Tasks.create.useAction()
|
||||
const [taskDescription, setTaskDescription] = useState('')
|
||||
|
||||
function handleCreateTask() {
|
||||
createTask({ description: taskDescription, isDone: false })
|
||||
setTaskDescription('')
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '1.5rem',
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleCreateTask}>Create task</button>
|
||||
</div>
|
||||
<ul>
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id}>{task.description}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="pages/MainPage.tsx"
|
||||
// highlight-next-line
|
||||
import { Tasks } from '@wasp/crud/Tasks'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MainPage = () => {
|
||||
// highlight-next-line
|
||||
// Thanks to full-stack type safety, all payload types are inferred
|
||||
// highlight-next-line
|
||||
// automatically
|
||||
// highlight-next-line
|
||||
const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
|
||||
// highlight-next-line
|
||||
const createTask = Tasks.create.useAction()
|
||||
const [taskDescription, setTaskDescription] = useState('')
|
||||
|
||||
function handleCreateTask() {
|
||||
createTask({ description: taskDescription, isDone: false })
|
||||
setTaskDescription('')
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '1.5rem',
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleCreateTask}>Create task</button>
|
||||
</div>
|
||||
<ul>
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id}>{task.description}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And here are the login and signup pages, where we are using Wasp's [Auth UI](/docs/auth/ui) components:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<LoginForm />
|
||||
<div>
|
||||
<Link to="/signup">Create an account</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<LoginForm />
|
||||
<div>
|
||||
<Link to="/signup">Create an account</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/SignupPage.jsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
export function SignupPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<SignupForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/SignupPage.tsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
export function SignupPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<SignupForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
That's it. You can now run `wasp start` and see the app in action. ⚡️
|
||||
|
||||
You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.
|
||||
|
||||
## Future of CRUD Operations in Wasp
|
||||
|
||||
CRUD operations currently have a limited set of knowledge about the business logic they are implementing.
|
||||
|
||||
- For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the `create` operation in the example above.
|
||||
- Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
|
||||
- Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.
|
||||
|
||||
CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.
|
||||
|
||||
We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on [this GitHub issue](https://github.com/wasp-lang/wasp/issues/1253) to see how we are doing.
|
||||
|
||||
## API Reference
|
||||
|
||||
CRUD declaration work on top of existing entity declaration. We'll fully explore the API using two examples:
|
||||
|
||||
1. A basic CRUD declaration that relies on default options.
|
||||
2. A more involved CRUD declaration that uses extra options and overrides.
|
||||
|
||||
### Declaring a CRUD With Default Options
|
||||
|
||||
If we create CRUD operations for an entity named `Task`, like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks { // crud name here is "Tasks"
|
||||
entity: Task,
|
||||
operations: {
|
||||
get: {},
|
||||
getAll: {},
|
||||
create: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Wasp will give you the following default implementations:
|
||||
|
||||
**get** - returns one entity based on the `id` field
|
||||
|
||||
```js
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.findUnique({ where: { id: args.id } })
|
||||
```
|
||||
|
||||
**getAll** - returns all entities
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
// If the operation is not public, Wasp checks if an authenticated user
|
||||
// is making the request.
|
||||
|
||||
return Task.findMany()
|
||||
```
|
||||
|
||||
**create** - creates a new entity
|
||||
|
||||
```js
|
||||
// ...
|
||||
return Task.create({ data: args.data })
|
||||
```
|
||||
|
||||
**update** - updates an existing entity
|
||||
|
||||
```js
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.update({ where: { id: args.id }, data: args.data })
|
||||
```
|
||||
|
||||
**delete** - deletes an existing entity
|
||||
|
||||
```js
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.delete({ where: { id: args.id } })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks { // crud name here is "Tasks"
|
||||
entity: Task,
|
||||
operations: {
|
||||
get: {},
|
||||
getAll: {},
|
||||
create: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Wasp will give you the following default implementations:
|
||||
|
||||
**get** - returns one entity based on the `id` field
|
||||
|
||||
```ts
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.findUnique({ where: { id: args.id } })
|
||||
```
|
||||
|
||||
**getAll** - returns all entities
|
||||
|
||||
```ts
|
||||
// ...
|
||||
|
||||
// If the operation is not public, Wasp checks if an authenticated user
|
||||
// is making the request.
|
||||
|
||||
return Task.findMany()
|
||||
```
|
||||
|
||||
**create** - creates a new entity
|
||||
|
||||
```ts
|
||||
// ...
|
||||
return Task.create({ data: args.data })
|
||||
```
|
||||
|
||||
**update** - updates an existing entity
|
||||
|
||||
```ts
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.update({ where: { id: args.id }, data: args.data })
|
||||
```
|
||||
|
||||
**delete** - deletes an existing entity
|
||||
|
||||
```ts
|
||||
// ...
|
||||
// Wasp uses the field marked with `@id` in Prisma schema as the id field.
|
||||
return Task.delete({ where: { id: args.id } })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info Current Limitations
|
||||
In the default `create` and `update` implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.
|
||||
|
||||
[In the future](#future-of-crud-operations-in-wasp), we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.
|
||||
|
||||
For now, the solution is to provide an override function. You can override the default implementation by using the `overrideFn` option and implementing the validation logic yourself.
|
||||
|
||||
:::
|
||||
|
||||
### Declaring a CRUD With All Available Options
|
||||
|
||||
Here's an example of a more complex CRUD declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks { // crud name here is "Tasks"
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {
|
||||
isPublic: true, // optional, defaults to false
|
||||
},
|
||||
get: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js", // optional
|
||||
},
|
||||
update: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks { // crud name here is "Tasks"
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {
|
||||
isPublic: true, // optional, defaults to false
|
||||
},
|
||||
get: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js", // optional
|
||||
},
|
||||
update: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The CRUD declaration features the following fields:
|
||||
|
||||
- `entity: Entity` <Required />
|
||||
|
||||
The entity to which the CRUD operations will be applied.
|
||||
|
||||
- `operations: { [operationName]: CrudOperationOptions }` <Required />
|
||||
|
||||
The operations to be generated. The key is the name of the operation, and the value is the operation configuration.
|
||||
|
||||
- The possible values for `operationName` are:
|
||||
- `getAll`
|
||||
- `get`
|
||||
- `create`
|
||||
- `update`
|
||||
- `delete`
|
||||
- `CrudOperationOptions` can have the following fields:
|
||||
- `isPublic: bool` - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to `false`.
|
||||
- `overrideFn: ServerImport` - The import statement of the optional override implementation in Node.js.
|
||||
|
||||
#### Defining the overrides
|
||||
|
||||
Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:
|
||||
|
||||
- `args`
|
||||
|
||||
The arguments of the operation i.e. the data sent from the client.
|
||||
|
||||
- `context`
|
||||
|
||||
Context contains the `user` making the request and the `entities` object with the entity that's being operated on.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
You can also import types for each of the functions you want to override from `@wasp/crud/{crud name}`. The available types are:
|
||||
|
||||
- `GetAllQuery`
|
||||
- `GetQuery`
|
||||
- `CreateAction`
|
||||
- `UpdateAction`
|
||||
- `DeleteAction`
|
||||
|
||||
If you have a CRUD named `Tasks`, you would import the types like this:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
GetAllQuery,
|
||||
GetQuery,
|
||||
CreateAction,
|
||||
UpdateAction,
|
||||
DeleteAction,
|
||||
} from '@wasp/crud/Tasks'
|
||||
|
||||
// Each of the types is a generic type, so you can use it like this:
|
||||
export const getAllOverride: GetAllQuery<Input, Output> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
For a usage example, check the [example guide](/docs/data-model/crud#adding-crud-to-the-task-entity-).
|
||||
|
||||
#### Using the CRUD operations in client code
|
||||
|
||||
On the client, you import the CRUD operations from `@wasp/crud/{crud name}`. The names of the imports are the same as the names of the operations. For example, if you have a CRUD called `Tasks`, you would import the operations like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="SomePage.jsx"
|
||||
import { Tasks } from '@wasp/crud/Tasks'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="SomePage.tsx"
|
||||
import { Tasks } from '@wasp/crud/Tasks'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You can then access the operations like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="SomePage.jsx"
|
||||
const { data } = Tasks.getAll.useQuery()
|
||||
const { data } = Tasks.get.useQuery({ id: 1 })
|
||||
const createAction = Tasks.create.useAction()
|
||||
const updateAction = Tasks.update.useAction()
|
||||
const deleteAction = Tasks.delete.useAction()
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="SomePage.tsx"
|
||||
const { data } = Tasks.getAll.useQuery()
|
||||
const { data } = Tasks.get.useQuery({ id: 1 })
|
||||
const createAction = Tasks.create.useAction()
|
||||
const updateAction = Tasks.update.useAction()
|
||||
const deleteAction = Tasks.delete.useAction()
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
All CRUD operations are implemented with [Queries and Actions](/docs/data-model/operations/overview) under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)
|
||||
|
||||
---
|
||||
|
||||
Join our **community** on [Discord](https://discord.com/invite/rzdnErX), where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.
|
105
web/docs/data-model/entities.md
Normal file
105
web/docs/data-model/entities.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Entities
|
||||
---
|
||||
|
||||
Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.
|
||||
|
||||
Wasp uses the excellent [Prisma ORM](https://www.prisma.io/) to implement all database functionality and occasionally enhances it with a thin abstraction layer.
|
||||
Wasp Entities directly correspond to [Prisma's data model](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model). Still, you don't need to be familiar with Prisma to effectively use Wasp, as it comes with a simple API wrapper for working with Prisma's core features.
|
||||
|
||||
The only requirement for defining Wasp Entities is familiarity with the **_Prisma Schema Language (PSL)_**, a simple definition language explicitly created for defining models in Prisma.
|
||||
The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:
|
||||
|
||||
- [Basic intro and examples](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema)
|
||||
- [A more exhaustive language specification](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
||||
|
||||
## Defining an Entity
|
||||
|
||||
As mentioned, an `entity` declaration represents a database model.
|
||||
|
||||
Each `Entity` declaration corresponds 1-to-1 to [Prisma's data model](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model). Here's how you could define an Entity that represents a Task:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Let's go through this declaration in detail:
|
||||
|
||||
- `entity Task` - This tells Wasp that we wish to define an Entity (i.e., database model) called `Task`. Wasp automatically creates a table called `tasks`.
|
||||
- `{=psl ... psl=}` - Wasp treats everything that comes between the two `psl` tags as [PSL (Prisma Schema Language)](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema).
|
||||
|
||||
The above PSL definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the `tasks` table has three columns):
|
||||
|
||||
- `id` - An integer value serving as a primary key. The database automatically generates it by incrementing the previously generated `id`.
|
||||
- `description` - A string value for storing the task's description.
|
||||
- `isDone` - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to `false` by default.
|
||||
|
||||
### Working with Entities
|
||||
|
||||
Let's see how you can define and work with Wasp Entities:
|
||||
|
||||
1. Create/update some Entities in your `.wasp` file.
|
||||
2. Run `wasp db migrate-dev`. This command syncs the database model with the Entity definitions in your `.wasp` file. It does this by creating migration scripts.
|
||||
3. Migration scripts are automatically placed in the `migrations/` folder. Make sure to commit this folder into version control.
|
||||
4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about [operations](/docs/data-model/operations/overview)).
|
||||
|
||||
#### Using Entities in Operations
|
||||
|
||||
Most of the time, you will be working with Entities within the context of [Operations (Queries & Actions)](/docs/data-model/operations/overview). We'll see how that's done on the next page.
|
||||
|
||||
#### Using Entities directly
|
||||
|
||||
If you need more control, you can directly interact with Entities by importing and using the [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client/crud). We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.
|
||||
|
||||
You can only use the Prisma Client in your Wasp server code. You can import it like this:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
import prismaClient from '@wasp/dbClient'`
|
||||
|
||||
prismaClient.task.create({
|
||||
description: "Read the Entities doc",
|
||||
isDone: true // almost :)
|
||||
})
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
import prismaClient from '@wasp/dbClient'`
|
||||
|
||||
prismaClient.task.create({
|
||||
description: "Read the Entities doc",
|
||||
isDone: true // almost :)
|
||||
})
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Next steps
|
||||
|
||||
Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!
|
14
web/docs/data-model/operations/_superjson-note.md
Normal file
14
web/docs/data-model/operations/_superjson-note.md
Normal file
@ -0,0 +1,14 @@
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
:::tip
|
||||
Wasp uses [superjson](https://github.com/blitz-js/superjson) under the hood.
|
||||
This means you're not limited to only sending and receiving JSON payloads.
|
||||
|
||||
You can send and receive any superjson-compatible payload (like Dates, Sets, Lists, circular references, etc.) and let Wasp handle the (de)serialization.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
As long as you're annotating your Queries with the correct automatically generated types, TypeScript ensures your payloads are valid (i.e., Wasp knows how to serialize and deserialize them).
|
||||
</ShowForTs>
|
||||
|
||||
:::
|
875
web/docs/data-model/operations/actions.md
Normal file
875
web/docs/data-model/operations/actions.md
Normal file
@ -0,0 +1,875 @@
|
||||
---
|
||||
title: Actions
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
import SuperjsonNote from './\_superjson-note.md';
|
||||
|
||||
We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the [API Reference](#api-reference).
|
||||
|
||||
Actions are quite similar to [Queries](/docs/data-model/operations/queries.md), but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price.
|
||||
|
||||
Actions and Queries work together to keep data caches up-to-date.
|
||||
|
||||
:::tip
|
||||
Actions are almost identical to Queries in terms of their API.
|
||||
Therefore, if you're already familiar with Queries, you might find reading the entire guide repetitive.
|
||||
|
||||
We instead recommend skipping ahead and only reading [the differences between Queries and Actions](#differences-between-queries-and-actions), and consulting the [API Reference](#api-reference) as needed.
|
||||
:::
|
||||
|
||||
## Working with Actions
|
||||
|
||||
Actions are declared in Wasp and implemented in NodeJS. Wasp runs Actions within the server's context, but it also generates code that allows you to call them from anywhere in your code (either client or server) using the same interface.
|
||||
|
||||
This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching.
|
||||
Instead, just focus on developing the business logic inside your Action, and let Wasp handle the rest!
|
||||
|
||||
To create an Action, you need to:
|
||||
|
||||
1. Declare the Action in Wasp using the `action` declaration.
|
||||
2. Implement the Action's NodeJS functionality.
|
||||
|
||||
Once these two steps are completed, you can use the Action from anywhere in your code.
|
||||
|
||||
### Declaring Actions
|
||||
|
||||
To create an Action in Wasp, we begin with an `action` declaration. Let's declare two Actions - one for creating a task, and another for marking tasks as done:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js"
|
||||
}
|
||||
|
||||
action markTaskAsDone {
|
||||
fn: import { markTaskAsDone } from "@server/actions.js"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js"
|
||||
}
|
||||
|
||||
action markTaskAsDone {
|
||||
fn: import { markTaskAsDone } from "@server/actions.js"
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
Even though you are using TypeScript and plan to implement this Action in `src/server/actions.ts`, you still need to import it using a `.js` extension. Wasp internally uses `esnext` module resolution, which requires importing all files with a `.js` extension. This is only needed when importing `@server` files.
|
||||
|
||||
Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588).
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
If you want to know about all supported options for the `action` declaration, take a look at the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
The names of Wasp Actions and their implementations don't necessarily have to match. However, to avoid confusion, we'll keep them the same.
|
||||
|
||||
<SuperjsonNote />
|
||||
|
||||
After declaring a Wasp Action, two important things happen:
|
||||
|
||||
- Wasp **generates a server-side NodeJS function** that shares its name with the Action.
|
||||
|
||||
- Wasp **generates a client-side JavaScript function** that shares its name with the Action (e.g., `markTaskAsDone`).
|
||||
This function takes a single optional argument - an object containing any serializable data you wish to use inside the Action.
|
||||
Wasp will send this object over the network and pass it into the Action's implementation as its first positional argument (more on this when we look at the implementations).
|
||||
Such an abstraction works thanks to an HTTP API route handler Wasp generates on the server, which calls the Action's NodeJS implementation under the hood.
|
||||
|
||||
Generating these two functions ensures a uniform calling interface across the entire app (both client and server).
|
||||
|
||||
### Implementing Actions in Node
|
||||
|
||||
Now that we've declared the Action, what remains is to implement it. We've instructed Wasp to look for the Actions' implementations in the file `src/server/actions.{js,ts}`, so that's where we should export them from.
|
||||
|
||||
Here's how you might implement the previously declared Actions `createTask` and `markTaskAsDone`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
// our "database"
|
||||
let nextId = 4
|
||||
const tasks = [
|
||||
{ id: 1, description: 'Buy some eggs', isDone: true },
|
||||
{ id: 2, description: 'Make an omelette', isDone: false },
|
||||
{ id: 3, description: 'Eat breakfast', isDone: false },
|
||||
]
|
||||
|
||||
// You don't need to use the arguments if you don't need them
|
||||
export const createTask = (args) => {
|
||||
const newTask = {
|
||||
id: nextId,
|
||||
isDone: false,
|
||||
description: args.description,
|
||||
}
|
||||
nextId += 1
|
||||
tasks.push(newTask)
|
||||
return newTask
|
||||
}
|
||||
|
||||
// The 'args' object is something sent by the caller (most often from the client)
|
||||
export const markTaskAsDone = (args) => {
|
||||
const task = tasks.find((task) => task.id === args.id)
|
||||
if (!task) {
|
||||
// We'll show how to properly handle such errors later
|
||||
return
|
||||
}
|
||||
task.isDone = true
|
||||
}
|
||||
```
|
||||
|
||||
<SuperjsonNote />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import { CreateTask, MarkTaskAsDone } from '@wasp/actions/types'
|
||||
|
||||
type Task = {
|
||||
id: number
|
||||
description: string
|
||||
isDone: boolean
|
||||
}
|
||||
|
||||
// our "database"
|
||||
let nextId = 4
|
||||
const tasks = [
|
||||
{ id: 1, description: 'Buy some eggs', isDone: true },
|
||||
{ id: 2, description: 'Make an omelette', isDone: false },
|
||||
{ id: 3, description: 'Eat breakfast', isDone: false },
|
||||
]
|
||||
|
||||
// You don't need to use the arguments if you don't need them
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>, Task> = (
|
||||
args
|
||||
) => {
|
||||
const newTask = {
|
||||
id: nextId,
|
||||
isDone: false,
|
||||
description: args.description,
|
||||
}
|
||||
nextId += 1
|
||||
tasks.push(newTask)
|
||||
return newTask
|
||||
}
|
||||
|
||||
// The 'args' object is something sent by the caller (most often from the client)
|
||||
export const markTaskAsDone: MarkTaskAsDone<Pick<Task, 'id'>, void> = (
|
||||
args
|
||||
) => {
|
||||
const task = tasks.find((task) => task.id === args.id)
|
||||
if (!task) {
|
||||
// We'll show how to properly handle such errors later
|
||||
return
|
||||
}
|
||||
task.isDone = true
|
||||
}
|
||||
```
|
||||
|
||||
Wasp automatically generates the types `CreateTask` and `MarkTaskAsDone` based on the declarations in your Wasp file:
|
||||
|
||||
- `CreateTask` is a generic type that Wasp automatically generated based on the Action declaration for `createTask`.
|
||||
- `MarkTaskAsDone` is a generic type that Wasp automatically generated based on the Action declaration for `markTaskAsDone`.
|
||||
|
||||
You can use these types to specify the Action's input and output types.
|
||||
|
||||
The Action `createTask` expects to get an object of type `{ description: string }` and returns the newly created task (an object of type `Task`).
|
||||
|
||||
The Action `markTaskAsDone`, expects an object of type `{ id: number }` and doesn't return anything (i.e., its return type is `void`).
|
||||
|
||||
We've derived most of the payload types from the type `Task`.
|
||||
|
||||
Annotating the Actions is optional, but highly recommended. Doing so enables **full-stack type safety**. We'll see what this means when calling the Action from the client.
|
||||
|
||||
:::tip
|
||||
Wasp uses [superjson](https://github.com/blitz-js/superjson) under the hood. In other words, you don't need to limit yourself to only sending and receiving JSON payloads.
|
||||
|
||||
Send and receive any superjson-compatible payload (e.g., Dates, Sets, Lists, circular references, etc.) and let Wasp take care of the (de)serialization.
|
||||
|
||||
As long as you're annotating your Actions with correct automatically generated types, TypeScript ensures your payloads are valid (i.e., that Wasp knows how to serialize and deserialize them).
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
For a detailed explanation of the Action definition API (i.e., arguments and return values), check the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
### Using Actions
|
||||
|
||||
To use an Action, you can import it from `@wasp` and call it directly. As mentioned, the usage doesn't change depending on whether you're on the server or the client:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```javascript
|
||||
import createTask from '@wasp/actions/createTask.js'
|
||||
import markTasAsDone from '@wasp/actions/markTasAsDone.js'
|
||||
|
||||
// ...
|
||||
|
||||
const newTask = await createTask({ description: 'Learn TypeScript' })
|
||||
await markTasAsDone({ id: 1 })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```typescript
|
||||
import createTask from '@wasp/actions/createTask.js'
|
||||
import markTasAsDone from '@wasp/actions/markTasAsDone.js'
|
||||
|
||||
// TypeScript automatically infers the return values and type-checks
|
||||
// the payloads.
|
||||
const newTask = await createTask({ description: 'Keep learning TypeScript' })
|
||||
await markTasAsDone({ id: 1 })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When using Actions on the client, you'll most likely want to use them inside a component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {4,25} title=src/client/pages/Task.jsx
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getTask from '@wasp/queries/getTask'
|
||||
import markTaskAsDone from '@wasp/actions/markTaskAsDone'
|
||||
|
||||
export const TaskPage = ({ id }) => {
|
||||
const { data: task } = useQuery(getTask, { id })
|
||||
|
||||
if (!task) {
|
||||
return <h1>"Loading"</h1>
|
||||
}
|
||||
|
||||
const { description, isDone } = task
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? 'Yes' : 'No'}
|
||||
</p>
|
||||
{isDone || (
|
||||
<button onClick={() => markTaskAsDone({ id })}>Mark as done.</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {4,25} title=src/client/pages/Task.tsx
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getTask from '@wasp/queries/getTask'
|
||||
import markTaskAsDone from '@wasp/actions/markTaskAsDone'
|
||||
|
||||
export const TaskPage = ({ id }: { id: number }) => {
|
||||
const { data: task } = useQuery(getTask, { id })
|
||||
|
||||
if (!task) {
|
||||
return <h1>"Loading"</h1>
|
||||
}
|
||||
|
||||
const { description, isDone } = task
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? 'Yes' : 'No'}
|
||||
</p>
|
||||
{isDone || (
|
||||
<button onClick={() => markTaskAsDone({ id })}>Mark as done.</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Since Actions don't require reactivity, they are safe to use inside components without a hook. Still, Wasp provides comes with the `useAction` hook you can use to enhance actions. Read all about it in the [API Reference](#api-reference).
|
||||
|
||||
### Error Handling
|
||||
|
||||
For security reasons, all exceptions thrown in the Action's NodeJS implementation are sent to the client as responses with the HTTP status code `500`, with all other details removed.
|
||||
Hiding error details by default helps against accidentally leaking possibly sensitive information over the network.
|
||||
|
||||
If you do want to pass additional error information to the client, you can construct and throw an appropriate `HttpError` in your implementation:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/actions.js
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (args, context) => {
|
||||
throw new HttpError(
|
||||
403, // status code
|
||||
"You can't do this!", // message
|
||||
{ foo: 'bar' } // data
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/actions.ts
|
||||
import { CreateTask } from '@wasp/actions/types'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask: CreateTask = async (args, context) => {
|
||||
throw new HttpError(
|
||||
403, // status code
|
||||
"You can't do this!", // message
|
||||
{ foo: 'bar' } // data
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Using Entities in Actions
|
||||
|
||||
In most cases, resources used in Actions will be [Entities](/docs/data-model/entities.md).
|
||||
To use an Entity in your Action, add it to the `action` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {4,9} title="main.wasp"
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action markTaskAsDone {
|
||||
fn: import { markTaskAsDone } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {4,9} title="main.wasp"
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action markTaskAsDone {
|
||||
fn: import { markTaskAsDone } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Wasp will inject the specified Entity into the Action's `context` argument, giving you access to the Entity's Prisma API.
|
||||
Wasp invalidates frontend Query caches by looking at the Entities used by each Action/Query. Read more about Wasp's smart cache invalidation [here](#cache-invalidation).
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
// The 'args' object is the payload sent by the caller (most often from the client)
|
||||
export const createTask = async (args, context) => {
|
||||
const newTask = await context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
isDone: false,
|
||||
},
|
||||
})
|
||||
return newTask
|
||||
}
|
||||
|
||||
export const markTaskAsDone = async (args, context) => {
|
||||
await context.entities.Task.update({
|
||||
where: { id: args.id },
|
||||
data: { isDone: true },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import { CreateTask, MarkTaskAsDone } from '@wasp/actions/types'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
// The 'args' object is the payload sent by the caller (most often from the client)
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
const newTask = await context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
isDone: false,
|
||||
},
|
||||
})
|
||||
return newTask
|
||||
}
|
||||
|
||||
export const markTaskAsDone: MarkTaskAsDone<Pick<Task, 'id'>, void> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
await context.entities.Task.update({
|
||||
where: { id: args.id },
|
||||
data: { isDone: true },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Again, annotating the Actions is optional, but greatly improves **full-stack type safety**.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
||||
|
||||
### Prisma Error Helpers
|
||||
|
||||
In your Operations, you may wish to handle general Prisma errors with HTTP-friendly responses.
|
||||
|
||||
Wasp exposes two helper functions, `isPrismaError`, and `prismaErrorToHttpError`, for this purpose. As of now, we convert two specific Prisma errors (which we will continue to expand), with the rest being `500`. See the [source here](https://github.com/wasp-lang/wasp/blob/main/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js).
|
||||
|
||||
Here's how you can import and use them:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
|
||||
|
||||
// ...
|
||||
|
||||
try {
|
||||
await context.entities.Task.create({...})
|
||||
} catch (e) {
|
||||
if (isPrismaError(e)) {
|
||||
throw prismaErrorToHttpError(e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```js
|
||||
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
|
||||
|
||||
// ...
|
||||
|
||||
try {
|
||||
await context.entities.Task.create({...})
|
||||
} catch (e) {
|
||||
if (isPrismaError(e)) {
|
||||
throw prismaErrorToHttpError(e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Cache Invalidation
|
||||
|
||||
One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date.
|
||||
Since Wasp uses _react-query_ for Query management, we must make sure to invalidate Queries (more specifically, their cached results managed by _react-query_) whenever they become stale.
|
||||
|
||||
It's possible to invalidate the caches manually through several mechanisms _react-query_ provides (e.g., refetch, direct invalidation).
|
||||
However, since manual cache invalidation quickly becomes complex and error-prone, Wasp offers a faster and a more effective solution to get you started: **automatic Entity-based Query cache invalidation**.
|
||||
Because Actions can (and most often do) modify the state while Queries read it, Wasp invalidates a Query's cache whenever an Action that uses the same Entity is executed.
|
||||
|
||||
For example, if the Action `createTask` and Query `getTasks` both use the Entity `Task`, executing `createTask` may cause the cached result of `getTasks` to become outdated. In response, Wasp will invalidate it, causing `getTasks` to refetch data from the server and update it.
|
||||
|
||||
In practice, this means that Wasp keeps the Queries "fresh" without requiring you to think about cache invalidation.
|
||||
|
||||
On the other hand, this kind of automatic cache invalidation can become wasteful (some updates might not be necessary) and will only work for Entities. If that's an issue, you can use the mechanisms provided by _react-query_ for now, and expect more direct support in Wasp for handling those use cases in a nice, elegant way.
|
||||
|
||||
If you wish to optimistically set cache values after performing an Action, you can do so using [optimistic updates](https://stackoverflow.com/a/33009713). Configure them using Wasp's [useAction hook](#the-useaction-hook-and-optimistic-updates). This is currently the only manual cache invalidation mechanism Wasps supports natively. For everything else, you can always rely on _react-query_.
|
||||
|
||||
## Differences Between Queries and Actions
|
||||
|
||||
Actions and Queries are two closely related concepts in Wasp. They might seem to perform similar tasks, but Wasp treats them differently, and each concept represents a different thing.
|
||||
|
||||
Here are the key differences between Queries and Actions:
|
||||
|
||||
1. Actions can (and often should) modify the server's state, while Queries are only permitted to read it. Wasp relies on you adhering to this convention when performing cache invalidations, so it's crucial to follow it.
|
||||
2. Actions don't need to be reactive, so you can call them directly. However, Wasp does provide a [`useAction` React hook](#the-useaction-hook-and-optimistic-updates) for adding extra behavior to the Action (like optimistic updates).
|
||||
3. `action` declarations in Wasp are mostly identical to `query` declarations. The only difference lies in the declaration's name.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Declaring Actions in Wasp
|
||||
|
||||
The `action` declaration supports the following fields:
|
||||
|
||||
- `fn: ServerImport` <Required />
|
||||
|
||||
The import statement of the Action's NodeJs implementation.
|
||||
|
||||
- `entities: [Entity]`
|
||||
|
||||
A list of entities you wish to use inside your Action.
|
||||
For instructions on using Entities in Actions, take a look at [the guide](#using-entities-in-actions).
|
||||
|
||||
#### Example
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
Declaring the Action:
|
||||
|
||||
```wasp
|
||||
query createFoo {
|
||||
fn: import { createFoo } from "@server/actions.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Enables you to import and use it anywhere in your code (on the server or the client):
|
||||
|
||||
```js
|
||||
import createFoo from '@wasp/actions'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
Declaring the Action:
|
||||
|
||||
```wasp
|
||||
query createFoo {
|
||||
fn: import { createFoo } from "@server/actions.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
And also creates a type you can import on the server:
|
||||
|
||||
```ts
|
||||
import createFoo from '@wasp/actions'
|
||||
```
|
||||
|
||||
As well as the following type import on the server:
|
||||
|
||||
```ts
|
||||
import type { CreateFoo } from '@wasp/actions/types'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Implementing Actions
|
||||
|
||||
The Action's implementation is a NodeJS function that takes two arguments (it can be an `async` function if you need to use the `await` keyword).
|
||||
Since both arguments are positional, you can name the parameters however you want, but we'll stick with `args` and `context`:
|
||||
|
||||
1. `args` (type depends on the Action)
|
||||
|
||||
An object containing the data **passed in when calling the Action** (e.g., filtering conditions).
|
||||
Check [the usage examples](#using-actions) to see how to pass this object to the Action.
|
||||
|
||||
2. `context` (type depends on the Action)
|
||||
|
||||
An additional context object **passed into the Action by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Actions](#using-entities-in-actions) to see how to use the entities field on the `context` object, or the [auth section](/docs/auth/overview#using-the-contextuser-object) to see how to use the `user` object.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
Afer you [declare the Action](#declaring-actions), Wasp generates a generic type you can use when defining its implementation.
|
||||
For the Action declared as `createSomething`, the generated type is called `CreateSomething`:
|
||||
|
||||
```ts
|
||||
import { CreateSomething } from '@wasp/actions/types'
|
||||
```
|
||||
|
||||
It expects two (optional) type arguments:
|
||||
|
||||
1. `Input`
|
||||
|
||||
The type of the `args` object (i.e., the Action's input payload). The default value is `never`.
|
||||
|
||||
2. `Output`
|
||||
|
||||
The type of the Action's return value (i.e., the Action's output payload). The default value is `unknown`.
|
||||
|
||||
The defaults were chosen to make the type signature as permissive as possible. If don't want your Action to take/return anything, use `void` as a type argument.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
#### Example
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
The following Action:
|
||||
|
||||
```wasp
|
||||
action createFoo {
|
||||
fn: import { createFoo } from "@server/actions.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Expects to find a named export `createfoo` from the file `src/server/actions.js`
|
||||
|
||||
```js title=actions.js
|
||||
export const createFoo = (args, context) => {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp
|
||||
action createFoo {
|
||||
fn: import { createFoo } from "@server/actions.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Expects to find a named export `createfoo` from the file `src/server/actions.js`
|
||||
|
||||
You can use the generated type `CreateFoo` and specify the Action's inputs and outputs using its type arguments.
|
||||
|
||||
```ts title=actions.ts
|
||||
import { CreateFoo } from "@wasp/actions/types";
|
||||
|
||||
type Foo = // ...
|
||||
|
||||
export const createFoo: CreateFoo<{ bar: string }, Foo> = (args, context) => {
|
||||
// implementation
|
||||
};
|
||||
```
|
||||
|
||||
In this case, the Action expects to receive an object with a `bar` field of type `string` (this is the type of `args`), and return a value of type `Foo` (this must match the type of the Action's return value).
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### The `useAction` Hook and Optimistic Updates
|
||||
|
||||
Make sure you understand how [Queries](/docs/data-model/operations/queries.md) and [Cache Invalidation](#cache-invalidation) work before reading this chapter.
|
||||
|
||||
When using Actions in components, you can enhance them with the help of the `useAction` hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions.
|
||||
In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).
|
||||
|
||||
The `useAction` hook accepts two arguments:
|
||||
|
||||
- `actionFn` <Required />
|
||||
|
||||
The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.
|
||||
|
||||
- `actionOptions`
|
||||
|
||||
An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:
|
||||
|
||||
- `optimisticUpdates`
|
||||
|
||||
An array of objects where each object defines an [optimistic update](https://stackoverflow.com/a/33009713) to perform on the Query cache. To define an optimistic update, you must specify the following properties:
|
||||
|
||||
- `getQuerySpecifier` <Required />
|
||||
|
||||
A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with `useQuery(fetchFilteredTasks, {isDone: true }]`, your `getQuerySpecifier` function would have to return the array `[fetchFilteredTasks, { isDone: true}]`. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).
|
||||
|
||||
- `updateQuery` <Required />
|
||||
|
||||
The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:
|
||||
|
||||
- `item` - The argument you pass into the decorated Action.
|
||||
- `oldData` - The currently cached value for the Query identified by the specifier.
|
||||
|
||||
:::caution
|
||||
The `updateQuery` function must be a pure function. It must return the desired cache value identified by the `getQuerySpecifier` function and _must not_ perform any side effects.
|
||||
|
||||
Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).
|
||||
|
||||
Finally, your implementation of the `updateQuery` function should work correctly regardless of the state of `oldData` (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use _react-query_'s lower-level API (read more about it [here](#advanced-usage)).
|
||||
:::
|
||||
|
||||
Here's an example showing how to configure the Action `markTaskAsDone` that toggles a task's `isDone` status to perform an optimistic update:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {3,9,10,11,12,13,14,15,16,34} title=src/client/pages/Task.jsx
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { useAction } from '@wasp/actions'
|
||||
import getTask from '@wasp/queries/getTask'
|
||||
import markTaskAsDone from '@wasp/actions/markTaskAsDone'
|
||||
|
||||
const TaskPage = ({ id }) => {
|
||||
const { data: task } = useQuery(getTask, { id })
|
||||
const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: ({ id }) => [getTask, { id }],
|
||||
updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (!task) {
|
||||
return <h1>"Loading"</h1>
|
||||
}
|
||||
|
||||
const { description, isDone } = task
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? 'Yes' : 'No'}
|
||||
</p>
|
||||
{isDone || (
|
||||
<button onClick={() => markTaskAsDoneOptimistically({ id })}>
|
||||
Mark as done.
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```jsx {2,4,8,12,13,14,15,16,17,18,19,37} title=src/client/pages/Task.js
|
||||
import React from "react";
|
||||
import { useQuery } from "@wasp/queries";
|
||||
import { useAction, OptimisticUpdateDefinition } from "@wasp/actions";
|
||||
import getTask from "@wasp/queries/getTask";
|
||||
import markTaskAsDone from "@wasp/actions/markTaskAsDone";
|
||||
|
||||
type TaskPayload = Pick<Task, "id">;
|
||||
|
||||
const TaskPage = ({ id }: { id: number }) => {
|
||||
const { data: task } = useQuery(getTask, { id });
|
||||
const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: ({ id }) => [getTask, { id }],
|
||||
updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task>,
|
||||
],
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return <h1>"Loading"</h1>;
|
||||
}
|
||||
|
||||
const { description, isDone } = task;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? "Yes" : "No"}
|
||||
</p>
|
||||
{isDone || (
|
||||
<button onClick={() => markTaskAsDoneOptimistically({ id })}>
|
||||
Mark as done.
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskPage;
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Advanced usage
|
||||
|
||||
The `useAction` hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.
|
||||
|
||||
Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's `useAction` hook, you can use _react-query_'s `useMutation` hook and directly work with [their low-level API](https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates).
|
||||
|
||||
If you decide to use _react-query_'s API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the `queryCacheKey` property on any Query:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
|
||||
const queryKey = getTasks.queryCacheKey
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
|
||||
const queryKey = getTasks.queryCacheKey
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
12
web/docs/data-model/operations/overview.md
Normal file
12
web/docs/data-model/operations/overview.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
While Entities enable help you define your app's data model and relationships, Operations are all about working with this data.
|
||||
|
||||
There are two kinds of Operations: [Queries](/docs/data-model/operations/queries.md) and [Actions](/docs/data-model/operations/actions.md). As their names suggest,
|
||||
Queries are meant for reading data, and Actions are meant for changing it (either by updating existing entries or creating new ones).
|
||||
|
||||
Keep reading to find out all there is to know about Operations in Wasp.
|
656
web/docs/data-model/operations/queries.md
Normal file
656
web/docs/data-model/operations/queries.md
Normal file
@ -0,0 +1,656 @@
|
||||
---
|
||||
title: Queries
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
import SuperjsonNote from './\_superjson-note.md';
|
||||
|
||||
We'll explain what Queries are and how to use them. If you're looking for a detailed API specification, skip ahead to the [API Reference](#api-reference).
|
||||
|
||||
You can use Queries to fetch data from the server. They shouldn't modify the server's state.
|
||||
Fetching all comments on a blog post, a list of users that liked a video, information about a single product based on its ID... All of these are perfect use cases for a Query.
|
||||
|
||||
:::tip
|
||||
Queries are fairly similar to Actions in terms of their API.
|
||||
Therefore, if you're already familiar with Actions, you might find reading the entire guide repetitive.
|
||||
|
||||
We instead recommend skipping ahead and only reading [the differences between Queries and Actions](/docs/data-model/operations/actions#differences-between-queries-and-actions), and consulting the [API Reference](#api-reference) as needed.
|
||||
:::
|
||||
|
||||
## Working with Queries
|
||||
|
||||
You declare queries in the `.wasp` file and implement them using NodeJS. Wasp not only runs these queries within the server's context but also creates code that enables you to call them from any part of your codebase, whether it's on the client or server side.
|
||||
|
||||
This means you don't have to build an HTTP API for your query, manage server-side request handling, or even deal with client-side response handling and caching.
|
||||
Instead, just concentrate on implementing the business logic inside your query, and let Wasp handle the rest!
|
||||
|
||||
To create a Query, you must:
|
||||
|
||||
1. Declare the Query in Wasp using the `query` declaration.
|
||||
2. Define the Query's NodeJS implementation.
|
||||
|
||||
After completing these two steps, you'll be able to use the Query from any point in your code.
|
||||
|
||||
### Declaring Queries
|
||||
|
||||
To create a Query in Wasp, we begin with a `query` declaration.
|
||||
|
||||
Let's declare two Queries - one to fetch all tasks, and another to fetch tasks based on a filter, such as whether a task is done:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getAllTasks {
|
||||
fn: import { getAllTasks } from "@server/queries.js"
|
||||
}
|
||||
|
||||
query getFilteredTasks {
|
||||
fn: import { getFilteredTasks } from "@server/queries.js"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getAllTasks {
|
||||
fn: import { getAllTasks } from "@server/queries.js"
|
||||
}
|
||||
|
||||
query getFilteredTasks {
|
||||
fn: import { getFilteredTasks } from "@server/queries.js"
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
Even though you are using TypeScript and plan to implement this Query in `src/server/queries.ts`, you still need to import it using a `.js` extension. Wasp internally uses `esnext` module resolution, which requires importing all files with a `.js` extension. This is only needed when importing `@server` files.
|
||||
|
||||
Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588).
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
If you want to know about all supported options for the `query` declaration, take a look at the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
The names of Wasp Queries and their implementations don't need to match, but we'll keep them the same to avoid confusion.
|
||||
|
||||
:::info
|
||||
You might have noticed that we told Wasp to import Query implementations that don't yet exist. Don't worry about that for now. We'll write the implementations imported from `queries.{js,ts}` in the next section.
|
||||
|
||||
It's a good idea to start with the high-level concept (i.e., the Query declaration in the Wasp file) and only then deal with the implementation details (i.e., the Query's implementation in JavaScript).
|
||||
:::
|
||||
|
||||
After declaring a Wasp Query, two important things happen:
|
||||
|
||||
- Wasp **generates a server-side NodeJS function** that shares its name with the Query.
|
||||
|
||||
- Wasp **generates a client-side JavaScript function** that shares its name with the Query (e.g., `getFilteredTasks`).
|
||||
This function takes a single optional argument - an object containing any serializable data you wish to use inside the Query.
|
||||
Wasp will send this object over the network and pass it into the Query's implementation as its first positional argument (more on this when we look at the implementations).
|
||||
Such an abstraction works thanks to an HTTP API route handler Wasp generates on the server, which calls the Query's NodeJS implementation under the hood.
|
||||
|
||||
Generating these two functions ensures a uniform calling interface across the entire app (both client and server).
|
||||
|
||||
### Implementing Queries in Node
|
||||
|
||||
Now that we've declared the Query, what remains is to implement it.
|
||||
We've instructed Wasp to look for the Queries' implementations in the file `src/server/queries.{js,ts}`, so that's where we should export them from.
|
||||
|
||||
Here's how you might implement the previously declared Queries `getAllTasks` and `getFilteredTasks`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/queries.js"
|
||||
// our "database"
|
||||
const tasks = [
|
||||
{ id: 1, description: 'Buy some eggs', isDone: true },
|
||||
{ id: 2, description: 'Make an omelette', isDone: false },
|
||||
{ id: 3, description: 'Eat breakfast', isDone: false },
|
||||
]
|
||||
|
||||
// You don't need to use the arguments if you don't need them
|
||||
export const getAllTasks = () => {
|
||||
return tasks
|
||||
}
|
||||
|
||||
// The 'args' object is something sent by the caller (most often from the client)
|
||||
export const getFilteredTasks = (args) => {
|
||||
const { isDone } = args
|
||||
return tasks.filter((task) => task.isDone === isDone)
|
||||
}
|
||||
```
|
||||
|
||||
<SuperjsonNote />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/queries.ts"
|
||||
import { GetAllTasks, GetFilteredTasks } from '@wasp/queries/types'
|
||||
|
||||
type Task = {
|
||||
id: number
|
||||
description: string
|
||||
isDone: boolean
|
||||
}
|
||||
|
||||
// our "database"
|
||||
const tasks: Task[] = [
|
||||
{ id: 1, description: 'Buy some eggs', isDone: true },
|
||||
{ id: 2, description: 'Make an omelette', isDone: false },
|
||||
{ id: 3, description: 'Eat breakfast', isDone: false },
|
||||
]
|
||||
|
||||
// You don't need to use the arguments if you don't need them
|
||||
export const getAllTasks: GetAllTasks<void, Task[]> = () => {
|
||||
return tasks
|
||||
}
|
||||
|
||||
// The 'args' object is something sent by the caller (most often from the client)
|
||||
export const getFilteredTasks: GetFilteredTasks<
|
||||
Pick<Task, 'isDone'>,
|
||||
Task[]
|
||||
> = (args) => {
|
||||
const { isDone } = args
|
||||
return tasks.filter((task) => task.isDone === isDone)
|
||||
}
|
||||
```
|
||||
|
||||
Wasp automatically generates the types `GetTasks` and `GetFilteredTasks` based on your Wasp file's declarations:
|
||||
|
||||
- `GetTasks` is a generic type automatically generated by Wasp, based on the Query declaration for `getTasks`.
|
||||
- `GetFilteredTasks` is also a generic type automatically generated by Wasp, based on the Query declaration for `getFilteredTasks`.
|
||||
|
||||
You can utilize these types to define the input and output types for your Query.
|
||||
|
||||
For example, the Query `getTasks` doesn't expect any arguments (its input type is `void`), but it does return a list of tasks (its output type is `Task[]`).
|
||||
|
||||
On the other hand, the Query `getFilteredTasks` expects an object of type `{ isDone: boolean }`. This type is derived from the `Task` type.
|
||||
|
||||
While annotating the Queries is optional, it's highly recommended. Doing so enables **full-stack type safety**. We'll explore what this means when we discuss calling the Query from the client.
|
||||
|
||||
<SuperjsonNote />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
For a detailed explanation of the Query definition API (i.e., arguments and return values), check the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
### Using Queries
|
||||
|
||||
To use a Query, you can import it from `@wasp` and call it directly. As mentioned, the usage doesn't change depending on whether you're on the server or the client:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```javascript
|
||||
import getAllTasks from '@wasp/queries/getAllTasks.js'
|
||||
import getFilteredTasks from '@wasp/queries/getFilteredTasks.js'
|
||||
|
||||
// ...
|
||||
|
||||
const allTasks = await getAllTasks()
|
||||
const doneTasks = await getFilteredTasks({ isDone: true })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```typescript
|
||||
import getAllTasks from '@wasp/queries/getAllTasks.js'
|
||||
import getFilteredTasks from '@wasp/queries/getFilteredTasks.js'
|
||||
|
||||
// TypeScript automatically infers the return values and type-checks
|
||||
// the payloads.
|
||||
const allTasks = await getAllTasks()
|
||||
const doneTasks = await getFilteredTasks({ isDone: true })
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### The `useQuery` hook
|
||||
|
||||
When using Queries on the client, you can make them reactive with the `useQuery` hook.
|
||||
This hook comes bundled with Wasp and is a thin wrapper around the `useQuery` hook from [_react-query_](https://github.com/tannerlinsley/react-query). The only difference is that you don't need to supply the key - Wasp handles this for you automatically.
|
||||
|
||||
Here's an example of calling the Queries using the `useQuery` hook:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title=src/client/MainPage.jsx
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getAllTasks from '@wasp/queries/getAllTasks'
|
||||
import getFilteredTasks from '@wasp/queries/getFilteredTasks'
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: allTasks, error: error1 } = useQuery(getAllTasks)
|
||||
const { data: doneTasks, error: error2 } = useQuery(getFilteredTasks, {
|
||||
isDone: true,
|
||||
})
|
||||
|
||||
if (error1 !== null || error2 !== null) {
|
||||
return <div>There was an error</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>All Tasks</h2>
|
||||
{allTasks && allTasks.length > 0
|
||||
? allTasks.map((task) => <Task key={task.id} {...task} />)
|
||||
: 'No tasks'}
|
||||
|
||||
<h2>Finished Tasks</h2>
|
||||
{doneTasks && doneTasks.length > 0
|
||||
? doneTasks.map((task) => <Task key={task.id} {...task} />)
|
||||
: 'No finished tasks'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ description, isDone }: Task) => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? 'Yes' : 'No'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title=src/client/MainPage.tsx
|
||||
import React from 'react'
|
||||
import { Task } from '@wasp/entities'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import getAllTasks from '@wasp/queries/getAllTasks'
|
||||
import getFilteredTasks from '@wasp/queries/getFilteredTasks'
|
||||
|
||||
const MainPage = () => {
|
||||
// TypeScript will automatically infer and type-check payload types.
|
||||
const { data: allTasks, error: error1 } = useQuery(getAllTasks)
|
||||
const { data: doneTasks, error: error2 } = useQuery(getFilteredTasks, {
|
||||
isDone: true,
|
||||
})
|
||||
|
||||
if (error1 !== null || error2 !== null) {
|
||||
return <div>There was an error</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>All Tasks</h2>
|
||||
{allTasks && allTasks.length > 0
|
||||
? allTasks.map((task) => <Task key={task.id} {...task} />)
|
||||
: 'No tasks'}
|
||||
|
||||
<h2>Finished Tasks</h2>
|
||||
{doneTasks && doneTasks.length > 0
|
||||
? doneTasks.map((task) => <Task key={task.id} {...task} />)
|
||||
: 'No finished tasks'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ description, isDone }: Task) => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description: </strong>
|
||||
{description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Is done: </strong>
|
||||
{isDone ? 'Yes' : 'No'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
Notice how you don't need to annotate the Query's return value type. Wasp automatically infers the from the Query's backend implementation. This is **full-stack type safety**: the types on the client always match the types on the server.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<small>
|
||||
|
||||
For a detailed specification of the `useQuery` hook, check the [API Reference](#api-reference).
|
||||
|
||||
</small>
|
||||
|
||||
### Error Handling
|
||||
|
||||
For security reasons, all exceptions thrown in the Query's NodeJS implementation are sent to the client as responses with the HTTP status code `500`, with all other details removed.
|
||||
Hiding error details by default helps against accidentally leaking possibly sensitive information over the network.
|
||||
|
||||
If you do want to pass additional error information to the client, you can construct and throw an appropriate `HttpError` in your implementation:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title=src/server/queries.js
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getAllTasks = async (args, context) => {
|
||||
throw new HttpError(
|
||||
403, // status code
|
||||
"You can't do this!", // message
|
||||
{ foo: 'bar' } // data
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/server/queries.ts
|
||||
import { GetAllTasks } from '@wasp/queries/types'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getAllTasks: GetAllTasks = async (args, context) => {
|
||||
throw new HttpError(
|
||||
403, // status code
|
||||
"You can't do this!", // message
|
||||
{ foo: 'bar' } // data
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
If the status code is `4xx`, the client will receive a response object with the corresponding `message` and `data` fields, and it will rethrow the error (including these fields).
|
||||
To prevent information leakage, the server won't forward these fields for any other HTTP status codes.
|
||||
|
||||
### Using Entities in Queries
|
||||
|
||||
In most cases, resources used in Queries will be [Entities](/docs/data-model/entities.md).
|
||||
To use an Entity in your Query, add it to the `query` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp {4,9} title="main.wasp"
|
||||
|
||||
query getAllTasks {
|
||||
fn: import { getAllTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
query getFilteredTasks {
|
||||
fn: import { getFilteredTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp {4,9} title="main.wasp"
|
||||
|
||||
query getAllTasks {
|
||||
fn: import { getAllTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
query getFilteredTasks {
|
||||
fn: import { getFilteredTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Wasp will inject the specified Entity into the Query's `context` argument, giving you access to the Entity's Prisma API:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/queries.js"
|
||||
export const getAllTasks = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
|
||||
export const getFilteredTasks = async (args, context) => {
|
||||
return context.entities.Task.findMany({
|
||||
where: { isDone: args.isDone },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/queries.ts"
|
||||
import { Task } from '@wasp/entities'
|
||||
import { GetAllTasks, GetFilteredTasks } from '@wasp/queries/types'
|
||||
|
||||
export const getAllTasks: GetAllTasks<void, Task[]> = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
|
||||
export const getFilteredTasks: GetFilteredTasks<
|
||||
Pick<Task, 'isDone'>,
|
||||
Task[]
|
||||
> = async (args, context) => {
|
||||
return context.entities.Task.findMany({
|
||||
where: { isDone: args.isDone },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Again, annotating the Queries is optional, but greatly improves **full-stack type safety**.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
||||
|
||||
## API Reference
|
||||
|
||||
### Declaring Queries
|
||||
|
||||
The `query` declaration supports the following fields:
|
||||
|
||||
- `fn: ServerImport` <Required />
|
||||
|
||||
The import statement of the Query's NodeJs implementation.
|
||||
|
||||
- `entities: [Entity]`
|
||||
|
||||
A list of entities you wish to use inside your Query.
|
||||
For instructions on using Entities in Queries, take a look at [the guide](#using-entities-in-queries).
|
||||
|
||||
#### Example
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
Declaring the Query:
|
||||
|
||||
```wasp
|
||||
query getFoo {
|
||||
fn: import { getFoo } from "@server/queries.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Enables you to import and use it anywhere in your code (on the server or the client):
|
||||
|
||||
```js
|
||||
import getFoo from '@wasp/queries'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
Declaring the Query:
|
||||
|
||||
```wasp
|
||||
query getFoo {
|
||||
fn: import { getFoo } from "@server/queries.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Enables you to import and use it anywhere in your code (on the server or the client):
|
||||
|
||||
```ts
|
||||
import getFoo from '@wasp/queries'
|
||||
```
|
||||
|
||||
And also creates a type you can import on the server:
|
||||
|
||||
```ts
|
||||
import type { GetFoo } from '@wasp/queries/types'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Implementing Queries
|
||||
|
||||
The Query's implementation is a NodeJS function that takes two arguments (it can be an `async` function if you need to use the `await` keyword).
|
||||
Since both arguments are positional, you can name the parameters however you want, but we'll stick with `args` and `context`:
|
||||
|
||||
1. `args` (type depends on the Query)
|
||||
|
||||
An object containing the data **passed in when calling the query** (e.g., filtering conditions).
|
||||
Check [the usage examples](#using-queries) to see how to pass this object to the Query.
|
||||
|
||||
2. `context` (type depends on the Query)
|
||||
|
||||
An additional context object **passed into the Query by Wasp**. This object contains user session information, as well as information about entities. Check the [section about using entities in Queries](#using-entities-in-queries) to see how to use the entities field on the `context` object, or the [auth section](/docs/auth/overview#using-the-contextuser-object) to see how to use the `user` object.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
Afer you [declare the query](#declaring-queries), Wasp generates a generic type you can use when defining its implementation.
|
||||
For the Query declared as `getSomething`, the generated type is called `GetSomething`:
|
||||
|
||||
```ts
|
||||
import { GetSomething } from '@wasp/queries/types'
|
||||
```
|
||||
|
||||
It expects two (optional) type arguments:
|
||||
|
||||
1. `Input`
|
||||
|
||||
The type of the `args` object (i.e., the Query's input payload). The default value is `never`.
|
||||
|
||||
2. `Output`
|
||||
|
||||
The type of the Query's return value (i.e., the Query's output payload). The default value is `unknown`.
|
||||
|
||||
The defaults were chosen to make the type signature as permissive as possible. If don't want your Query to take/return anything, use `void` as a type argument.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
#### Example
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
The following Query:
|
||||
|
||||
```wasp
|
||||
query getFoo {
|
||||
fn: import { getFoo } from "@server/queries.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Expects to find a named export `getFoo` from the file `src/server/queries.js`
|
||||
|
||||
```js title=queries.js
|
||||
export const getFoo = (args, context) => {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
The following Query:
|
||||
|
||||
```wasp
|
||||
query getFoo {
|
||||
fn: import { getFoo } from "@server/queries.js"
|
||||
entities: [Foo]
|
||||
}
|
||||
```
|
||||
|
||||
Expects to find a named export `getFoo` from the file `src/server/queries.js`
|
||||
|
||||
You can use the generated type `GetFoo` and specify the Query's inputs and outputs using its type arguments.
|
||||
|
||||
```ts title=queries.ts
|
||||
import { GetFoo } from "@wasp/queries/types";
|
||||
|
||||
type Foo = // ...
|
||||
|
||||
export const getFoo: GetFoo<{ id: number }, Foo> = (args, context) => {
|
||||
// implementation
|
||||
};
|
||||
```
|
||||
|
||||
In this case, the Query expects to receive an object with an `id` field of type `number` (this is the type of `args`), and return a value of type `Foo` (this must match the type of the Query's return value).
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### The `useQuery` Hook
|
||||
|
||||
Wasp's `useQuery` hook is a thin wrapper around the `useQuery` hook from [_react-query_](https://github.com/tannerlinsley/react-query).
|
||||
One key difference is that Wasp doesn't expect you to supply the cache key - it takes care of it under the hood.
|
||||
|
||||
Wasp's `useQuery` hook accepts three arguments:
|
||||
|
||||
- `queryFn` <Required />
|
||||
|
||||
The client-side query function generated by Wasp based on a `query` declaration in your `.wasp` file.
|
||||
|
||||
- `queryFnArgs`
|
||||
|
||||
The arguments object (payload) you wish to pass into the Query. The Query's NodeJS implementation will receive this object as its first positional argument.
|
||||
|
||||
- `options`
|
||||
|
||||
A _react-query_ `options` object. Use this to change
|
||||
[the default
|
||||
behavior](https://react-query.tanstack.com/guides/important-defaults) for
|
||||
this particular Query. If you want to change the global defaults, you can do
|
||||
so in the [client setup function](/docs/project/client-config.md#overriding-default-behaviour-for-queries).
|
||||
|
||||
For an example of usage, check [this section](#the-usequery-hook).
|
@ -1,633 +0,0 @@
|
||||
---
|
||||
title: Deploying
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import AddExternalAuthEnvVarsReminder from './_addExternalAuthEnvVarsReminder.md'
|
||||
|
||||
:::info
|
||||
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly a bit bigger changes in the future.
|
||||
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
||||
:::
|
||||
|
||||
## Deploying with the Wasp CLI
|
||||
|
||||
Using the Wasp CLI, you can easily deploy a new app to [Fly.io](https://fly.io) with just a single command:
|
||||
```shell
|
||||
wasp deploy fly launch my-wasp-app mia
|
||||
```
|
||||
|
||||
Under the covers, this runs the equivalent of the following commands:
|
||||
```shell
|
||||
wasp deploy fly setup my-wasp-app mia
|
||||
wasp deploy fly create-db mia
|
||||
wasp deploy fly deploy
|
||||
```
|
||||
|
||||
In the above commands, we used an app basename of `my-wasp-app` and deployed it to the Miami, Florida (US) region (called `mia`). The basename is used to create all three app tiers, so you will have three components in your Fly dashboard:
|
||||
|
||||
- `my-wasp-app-client`
|
||||
- `my-wasp-app-server`
|
||||
- `my-wasp-app-db`
|
||||
|
||||
:::tip Unique Name
|
||||
Your app name must be unique across all of Fly or deployment will fail. Additionally, please do not CTRL-C or exit your terminal as these commands run.
|
||||
:::
|
||||
|
||||
:::caution A Note on Free Tiers 💳
|
||||
Fly has [free allowances](https://fly.io/docs/about/pricing/#plans) for up to 3 VMs (so deploying a Wasp app to a fresh account is free), but all plans require you to add your credit card info before proceeding. If you don't, the deploy will fail!
|
||||
|
||||
To do so, go to your [account's billing page](https://fly.io/dashboard/personal/billing).
|
||||
:::
|
||||
|
||||
The list of available Fly regions can be found [here](https://fly.io/docs/reference/regions/). You can also run `wasp deploy fly cmd platform regions --context server`.
|
||||
|
||||
### Using a custom domain for your app
|
||||
|
||||
Setting up a custom domain for your app is easy. First, you need to add your domain to Fly. You can do this by running:
|
||||
```shell
|
||||
wasp deploy fly cmd --context client certs create mycoolapp.com
|
||||
```
|
||||
|
||||
:::info Use Your Own Domain
|
||||
Make sure to replace `mycoolapp.com` with your domain in all of the commands mentioned in this section 🙃
|
||||
:::
|
||||
|
||||
This command will output the instructions to add the DNS records to your domain. It will look something like this:
|
||||
```shell-session
|
||||
You can direct traffic to mycoolapp.com by:
|
||||
|
||||
1: Adding an A record to your DNS service which reads
|
||||
|
||||
A @ 66.241.1XX.154
|
||||
|
||||
You can validate your ownership of mycoolapp.com by:
|
||||
|
||||
2: Adding an AAAA record to your DNS service which reads:
|
||||
|
||||
AAAA @ 2a09:82XX:1::1:ff40
|
||||
```
|
||||
|
||||
Next, you need to add the DNS records for your domain. This will depend on your domain provider, but it should be a matter of adding an A record for `@` and an AAAA record for `@` with the values provided by the previous command.
|
||||
|
||||
Finally, you need to set your domain as the `WASP_WEB_CLIENT_URL` environment variable for your server app. We need to do this to keep our CORS configuration up to date.
|
||||
|
||||
You can do this by running:
|
||||
```shell
|
||||
wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com
|
||||
```
|
||||
|
||||
That's it, your app should be available at `https://mycoolapp.com`! 🎉
|
||||
|
||||
### Deployment commands
|
||||
|
||||
#### Apps and the database
|
||||
|
||||
`setup` will create your client and server apps on Fly, and add some secrets, but does _not_ deploy them. We need a database first, which we create with `create-db`, and it is automatically linked to your server.
|
||||
|
||||
:::note
|
||||
We only run the `setup` and `create-db` steps once.
|
||||
|
||||
You may notice after running `setup` you have a `fly-server.toml` and `fly-client.toml` in your Wasp project directory. Those are meant to be version controlled. If you want to maintain multiple apps, you can add the `--fly-toml-dir <abs-path>` option to point to different directories, like "dev" or "staging".
|
||||
:::
|
||||
|
||||
Finally, we `deploy` which will push your client and server live. We run this single command each time you want to update your app.
|
||||
|
||||
:::note
|
||||
Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducability, the CLI defaults to the use of a remote Fly.io builder. If you wish to build locally, you may supply the `--build-locally` option to `wasp deploy fly launch` or `wasp deploy fly deploy`.
|
||||
:::
|
||||
|
||||
#### Setting secrets
|
||||
|
||||
If you would like to run arbitrary Fly commands (eg, `flyctl secrets list` for your server app), you can run them like so:
|
||||
```shell
|
||||
wasp deploy fly cmd secrets list --context server
|
||||
```
|
||||
|
||||
:::note
|
||||
If you are deploying an app that requires any other environment variables (like social auth secrets), you will want to set your environment variables up like so:
|
||||
|
||||
During `launch`:
|
||||
```
|
||||
wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>
|
||||
```
|
||||
|
||||
After `launch`/`setup`:
|
||||
```
|
||||
wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server
|
||||
```
|
||||
:::
|
||||
|
||||
#### Multiple orgs
|
||||
|
||||
If you have multiple orgs, you can specify a `--org` option. For example: `wasp deploy fly launch my-wasp-app mia --org hive`
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
In addition to the CLI, you can deploy a Wasp project by generating the code and then deploying generated code "manually", as explained below.
|
||||
|
||||
In the future, the plan is to have Wasp take care of it completely: you would declaratively define your deployment in .wasp and then just call `wasp deploy` ([github issue](https://github.com/wasp-lang/wasp/issues/169)).
|
||||
|
||||
If you want to deploy your App completely **free** of charge, continue reading below for guides on using Fly.io as your backend (server) provider and Netlify for your frontend (client).
|
||||
|
||||
If you prefer to host client and server on **one platform**, and don't mind paying a very small fee for extra features, we suggest following the guide on using [Railway as your provider](#deploying-to-railway-freemium-all-in-one-solution).
|
||||
|
||||
### Generating deployable code
|
||||
The following command generates deployable code for the whole app in the `.wasp/build/` directory:
|
||||
```
|
||||
wasp build
|
||||
```
|
||||
|
||||
:::note Using SQLite in production
|
||||
You will not be able to build the app if you are using SQLite as a database (which is a default database) -> you will have to [switch to PostgreSQL](/docs/language/features#migrating-from-sqlite-to-postgresql).
|
||||
:::
|
||||
|
||||
## Deploying API server (backend)
|
||||
|
||||
:::note
|
||||
Since Wasp's API server is a Node.js app, it can be deployed anywhere where Node.js apps can be deployed. Below, we provide instructions for deploying to a few hosting providers.
|
||||
:::
|
||||
|
||||
|
||||
In `.wasp/build/`, there is a `Dockerfile` describing an image for building the server.
|
||||
|
||||
To run server in production, deploy this docker image to your favorite hosting provider, ensure that env vars are correctly set, and that is it.
|
||||
|
||||
Below we will explain the required env vars and also provide detailed instructions for deploying to Fly.io or Heroku.
|
||||
|
||||
### Env vars
|
||||
|
||||
Server uses following environment variables, so you need to ensure they are set on your hosting provider:
|
||||
|
||||
- `PORT` -> The port number at which it will listen for requests (e.g. `3001`).
|
||||
- `DATABASE_URL` -> The URL of the Postgres database it should use (e.g. `postgresql://mydbuser:mypass@localhost:5432/nameofmydb`).
|
||||
- `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://<app-name>.netlify.app`), which is necessary for CORS.
|
||||
- `JWT_SECRET` -> You need this if you are using Wasp's `auth` feature. Set it to a random string (password), at least 32 characters long.
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
### Deploying to Fly.io (free, recommended)
|
||||
|
||||
Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/).
|
||||
|
||||
:::note
|
||||
Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducability, we will default to the use of a remote Fly.io builder.
|
||||
|
||||
Additionally, `fly` is a symlink for `flyctl` on most systems and they can be used interchangeably.
|
||||
:::
|
||||
|
||||
Make sure you are logged in with `flyctl` CLI. You can check if you are logged in with `flyctl auth whoami`, and if you are not, you can log in with `flyctl auth login`.
|
||||
|
||||
#### Set up a Fly.io app (only once per Wasp app)
|
||||
|
||||
Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app. Position yourself in .wasp/build/ directory (reminder: which you created by running `wasp build` previously):
|
||||
|
||||
```bash
|
||||
cd .wasp/build
|
||||
```
|
||||
|
||||
Now from within the `build` directory, run the launch command to set up a new app and create a `fly.toml` file:
|
||||
|
||||
```bash
|
||||
flyctl launch --remote-only
|
||||
```
|
||||
|
||||
This will ask a series of questions, including what region to deploy in and if you would like a database.
|
||||
|
||||
- Say **yes to "Would you like to set up a Postgresql database now?", and select Development**, and Fly.io will set a `DATABASE_URL` for you.
|
||||
- Say **no to "Would you like to deploy now?"**, as well as any additional questions. We still need to set a few environment variables.
|
||||
|
||||
:::note
|
||||
If your attempts to initiate a new app fail for whatever reason, then you can run `flyctl apps destroy <app-name>` before trying again.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
What does it look like when your DB is deployed correctly?
|
||||
</summary>
|
||||
<div>
|
||||
<p>When your DB is deployed correctly, you will be able to view it in the <a href="https://fly.io/dashboard">Fly.io dashboard</a>:</p>
|
||||
<img width="662" alt="image" src="https://user-images.githubusercontent.com/70215737/201068630-d100db2c-ade5-4874-a29f-6e1890dba2fc.png" />
|
||||
</div>
|
||||
</details>
|
||||
:::
|
||||
|
||||
Next, let's copy the `fly.toml` file up to our Wasp project dir for safekeeping.
|
||||
```bash
|
||||
cp fly.toml ../../
|
||||
```
|
||||
|
||||
Next, let's add a few more environment variables:
|
||||
|
||||
```bash
|
||||
flyctl secrets set PORT=8080
|
||||
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
||||
|
||||
# If you are using an external auth method (Google or GitHub), make sure to add their vars too!
|
||||
# flyctl secrets set GOOGLE_CLIENT_ID=<google_client_id>
|
||||
# flyctl secrets set GOOGLE_CLIENT_SECRET=<google_client_secret>
|
||||
```
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
NOTE: If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
||||
|
||||
If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.
|
||||
|
||||
#### Deploy to a Fly.io app
|
||||
|
||||
While still in the .wasp/build/ directory, run:
|
||||
|
||||
```bash
|
||||
flyctl deploy --remote-only --config ../../fly.toml
|
||||
```
|
||||
|
||||
This will build and deploy the backend of your Wasp app on Fly.io to `https://<app-name>.fly.dev`! 🤘🎸
|
||||
|
||||
Now, if you haven't, you can deploy your frontend -- [we suggest using Netlify](#deploying-web-client-frontend) for this -- and add the client url by running `flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_frontend>`
|
||||
|
||||
Additionally, some useful commands include:
|
||||
|
||||
```bash
|
||||
flyctl logs
|
||||
flyctl secrets list
|
||||
flyctl ssh console
|
||||
```
|
||||
|
||||
#### Redeploying after Wasp builds
|
||||
|
||||
When you rebuild your Wasp app (with `wasp build`), it will remove your .wasp/build/ directory. In there, you may have a `fly.toml` from any prior Fly.io deployments. While we will improve this process in the future, in the meantime, you have a few options:
|
||||
|
||||
1. Copy the `fly.toml` file to a versioned directory, like your Wasp project dir. From there, you can reference it in `flyctl deploy --config <path>` commands, like above.
|
||||
1. Backup the `fly.toml` file somewhere before running `wasp build`, and copy it into .wasp/build/ after. When the `fly.toml` file exists in .wasp/build/ dir, you do not need to specify the `--config <path>`.
|
||||
1. Run `flyctl config save -a <app-name>` to regenerate the `fly.toml` file from the remote state stored in Fly.io.
|
||||
|
||||
### Deploying to Heroku (non-free)
|
||||
|
||||
:::note
|
||||
Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter
|
||||
|
||||
As such, we recommend using an alternative provider like [Fly.io](#deploying-to-flyio-free-recommended) for your first apps.
|
||||
:::
|
||||
|
||||
You will need Heroku account, `heroku` CLI and `docker` CLI installed to follow these instructions.
|
||||
|
||||
Make sure you are logged in with `heroku` CLI. You can check if you are logged in with `heroku whoami`, and if you are not, you can log in with `heroku login`.
|
||||
|
||||
#### Set up a Heroku app (only once per Wasp app)
|
||||
|
||||
Unless you already have a heroku app that you want to deploy to, let's create a new Heroku app:
|
||||
|
||||
```
|
||||
heroku create <app-name>
|
||||
```
|
||||
|
||||
Unless you have external Postgres database that you want to use, let's create new database on Heroku and attach it to our app:
|
||||
|
||||
```
|
||||
heroku addons:create --app <app-name> heroku-postgresql:mini
|
||||
```
|
||||
:::caution
|
||||
|
||||
Heroku does not offer a free plan anymore and `mini` is their cheapest database instance - it costs $5/mo.
|
||||
|
||||
:::
|
||||
|
||||
Heroku will also set `DATABASE_URL` env var for us at this point. If you are using external database, you will have to set it yourself.
|
||||
|
||||
The `PORT` env var will also be provided by Heroku, so the only two left to set are the `JWT_SECRET` and `WASP_WEB_CLIENT_URL` env vars:
|
||||
|
||||
```
|
||||
heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||
heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
|
||||
```
|
||||
|
||||
NOTE: If you do not know what your frontend URL is yet, don't worry. You can set `WASP_WEB_CLIENT_URL` after you deploy your frontend.
|
||||
|
||||
#### Deploy to a Heroku app
|
||||
|
||||
Position yourself in `.wasp/build/` directory (reminder: which you created by running `wasp build` previously):
|
||||
|
||||
```
|
||||
cd .wasp/build
|
||||
```
|
||||
|
||||
assuming you were at the root of your Wasp project at that moment.
|
||||
|
||||
Log in to Heroku Container Registry:
|
||||
|
||||
```
|
||||
heroku container:login
|
||||
```
|
||||
|
||||
Build the docker image and push it to Heroku:
|
||||
|
||||
```
|
||||
heroku container:push --app <app-name> web
|
||||
```
|
||||
|
||||
App is still not deployed at this point.
|
||||
This step might take some time, especially the very first time, since there are no cached docker layers.
|
||||
|
||||
:::note
|
||||
|
||||
#### Note for Apple M1 users
|
||||
|
||||
Apple M1 users need to build a non-Arm image, so the above step will not work at this time. Instead of `heroku container:push`, users instead should:
|
||||
|
||||
```bash
|
||||
docker buildx build --platform linux/amd64 -t <app-name> .
|
||||
docker tag <app-name> registry.heroku.com/<app-name>/web
|
||||
docker push registry.heroku.com/<app-name>/web
|
||||
```
|
||||
|
||||
You are now ready to proceed to the next step.
|
||||
:::
|
||||
|
||||
Deploy the pushed image and restart the app:
|
||||
|
||||
```
|
||||
heroku container:release --app <app-name> web
|
||||
```
|
||||
|
||||
This is it, backend is deployed at `https://<app-name>.herokuapp.com`!
|
||||
|
||||
Additionally, you can check out the logs with:
|
||||
|
||||
```
|
||||
heroku logs --tail --app <app-name>
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
#### Note on using pg-boss with Heroku
|
||||
|
||||
If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called `PG_BOSS_NEW_OPTIONS` to `{"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}`. This is because pg-boss uses the `pg` extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
|
||||
|
||||
- https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||
:::
|
||||
|
||||
## Deploying web client (frontend)
|
||||
|
||||
Position yourself in `.wasp/build/web-app` directory (reminder: which you created by running `wasp build` previously):
|
||||
|
||||
```
|
||||
cd .wasp/build/web-app
|
||||
```
|
||||
|
||||
assuming you were at the root of your Wasp project at that moment.
|
||||
|
||||
Run
|
||||
|
||||
```
|
||||
npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
|
||||
```
|
||||
:::info NO SLASH
|
||||
Make sure your API URL does <strong>not</strong> have a trailing "/" on the end of it:<br/>
|
||||
✅ https://backend.example.com <br/>❌ https://backend.example.com/
|
||||
:::
|
||||
|
||||
where <url_to_wasp_backend> is url of the wasp backend that you previously deployed, e.g. `https://wasp-test.fly.dev`.
|
||||
|
||||
This will create `build/` directory, which you can deploy to any static hosting provider.
|
||||
Check instructions below for deploying to Netlify.
|
||||
|
||||
### Deploying to Netlify
|
||||
|
||||
Netlify is a static hosting solution that is free for many use cases.
|
||||
You will need Netlify account and `netlify` CLI installed to follow these instructions.
|
||||
|
||||
Make sure you are logged in with `netlify` CLI. You can check if you are logged in with `netlify status`, and if you are not, you can log in with `netlify login`.
|
||||
|
||||
While positioned in `.wasp/build/web-app/` directory, and after you have created `.wasp/build/web-app/build/` directory as per instructions above, run
|
||||
|
||||
```
|
||||
netlify deploy
|
||||
```
|
||||
|
||||
and carefully follow their instructions (i.e. do you want to create a new app or use existing one, team under which your app will reside, ..., final step to run `netlify deploy --prod`).
|
||||
|
||||
That is it!
|
||||
|
||||
NOTE: Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in your server hosting environment (e.g., Fly.io or Heroku).
|
||||
|
||||
## Deploying to Railway ("freemium", all-in-one solution)
|
||||
|
||||
Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app -- database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their [plans page](https://docs.railway.app/reference/plans) for more info.
|
||||
|
||||
:::caution ✋
|
||||
Due to Railway's current proxy configuration, client-side routing will not work correctly without some additional configuration, which is described in the toggle below.
|
||||
|
||||
If you prefer NOT to configure your client for Railway, another option is to deploy only your back-end there, while deploying your client to a seperate service such as [Netlify](#deploying-to-netlify)
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<em>Additional Configuration for Client-Side Routing</em>
|
||||
</summary>
|
||||
<div>
|
||||
|
||||
1. Ensure your Wasp project is built by running `wasp build` in the project dir.
|
||||
2. Go to `/.wasp/build/web-app` and create 2 files:
|
||||
|
||||
- Dockerfile: simply create a file named `Dockerfile` with the contents below (note, it's important to use `FROM nginx:1.19.10-alpine`, as using anything other than this version may throw a lot of exceptions on the server):
|
||||
```dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# Examples of any ENV variables that build requires for react app to have
|
||||
ARG PORT
|
||||
ARG WASP_WEB_CLIENT_URL
|
||||
ARG REACT_APP_API_URL
|
||||
ARG API_URL
|
||||
|
||||
ENV REACT_APP_PORT=$PORT
|
||||
ENV REACT_APP_WASP_WEB_CLIENT_URL=$WASP_WEB_CLIENT_URL
|
||||
ENV REACT_APP_API_URL=$REACT_APP_API_URL
|
||||
|
||||
# Add a work directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.19.10-alpine
|
||||
|
||||
# Set working directory to nginx asset directory
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Remove default nginx static assets
|
||||
RUN rm -rf ./*
|
||||
|
||||
COPY --from=builder /app/build .
|
||||
|
||||
COPY .nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
- Create a file calle `.dockerignore` with the following contents:
|
||||
```
|
||||
node_modules
|
||||
```
|
||||
3. Create a new directory `/.nginx` inside the `web-app` dir.
|
||||
4. Create a file `nginx.conf` inside the `/.nginx` dir with the following contents:
|
||||
```
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
5. Make sure that the `client` service in Railway, which you will set up below, has env variable `PORT` set to `8080` (see the [Add Enviornment Variables](/docs/deploying#add-environment-variables) section below for more info).
|
||||
6. Optionally, you may need to disable `tsc` in the `/.wasp/build/web-app/package.json` file if Docker fails on deploy due to typing issues:
|
||||
```
|
||||
"scripts": {
|
||||
"start": "npm run validate-env && vite",
|
||||
"build": "npm run validate-env && vite build",
|
||||
"validate-env": "node -r dotenv/config ./scripts/validate-env.mjs"
|
||||
},
|
||||
```
|
||||
7. Then continue to follow the instructions below to deploy your app to Railway.
|
||||
|
||||
</div>
|
||||
</details>
|
||||
:::
|
||||
|
||||
To get started, follow these steps:
|
||||
|
||||
1. [Generate deployable code](#generating-deployable-code) (`wasp build`)
|
||||
2. Sign up at [Railway.app](https://railway.app) (Tip! Sign up with your GitHub account to be elligble for the free tier)
|
||||
3. Before creating a new project, install the [Railway CLI](#https://docs.railway.app/develop/cli#install) by running the following command in your terminal:
|
||||
```shell
|
||||
curl -fsSL https://railway.app/install.sh | sh
|
||||
```
|
||||
4. While still in the terminal, run `railway login` and a browser tab will open to authenticate you.
|
||||
|
||||
#### Create New Project
|
||||
|
||||
Go back to your [Railway dashboard](https://railway.app/dashboard), click on **+ New Project**, and select `Provision PostgreSQL` from the dropdown menu.
|
||||
|
||||
Once it initializes, right click on the `+ New` button in the top right corner and select `>_ Empty Service`. Once it initializes, click on it, go to `Settings > General` and change the name (e.g. `server`).
|
||||
|
||||
Go ahead and create another empty service and name it (e.g. `client`).
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<em>Just in case, here is a helpful screenshot ;)</em>
|
||||
</summary>
|
||||
<div>
|
||||
<img alt="Create an Empty Service"
|
||||
src={useBaseUrl('img/deploying/railway-rename.png')} />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
#### Deploy to services
|
||||
|
||||
Now go back to your terminal and execute the following commands:
|
||||
|
||||
1. Move into your app's `.wasp/build/` directory, which was created when you ran `wasp build` previously:
|
||||
```shell
|
||||
cd .wasp/build
|
||||
```
|
||||
2. "Link" your app build to your newly created Railway project:
|
||||
```shell
|
||||
railway link
|
||||
```
|
||||
3. Push and deploy the project to railway (make sure you're in `.wasp/build`):
|
||||
```shell
|
||||
railway up
|
||||
```
|
||||
Select `server` when prompted with `Select Service`. Press enter.
|
||||
Railway will now locate the Dockerfile and deploy your server 👍
|
||||
|
||||
When deployment is finished, you might see: `Deployment live at <url_to_wasp_backend>`. If not, go now to your [Railway dashboard](https://railway.app/dashboard) and in the server instance's `Settings` tab, click `Generate Domain`. Copy the new URL as we will need it for step 5! 📜
|
||||
|
||||
4. Next, change into your app's frontend build directory `.wasp/build/web-app`:
|
||||
```shell
|
||||
cd web-app
|
||||
```
|
||||
5. Create the production build, adding the URL from step 3:
|
||||
```shell
|
||||
npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
|
||||
```
|
||||
:::info NO SLASH
|
||||
Make sure your API URL does <strong>not</strong> have a trailing "/" on the end of it:<br/>
|
||||
✅ https://backend.example.com <br/>❌ https://backend.example.com/
|
||||
:::
|
||||
|
||||
6. Change into the `.wasp/build/web-app/build` directory:
|
||||
```shell
|
||||
cd build
|
||||
```
|
||||
7. Next, we want to link this specific frontend directory to our project as well:
|
||||
```shell
|
||||
railway link
|
||||
```
|
||||
8. Deploy the client and select `client` when prompted with `Select Service`:
|
||||
```shell
|
||||
railway up
|
||||
```
|
||||
9. Your apps are deployed 🧙♂️. Now it's time to add environment variables, so open the project in the browser
|
||||
```shell
|
||||
railway open
|
||||
```
|
||||
|
||||
#### Add Environment Variables
|
||||
|
||||
Back in your [Railway dashboard](https://railway.app/dashboard), click on your project and you should see your newly deployed services: Postgres, Server, and Client.
|
||||
|
||||
Now you're going to pass each service the correct [environment variables](#env-vars). To do this, you first need to tell Railway to generate public domains for client and server.
|
||||
|
||||
Go to the server instance's `Settings` tab, and click `Generate Domain`. Do the same under the client's `Settings`.
|
||||
|
||||
The Postgres database is already initialized with a domain, so click on the Postgres instance, go to the **Connect** tab and copy the `Postgres Connection URL`.
|
||||
|
||||
Go back to your `server` instance and navigate to its `Variables` tab. Now add the copied Postgres URL as `DATABASE_URL`, as well as the client's domain as `WASP_WEB_CLIENT_URL`.
|
||||
|
||||
<AddExternalAuthEnvVarsReminder />
|
||||
|
||||
Next, copy the server's domain, move over to the client's `Variables` tab and add the generated server domain as a new variable called `REACT_APP_API_URL`.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<em>Having trouble finding these settings?</em>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<figure>
|
||||
<img src={useBaseUrl('img/deploying/railway-postgres-url.png')}/>
|
||||
<figcaption class="image-caption">Postgres Connection URL</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src={useBaseUrl('img/deploying/railway-server-var.png')}/>
|
||||
<figcaption class="image-caption">Env Variables</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
And now you should be deployed! 🐝 🚂 🚀
|
||||
|
||||
#### Updates & Redeploying
|
||||
When you make updates and need to redeploy, just follow [steps 3-7](#deploy-to-services) above. Remember, you can connect or disconnect your app to any project in your Railway account by using `railway link` or `railway unlink` from within the app's directory.
|
||||
|
||||
## Customizing the Dockerfile
|
||||
By default, Wasp will generate a multi-stage Dockerfile that is capable of building an image with your Wasp-generated server code and running it, along with any pending migrations, as in the deployment scenario above. If you need to customize this Dockerfile, you may do so by adding a Dockerfile to your project root directory. If present, Wasp will append the contents of this file to the _bottom_ of our default Dockerfile.
|
||||
|
||||
Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. You could also choose not to use any of our build stages and have your own custom Dockerfile used as-is. A few notes are in order:
|
||||
- if you override an intermediate build stage, no later build stages will be used unless you reproduce them below
|
||||
- the contents of the Dockerfile are dynamic, based on the features you use, and may change in future releases as well, so please verify the contents have not changed from time to time
|
||||
- be sure to supply an `ENTRYPOINT` in your final build stage or it will not have any effect
|
||||
|
||||
To see what your project's (potentially combined) Dockerfile will look like, run: `wasp dockerfile`
|
||||
|
||||
Here are the official docker docs on [multi-stage builds](https://docs.docker.com/build/building/multi-stage/). Please join our Discord if you have any questions, or if the customization hook provided here is not sufficient for your needs!
|
@ -15,7 +15,7 @@ The full list of examples can be found [here](https://github.com/wasp-lang/wasp/
|
||||
- in-browser dev environment: [GitPod](https://gitpod.io/#https://github.com/wasp-lang/gitpod-template)
|
||||
|
||||
## Waspello (Trello Clone)
|
||||
- **Features**: Auth ([Google](language/features#social-login-providers-oauth-20), [username/password](language/features#authentication--authorization)), [Optimistic Updates](language/features#the-useaction-hook), [Tailwind CSS integration](integrations/css-frameworks)
|
||||
- **Features**: Auth ([Google](language/features#social-login-providers-oauth-20), [username/password](language/features#authentication--authorization)), [Optimistic Updates](language/features#the-useaction-hook), [Tailwind CSS integration](/docs/project/css-frameworks)
|
||||
- Source code: [GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/waspello)
|
||||
- Hosted at [https://waspello-demo.netlify.app](https://waspello-demo.netlify.app/login)
|
||||
<p align='center'>
|
||||
|
@ -1,12 +1,13 @@
|
||||
---
|
||||
title: CLI Reference
|
||||
---
|
||||
This document describes the Wasp CLI commands, arguments, and options.
|
||||
This guide provides an overview of the Wasp CLI commands, arguments, and options.
|
||||
|
||||
## Overview
|
||||
|
||||
The `wasp` command can be called from command line once [installed](/docs/quick-start).
|
||||
When called without arguments, it will display its command usage and help document:
|
||||
Once [installed](/docs/quick-start), you can use the wasp command from your command line.
|
||||
|
||||
If you run the `wasp` command without any arguments, it will show you a list of available commands and their descriptions:
|
||||
|
||||
```
|
||||
USAGE
|
||||
@ -49,8 +50,10 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
|
||||
## Commands
|
||||
|
||||
### Creating a new project
|
||||
- `wasp new` runs the interactive mode for creating a new Wasp project. It will ask you for the project name, and then for the template to use. It will use the template to generate the directory with the provided project-name.
|
||||
### Creating a New Project
|
||||
- Use `wasp new` to start the interactive mode for setting up a new Wasp project.
|
||||
|
||||
This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.
|
||||
|
||||
```
|
||||
$ wasp new
|
||||
@ -69,7 +72,7 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
cd MyFirstProject
|
||||
wasp start
|
||||
```
|
||||
- `wasp new <project-name>` creates new Wasp project from the default template skipping the interactive mode.
|
||||
- To skip the interactive mode and create a new Wasp project with the default template, use `wasp new <project-name>`.
|
||||
|
||||
```
|
||||
$ wasp new MyFirstProject
|
||||
@ -81,10 +84,10 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
cd MyFirstProject
|
||||
wasp start
|
||||
```
|
||||
### In project
|
||||
- `wasp start` runs Wasp app in development mode. It opens a browser tab with your application running, and watches for any changes to .wasp or files in `src/` to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.
|
||||
- `wasp start db` starts the database for you. This can be very handy, since you don't need to spin up your own database or provide its connection URL to the Wasp app!
|
||||
- `wasp clean` deletes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. It is the Wasp equivalent to "try shutting it down and turning back on".
|
||||
### Project Commands
|
||||
- `wasp start` launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in `src/` to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.
|
||||
- `wasp start db` starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.
|
||||
- `wasp clean` removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.
|
||||
|
||||
```
|
||||
$ wasp clean
|
||||
@ -93,11 +96,15 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
Deleted .wasp/ directory.
|
||||
```
|
||||
|
||||
- `wasp build` generates full web app code, ready for deployment. Use when deploying or ejecting. Generated code goes in the .wasp/build folder.
|
||||
- `wasp build` generates the complete web app code, which is ready for [deployment](/docs/advanced/deployment/overview). Use this command when you're deploying or ejecting. The generated code is stored in the `.wasp/build` folder.
|
||||
|
||||
- `wasp deploy` makes it easy to get your app hosted on the web. Currently, Wasp offers support for [Fly.io](https://fly.io). Want another hosting provider? Let us know in Discord or make a PR by updating [this TypeScript app](https://github.com/wasp-lang/wasp/tree/main/waspc/packages/deploy)!
|
||||
- `wasp deploy` makes it easy to get your app hosted on the web.
|
||||
|
||||
Currently, Wasp offers support for [Fly.io](https://fly.io). If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating [this TypeScript app](https://github.com/wasp-lang/wasp/tree/main/waspc/packages/deploy).
|
||||
|
||||
Read more about automatic deployment [here](/docs/advanced/deployment/cli).
|
||||
|
||||
- `wasp telemetry` prints [telemetry](https://wasp-lang.dev/docs/telemetry) status.
|
||||
- `wasp telemetry` displays the status of [telemetry](https://wasp-lang.dev/docs/telemetry).
|
||||
|
||||
```
|
||||
$ wasp telemetry
|
||||
@ -108,30 +115,30 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.
|
||||
|
||||
```
|
||||
- `wasp deps` prints the dependencies that Wasp uses in your project.
|
||||
- `wasp info` prints basic information about current Wasp project.
|
||||
- `wasp deps` lists the dependencies that Wasp uses in your project.
|
||||
- `wasp info` provides basic details about the current Wasp project.
|
||||
|
||||
### Database
|
||||
Wasp has a set of commands for working with the database. They all start with `db` and mostly call prisma commands in the background.
|
||||
### Database Commands
|
||||
Wasp provides a suite of commands for managing the database. These commands all begin with `db` and primarily execute Prisma commands behind the scenes.
|
||||
|
||||
- `wasp db migrate-dev` ensures dev database corresponds to the current state of schema (entities): it generates a new migration if there are changes in the schema and it applies any pending migration to the database.
|
||||
- Supports a `--name foo` option for providing a migration name, as well as `--create-only` for creating an empty migration but not applying it.
|
||||
- `wasp db migrate-dev` synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.
|
||||
- The `--name foo` option allows you to specify a name for the migration, while the `--create-only` option lets you create an empty migration without applying it.
|
||||
|
||||
- `wasp db studio` opens the GUI for inspecting your database.
|
||||
|
||||
|
||||
### Bash Completion
|
||||
|
||||
To setup Bash completion, execute `wasp completion` and follow the instructions.
|
||||
To set up Bash completion, run the `wasp completion` command and follow the instructions.
|
||||
|
||||
|
||||
### Other
|
||||
- `wasp version` prints current version of CLI.
|
||||
### Miscellaneous Commands
|
||||
- `wasp version` displays the current version of the CLI.
|
||||
|
||||
```
|
||||
$ wasp version
|
||||
|
||||
0.2.0.1
|
||||
0.11.1
|
||||
```
|
||||
- `wasp uninstall` removes Wasp from your system.
|
||||
|
@ -1,12 +1,16 @@
|
||||
---
|
||||
title: Syntax
|
||||
title: Wasp Language (.wasp)
|
||||
---
|
||||
|
||||
Wasp is a declarative, statically typed, domain specific language (DSL).
|
||||
Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).
|
||||
|
||||
It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.
|
||||
|
||||
It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!
|
||||
|
||||
## Declarations
|
||||
|
||||
The central point of Wasp language are **declarations**, and Wasp source is at the end just a bunch of declarations, each of them describing a part of your web app.
|
||||
The central point of Wasp language are **declarations**, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.
|
||||
|
||||
```wasp
|
||||
app MyApp {
|
||||
@ -23,11 +27,13 @@ page DashboardPage {
|
||||
In the example above we described a web app via three declarations: `app MyApp { ... }`, `route RootRoute { ... }` and `page DashboardPage { ... }`.
|
||||
|
||||
Syntax for writing a declaration is `<declaration_type> <declaration_name> <declaration_body>`, where:
|
||||
|
||||
- `<declaration_type>` is one of the declaration types offered by Wasp (`app`, `route`, ...)
|
||||
- `<declaration_name>` is an identifier chosen by you to name this specific declaration
|
||||
- `<declaration_body>` is the value/definition of the declaration itself, which has to match the specific declaration body type determined by the chosen declaration type.
|
||||
- `<declaration_body>` is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.
|
||||
|
||||
So, for `app` declaration above, we have:
|
||||
|
||||
- declaration type `app`
|
||||
- declaration name `MyApp` (we could have used any other identifier, like `foobar`, `foo_bar`, or `hi3Ho`)
|
||||
- declaration body `{ title: "My app" }`, which is a dictionary with field `title` that has string value.
|
||||
@ -38,10 +44,11 @@ Each declaration has a meaning behind it that describes how your web app should
|
||||
|
||||
All the other types in Wasp language (primitive types (`string`, `number`), composite types (`dict`, `list`), enum types (`DbSystem`), ...) are used to define the declaration bodies.
|
||||
|
||||
## Complete list of Wasp types
|
||||
## Complete List of Wasp Types
|
||||
|
||||
Wasp's type system can be divided into two main categories of types: **fundamental types** and **domain types**.
|
||||
|
||||
While fundamental types are here to be basic building blocks of a language, and are very similar to what you would see in other popular languages, domain types are what makes Wasp special, as they model the concepts of a web app like `page`, `route` and similar.
|
||||
While fundamental types are here to be basic building blocks of a a language, and are very similar to what you would see in other popular languages, domain types are what makes Wasp special, as they model the concepts of a web app like `page`, `route` and similar.
|
||||
|
||||
- Fundamental types ([source of truth](https://github.com/wasp-lang/wasp/blob/main/waspc/src/Wasp/Analyzer/Type.hs))
|
||||
- Primitive types
|
||||
@ -81,4 +88,4 @@ While fundamental types are here to be basic building blocks of a language, and
|
||||
- **JobExecutor**
|
||||
- **EmailProvider**
|
||||
|
||||
For more details about each of the domain types, both regarding their body types and what they mean, check the [Features](/language/features.md) section.
|
||||
You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.
|
@ -1,217 +0,0 @@
|
||||
---
|
||||
title: Auth UI
|
||||
---
|
||||
|
||||
# Auth UI
|
||||
|
||||
![Auth UI](/img/authui/all_screens.gif)
|
||||
|
||||
## Usage
|
||||
|
||||
### The UI changes dynamically as you update the config
|
||||
|
||||
Based on your `main.wasp` file on the authentication providers you enabled, the Auth UI will show the corresponding buttons.
|
||||
|
||||
For example, if you only enabled e-mail authentication:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We'll get this:
|
||||
|
||||
![Auth UI](/img/authui/login.png)
|
||||
|
||||
And then we enable Google and Github:
|
||||
|
||||
```wasp title="main.wasp" {7-8}
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
auth: {
|
||||
methods: {
|
||||
email: {},
|
||||
google: {},
|
||||
github: {},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The form will automatically update itself to look like this:
|
||||
|
||||
![Auth UI](/img/authui/multiple_providers.png)
|
||||
|
||||
|
||||
|
||||
## Available components
|
||||
|
||||
Let's take a look at the components that are available for you to use.
|
||||
|
||||
### Login form
|
||||
|
||||
Useful for <span className="pill pill-username-password">username & password</span> and <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Login form](/img/authui/login.png)
|
||||
|
||||
You can use the `LoginForm` component to build your own login form.
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Use it like this
|
||||
<LoginForm />
|
||||
```
|
||||
|
||||
### Signup form
|
||||
|
||||
Useful for <span className="pill pill-username-password">username & password</span> and <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Signup form](/img/authui/signup.png)
|
||||
|
||||
You can use the `SignupForm` component to build your signup form.
|
||||
|
||||
```tsx title="client/SignupPage.tsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
// Use it like this
|
||||
<SignupForm />
|
||||
```
|
||||
|
||||
### Forgot password form
|
||||
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Forgot password form](/img/authui/forgot_password.png)
|
||||
|
||||
You can use the `ForgotPasswordForm` component to build your own forgot password form.
|
||||
|
||||
```tsx title="client/ForgotPasswordPage.tsx"
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
|
||||
// Use it like this
|
||||
<ForgotPasswordForm />
|
||||
```
|
||||
|
||||
### Reset password form
|
||||
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Reset password form](/img/authui/reset_password.png)
|
||||
|
||||
You can use the `ResetPasswordForm` component to build your reset password form.
|
||||
|
||||
```tsx title="client/ResetPasswordPage.tsx"
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
|
||||
// Use it like this
|
||||
<ResetPasswordForm />
|
||||
```
|
||||
|
||||
### Verify email form
|
||||
|
||||
Useful for <span className="pill pill-email">email</span> authentication.
|
||||
|
||||
![Verify email form](/img/authui/email_verification.png)
|
||||
|
||||
You can use the `VerifyEmailForm` component to build your own verify email form.
|
||||
|
||||
```tsx title="client/VerifyEmailPage.tsx"
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
|
||||
// Use it like this
|
||||
<VerifyEmailForm />
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
You customize all of the available forms by passing props to them.
|
||||
|
||||
Props you can pass to all of the forms:
|
||||
- `appearance` - appearance of the form, see below (optional)
|
||||
- `logo` - path to your logo (optional)
|
||||
- `socialLayout` - layout of the social buttons, which can be `vertical` or `horizontal` (optional)
|
||||
|
||||
### Theme colors override
|
||||
|
||||
We used [Stitches](https://stitches.dev/) to style the Auth UI. You can customize the styles by overriding the default theme tokens.
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Define your appearance
|
||||
const appearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
},
|
||||
}
|
||||
|
||||
// Use it like this
|
||||
<LoginForm appearance={appearance} />
|
||||
```
|
||||
|
||||
See the list of all available tokens [here](https://github.com/wasp-lang/wasp/blob/main/waspc/data/Generator/templates/react-app/src/stitches.config.js). We'll be adding more tokens soon 🙂
|
||||
|
||||
### Adding your logo
|
||||
|
||||
You can add your logo to the Auth UI by passing the `logo` prop to any of the components.
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Use it like this
|
||||
<LoginForm logo="/img/logo.png" />
|
||||
```
|
||||
|
||||
### Social buttons layout
|
||||
|
||||
You can change the layout of the social buttons by passing the `socialLayout` prop to any of the components.
|
||||
|
||||
If we pass in `vertical`:
|
||||
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
// Use it like this
|
||||
<LoginForm
|
||||
socialLayout="vertical"
|
||||
/>
|
||||
```
|
||||
|
||||
We get this:
|
||||
|
||||
![Vertical social buttons](/img/authui/vertical_social_buttons.png)
|
||||
|
||||
### Example of a custom login form
|
||||
|
||||
If we provide the logo and custom colors:
|
||||
```tsx title="client/LoginPage.tsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
import { appearance } from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
// Use it like this
|
||||
<LoginForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
/>
|
||||
```
|
||||
|
||||
We get this:
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<img src="/img/authui/custom_login.gif" alt="Custom login form" />
|
||||
</div>
|
@ -1,281 +0,0 @@
|
||||
---
|
||||
title: Automatic CRUD
|
||||
---
|
||||
|
||||
import ImgWithCaption from '../../blog/components/ImgWithCaption'
|
||||
|
||||
For some [Entity](/docs/language/features#entity), you can tell Wasp to automatically generate server-side logic ([Queries](/docs/language/features#query) and [Actions](/docs/language/features#action)) for creating, reading, updating and deleting such entities. As your entities update, Wasp will automatically regenerate the backend logic.
|
||||
|
||||
:::caution Early preview
|
||||
This feature is currently in early preview and we are actively working on it. Read more about [our plans](/docs/language/features#crud-operations-on-top-of-entities) for CRUD operations.
|
||||
:::
|
||||
|
||||
## Defining new CRUD operations
|
||||
|
||||
Imagine we have a `Task` entity and we want to enable CRUD operations for it.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean
|
||||
psl=}
|
||||
```
|
||||
|
||||
We can then define a new `crud` called `Tasks`.
|
||||
|
||||
We specify to use the `Task` entity and we enable the `getAll`, `get`, `create` and `update` operations (let's say we don't need the `delete` operation).
|
||||
|
||||
```wasp title="main.wasp"
|
||||
crud Tasks {
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {
|
||||
isPublic: true, // by default only logged in users can perform operations
|
||||
},
|
||||
get: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js",
|
||||
},
|
||||
update: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
1. It uses default implementation for `getAll`, `get` and `update`,
|
||||
2. ... while specifying a custom implementation for `create`.
|
||||
3. `getAll` will be public (no auth needed), while the rest of the operations will be private.
|
||||
|
||||
Here's what it looks like when visualized:
|
||||
|
||||
<ImgWithCaption alt="Automatic CRUD with Wasp" source="img/crud_diagram.png" caption="Visualization of the Tasks crud declaration"/>
|
||||
|
||||
We can now use the CRUD queries and actions we just specified in our client code.
|
||||
|
||||
## Example: simple TODO app
|
||||
|
||||
Let's create a full app example that uses automatic CRUD. We'll stick to using the `Task` entity from the previous example, but we'll add a `User` entity and enable [username and password](/docs/language/features#username-and-password) based auth.
|
||||
|
||||
<ImgWithCaption alt="Automatic CRUD with Wasp" source="img/crud-guide.gif" caption="We are building a simple tasks app with username based auth"/>
|
||||
|
||||
### Creating the app
|
||||
|
||||
We can start by running `wasp new tasksCrudApp` and then we'll add the following to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app tasksCrudApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Tasks Crud App",
|
||||
|
||||
// We enabled auth and set the auth method to username and password
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
tasks Task[]
|
||||
psl=}
|
||||
|
||||
// We defined a Task entity on which we'll enable CRUD later on
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
psl=}
|
||||
|
||||
// Tasks app routes
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import { MainPage } from "@client/MainPage.jsx",
|
||||
authRequired: true,
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { LoginPage } from "@client/LoginPage.jsx",
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { SignupPage } from "@client/SignupPage.jsx",
|
||||
}
|
||||
```
|
||||
|
||||
We can then run `wasp db migrate-dev` to create the database and run the migrations.
|
||||
|
||||
### Adding CRUD to the `Task` entity ✨
|
||||
|
||||
Let's add the following `crud` declaration to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
crud Tasks {
|
||||
entity: Task,
|
||||
operations: {
|
||||
getAll: {},
|
||||
create: {
|
||||
overrideFn: import { createTask } from "@server/tasks.js",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
You'll notice that we enabled only `getAll` and `create` operations. This means that only these operations will be available.
|
||||
|
||||
We also overrode the `create` operation with a custom implementation. This means that the `create` operation will not be generated, but instead, the `createTask` function from `@server/tasks.js` will be used.
|
||||
|
||||
### Our custom `create` operation
|
||||
|
||||
Here's the `src/server/tasks.js` file:
|
||||
|
||||
```ts title="src/server/tasks.ts" {20-25}
|
||||
import type { CreateAction } from '@wasp/crud/Tasks'
|
||||
import type { Task } from '@wasp/entities'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
type Input = { description: string; isDone: boolean }
|
||||
type Output = Task
|
||||
|
||||
export const createTask: CreateAction<Input, Output> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401, 'User not authenticated.')
|
||||
}
|
||||
|
||||
const { description, isDone } = args
|
||||
const { Task } = context.entities
|
||||
|
||||
return await Task.create({
|
||||
data: {
|
||||
description,
|
||||
isDone,
|
||||
// Connect the task to the user that is creating it
|
||||
user: {
|
||||
connect: {
|
||||
id: context.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
We made a custom `create` operation because we want to make sure that the task is connected to the user that is creating it. In the current iteration of CRUD operations that is not supported by default. Read more about the [default implementations](/docs/language/features#which-operations-are-supported).
|
||||
|
||||
### Using the generated CRUD operations on the client
|
||||
|
||||
And let's use the generated operations in our client code:
|
||||
|
||||
```jsx title="pages/MainPage.jsx" {1,5-6}
|
||||
import { Tasks } from '@wasp/crud/Tasks'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
|
||||
const createTask = Tasks.create.useAction()
|
||||
const [taskDescription, setTaskDescription] = useState('')
|
||||
|
||||
function handleCreateTask() {
|
||||
createTask({ description: taskDescription, isDone: false })
|
||||
setTaskDescription('')
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '1.5rem',
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleCreateTask}>Create task</button>
|
||||
</div>
|
||||
<ul>
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id}>{task.description}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
And here are the login and signup pages, where we are using Wasp's [Auth UI](/docs/guides/auth-ui) components:
|
||||
|
||||
```jsx title="src/client/LoginPage.jsx"
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export function LoginPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<LoginForm />
|
||||
<div>
|
||||
<Link to="/signup">Create an account</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="src/client/SignupPage.jsx"
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
export function SignupPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
placeContent: 'center',
|
||||
}}
|
||||
>
|
||||
<SignupForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
That's it. You can now run `wasp start` and see the app in action ⚡️
|
||||
|
||||
You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.
|
||||
|
||||
### Future of CRUD operations in Wasp
|
||||
|
||||
CRUD operations currently have a limited set of knowledge about the business logic they are implementing.
|
||||
|
||||
- For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the `create` operation in the example above.
|
||||
- Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
|
||||
- Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.
|
||||
|
||||
CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.
|
||||
|
||||
We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on [this Github issue](https://github.com/wasp-lang/wasp/issues/1253) to see how we are doing.
|
||||
|
||||
---
|
||||
|
||||
Join our **community** on [Discord](https://discord.com/invite/rzdnErX), where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.
|
@ -1,429 +0,0 @@
|
||||
---
|
||||
title: Email Authentication
|
||||
---
|
||||
|
||||
# Email Authentication
|
||||
|
||||
## Overview
|
||||
|
||||
Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides a set of routes and email templates that you can use to implement it in your app.
|
||||
|
||||
![Auth UI](/img/authui/all_screens.gif)
|
||||
|
||||
In this guide, we'll go through the easiest way to set up email authentication: using Wasp's [Auth UI](/docs/guides/auth-ui) components. Check out the Auth UI guide for more details on how to customize the UI.
|
||||
|
||||
## Outline of the guide
|
||||
|
||||
We'll need to take the following steps to set up email authentication:
|
||||
- [ ] Set up email authentication in `main.wasp`
|
||||
- [ ] Add the user entity
|
||||
- [ ] Add the routes and pages
|
||||
- [ ] Set up the email sender in `main.wasp` and `.env.server`
|
||||
- [ ] Use Auth UI components in our pages
|
||||
|
||||
Outline of the Wasp file we'll be working with:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// Configuring e-mail authentication
|
||||
app myApp { ... }
|
||||
|
||||
// Defining User entity
|
||||
entity User { ... }
|
||||
|
||||
// Defining routes and pages
|
||||
route SignupRoute { ... }
|
||||
page SignupPage { ... }
|
||||
// ...
|
||||
```
|
||||
|
||||
### Email authentication in `main.wasp`
|
||||
|
||||
Let's first set up the email authentication by adding the following to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "My App",
|
||||
auth: {
|
||||
// 1. Specify the user entity
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// 2. Enable email authentication
|
||||
email: {
|
||||
// 3. Specify the email from field
|
||||
fromField: {
|
||||
name: "My App Postman",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
// 4. Specify the email verification and password reset options
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
},
|
||||
allowUnverifiedLogin: false,
|
||||
},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
onAuthSucceededRedirectTo: "/profile"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### User entity
|
||||
|
||||
Then we'll define the `User` entity in our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp" {4-8}
|
||||
// 5. Define the user entity
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
// Add your own fields below
|
||||
// ...
|
||||
psl=}
|
||||
```
|
||||
|
||||
### Routes and pages
|
||||
|
||||
Next, we need to define the routes and pages for the authentication pages. We'll show the React code later, but for now we'll just define the routes and pages.
|
||||
|
||||
We'll add the following to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// 6. Define the routes
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import { Signup } from "@client/pages/auth/Signup.tsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import { Login } from "@client/pages/auth/Login.tsx"
|
||||
}
|
||||
|
||||
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
|
||||
page RequestPasswordResetPage {
|
||||
component: import { RequestPasswordReset } from "@client/pages/auth/RequestPasswordReset.tsx",
|
||||
}
|
||||
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth/PasswordReset.tsx",
|
||||
}
|
||||
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth/EmailVerification.tsx",
|
||||
}
|
||||
```
|
||||
|
||||
### Email sender
|
||||
|
||||
We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers. Read more about setting up the email sender in the [email sender setup guide](/docs/guides/sending-emails).
|
||||
|
||||
To set up SendGrid to send emails, we will add the following to our `main.wasp` file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app myApp {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
... and add the following to our `.env.server` file:
|
||||
|
||||
```c title=".env.server"
|
||||
SENDGRID_API_KEY=<your key>
|
||||
```
|
||||
|
||||
## Using Auth UI
|
||||
|
||||
:::info
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the page. Read more about how to add it [here](/docs/integrations/css-frameworks#tailwind).
|
||||
:::
|
||||
|
||||
### Signup page
|
||||
|
||||
![Auth UI](/img/authui/signup.png)
|
||||
|
||||
We are using the `SignupForm` component from `@wasp/auth/forms/Signup` to render the signup form.
|
||||
|
||||
```tsx title="client/pages/auth/Signup.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
export function Signup () {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Login page
|
||||
|
||||
![Auth UI](/img/authui/login.png)
|
||||
|
||||
We are using the `LoginForm` component from `@wasp/auth/forms/Login` to render the login form.
|
||||
|
||||
```tsx title="client/pages/auth/Login.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Don't have an account yet? <Link to="/signup">go to signup</Link>.
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Forgot your password?{' '}
|
||||
<Link to="/request-password-reset">reset it</Link>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Email verification setup
|
||||
|
||||
By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.
|
||||
|
||||
Our setup looks like this:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
emailVerification: {
|
||||
clientRoute: EmailVerificationRoute,
|
||||
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
|
||||
}
|
||||
```
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `EmailVerificationRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
|
||||
page EmailVerificationPage {
|
||||
component: import { EmailVerification } from "@client/pages/auth/EmailVerification.tsx",
|
||||
}
|
||||
```
|
||||
|
||||
### Email verification page
|
||||
|
||||
![Auth UI](/img/authui/email_verification.png)
|
||||
|
||||
This route goes to the `EmailVerification` page, which is defined in the `EmailVerification.tsx` file:
|
||||
|
||||
```jsx title="client/pages/auth/EmailVerification.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
|
||||
export function EmailVerification() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<VerifyEmailForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
You'll notice we are using the `VerifyEmailForm` component from the `@wasp/auth/forms/VerifyEmail` module. This will give a nice-looking form for the user to verify their e-mail.
|
||||
|
||||
We will also override the default e-mail content. We are using the `getVerificationEmailContent` function from the `@server/auth/email.js` file to generate the email content.
|
||||
|
||||
```ts title="server/auth/email.ts"
|
||||
import { GetVerificationEmailContentFn } from '@wasp/types'
|
||||
|
||||
export const getVerificationEmailContent: GetVerificationEmailContentFn = ({
|
||||
verificationLink,
|
||||
}) => ({
|
||||
subject: 'Verify your email',
|
||||
text: `Click the link below to verify your email: ${verificationLink}`,
|
||||
html: `
|
||||
<p>Click the link below to verify your email</p>
|
||||
<a href="${verificationLink}">Verify email</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
|
||||
## Password reset setup
|
||||
|
||||
Users can request a password and then they'll receive an e-mail with a link to reset their password.
|
||||
|
||||
Our setup in `main.wasp` looks like this:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
passwordReset: {
|
||||
clientRoute: PasswordResetRoute,
|
||||
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
|
||||
}
|
||||
```
|
||||
|
||||
### Request password reset page
|
||||
|
||||
![Request password reset page](/img/authui/forgot_password_after.png)
|
||||
|
||||
They request their password to be reset by going to the `/request-password-reset` page. We'll add a link to this page in the login page.
|
||||
|
||||
That page will look like this:
|
||||
|
||||
```jsx title="client/pages/auth/RequestPasswordReset.tsx"
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
|
||||
export function RequestPasswordReset() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
We will also override the default e-mail content that's sent. We are using the `getVerificationEmailContent` function from the `@server/auth/email.js` file to generate the email content.
|
||||
|
||||
```ts title="server/auth/email.ts"
|
||||
import { GetPasswordResetEmailContentFn } from '@wasp/types'
|
||||
|
||||
export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({
|
||||
passwordResetLink,
|
||||
}) => ({
|
||||
subject: 'Password reset',
|
||||
text: `Click the link below to reset your password: ${passwordResetLink}`,
|
||||
html: `
|
||||
<p>Click the link below to reset your password</p>
|
||||
<a href="${passwordResetLink}">Reset password</a>
|
||||
`,
|
||||
})
|
||||
```
|
||||
|
||||
### Password reset page
|
||||
|
||||
![Request password reset page](/img/authui/reset_password_after.png)
|
||||
|
||||
When the user receives an e-mail, they receive a link that goes to the client route specified in the `clientRoute` field. In our case, this is the `PasswordResetRoute` route we defined in the `main.wasp` file.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
|
||||
page PasswordResetPage {
|
||||
component: import { PasswordReset } from "@client/pages/auth/PasswordReset.tsx",
|
||||
}
|
||||
```
|
||||
|
||||
This route goes to the `PasswordResetPage` page, which is defined in the `PasswordReset.tsx` file. Users can enter their new password here:
|
||||
|
||||
```tsx title="client/pages/auth/PasswordReset.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
|
||||
export function PasswordReset() {
|
||||
return (
|
||||
<div className="w-full h-full bg-white">
|
||||
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
|
||||
<div className="w-full h-full max-w-sm p-5 bg-white">
|
||||
<div>
|
||||
<ResetPasswordForm />
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Logout action
|
||||
|
||||
To implement the logout action, you can use the `logout` function from the `@wasp/auth/logout` module. We can add it for example, to the `Navbar` component:
|
||||
|
||||
```jsx title="client/components/Navbar.tsx"
|
||||
import logout from '@wasp/auth/logout';
|
||||
|
||||
export function Navbar() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Getting the user in our client & server code
|
||||
|
||||
You read about our `useAuth` hook in [this section](/docs/language/features#accessing-the-currently-logged-in-user) of the docs.
|
||||
|
||||
In short, you can use the `useAuth` hook in your client code to get the currently logged-in user. If there is no user logged in, it will return `null`.
|
||||
|
||||
```jsx title="client/pages/Profile.tsx"
|
||||
import useAuth from '@wasp/auth'
|
||||
|
||||
export function Profile() {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
if (!user) {
|
||||
return <div>You are not logged in!</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Hello, {user.email}!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
And that's it! We now have a full authentication system in our app. We can register new users, login, logout, verify their e-mail, and reset their password.
|
||||
|
||||
We hope you enjoyed this guide and that you learned something new. If you have any questions, feel free to ask them on [our Discord server](https://discord.gg/rzdnErX).
|
@ -1,145 +0,0 @@
|
||||
---
|
||||
title: Middleware Customization
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# Customizing Express server middleware
|
||||
|
||||
Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-`api`/path basis.
|
||||
|
||||
## Default global middleware
|
||||
|
||||
- [Helmet](https://helmetjs.github.io/): Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it can help!
|
||||
- [CORS](https://github.com/expressjs/cors#readme): CORS is a package for providing a middleware that can be used to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options.
|
||||
- ⚠️ This is required for the frontend to communicate with the backend.
|
||||
- [Morgan](https://github.com/expressjs/morgan#readme): HTTP request logger middleware.
|
||||
- [express.json](https://expressjs.com/en/api.html#express.json) (which uses [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)): Parses incoming request bodies in a middleware before your handlers, making the result available under the `req.body` property.
|
||||
- ⚠️ This is required for Wasp Operations to function properly.
|
||||
- [express.urlencoded](https://expressjs.com/en/api.html#express.urlencoded) (which uses [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions)): Returns middleware that only parses urlencoded bodies and only looks at requests where the `Content-Type` header matches the type option.
|
||||
- [cookieParser](https://github.com/expressjs/cookie-parser#readme): Parse Cookie header and populate `req.cookies` with an object keyed by the cookie names.
|
||||
|
||||
## Customization
|
||||
|
||||
You have three places where you can customize middleware:
|
||||
1. global: here, any changes will apply by default *to all operations (`query` and `action`) and `api`.* This is helpful if you wanted to add support for multiple domains to CORS, for example. ⚠️ Please treat modifications to global middleware with extreme care!
|
||||
2. per-api: you can override middleware for a specific api route (exe: `POST /webhook/callback`). This is helpful if you want to disable JSON parsing, for example.
|
||||
3. per-path: this is helpful if you need to customize middleware for all methods for a given path. This is helpful for things like "complex CORS requests" which may need to apply to both `OPTIONS` and `GET`, or to apply some middleware to a _set of `api` routes_.
|
||||
|
||||
### Types
|
||||
|
||||
Below are the relevant TS types and the actual definitions of default middleware.
|
||||
|
||||
```ts
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
```
|
||||
|
||||
## 1. Customize global middleware
|
||||
|
||||
If you would like to modify the middleware for _all_ operations and APIs, you can do something like:
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/serverSetup.ts
|
||||
import cors from 'cors'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// Example of adding an extra domains to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Customize `api`-specific middleware
|
||||
|
||||
If you would like to modify the middleware for a single API, you can do something like:
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
import express from 'express'
|
||||
import { WebhookCallback } from '@wasp/apis/types'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const webhookCallback: WebhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
:::note
|
||||
This gets installed on a per-method basis. Behind the scenes, this results in something like:
|
||||
|
||||
```js
|
||||
router.post('/webhook/callback', webhookCallbackMiddleware, ...)
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Customize per-path middleware
|
||||
|
||||
If you would like to modify the middleware for all API routes under some common path, you can do something like:
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
const customMiddleware : express.RequestHandler = (_req, _res, next) => {
|
||||
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.middleware', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This gets installed at the router level for the path. Behind the scenes, this results in something like:
|
||||
|
||||
```js
|
||||
router.use('/foo/bar', fooBarNamespaceMiddleware)
|
||||
```
|
||||
:::
|
@ -1,147 +0,0 @@
|
||||
---
|
||||
title: Sending Emails
|
||||
---
|
||||
|
||||
import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
|
||||
# Sending Emails
|
||||
|
||||
With Wasp's email-sending feature, you can easily integrate email functionality into your web application.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Choose from one of the providers:
|
||||
- `Mailgun`,
|
||||
- `SendGrid`
|
||||
- or the good old `SMTP`.
|
||||
|
||||
Optionally, define the `defaultFrom` field, so you don't need to provide it whenever sending an e-mail.
|
||||
|
||||
## Sending e-mails
|
||||
|
||||
<SendingEmailsInDevelopment />
|
||||
|
||||
Before jumping into details about setting up various providers, let's see how easy it is to send e-mails.
|
||||
|
||||
You import the `emailSender` that is provided by the `@wasp/email` module and call the `send` method on it.
|
||||
|
||||
```ts title="src/actions/sendEmail.js"
|
||||
import { emailSender } from '@wasp/email/index.js'
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: 'John Doe',
|
||||
email: 'john@doe.com',
|
||||
},
|
||||
to: 'user@domain.com',
|
||||
subject: 'Saying hello',
|
||||
text: 'Hello world',
|
||||
html: 'Hello <strong>world</strong>'
|
||||
})
|
||||
```
|
||||
|
||||
Let's see what the `send` method accepts:
|
||||
|
||||
- `from` - the sender's details.
|
||||
- `name` - the name of the sender
|
||||
- `email` - the e-mail address of the sender
|
||||
- If you set up `defaultFrom` field in the `main.wasp`, this field is optional.
|
||||
- `to` - the recipient's e-mail address
|
||||
- `subject` - the subject of the e-mail
|
||||
- `text` - the text version of the e-mail
|
||||
- `html` - the HTML version of the e-mail
|
||||
|
||||
The `send` method returns an object with the status of the sent e-mail. It varies depending on the provider you use.
|
||||
|
||||
## Providers
|
||||
|
||||
For each provider, you'll need to set up env variables in the `.env.server` file at the root of your project.
|
||||
|
||||
## Using the SMTP provider
|
||||
|
||||
First, set the provider to `SMTP` in your `main.wasp` file.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, add the following env variables to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SMTP_HOST=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_PORT=
|
||||
```
|
||||
|
||||
Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.
|
||||
|
||||
## Using the Mailgun provider
|
||||
|
||||
Set the provider to `Mailgun` in the `main.wasp` file.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: Mailgun,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, get the Mailgun API key and domain and add them to your `.env.server` file.
|
||||
|
||||
### Getting the API key and domain
|
||||
|
||||
1. Go to [Mailgun](https://www.mailgun.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
|
||||
5. Copy the domain and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
MAILGUN_API_KEY=
|
||||
MAILGUN_DOMAIN=
|
||||
```
|
||||
|
||||
## Using the SendGrid provider
|
||||
|
||||
Set the provider field to `SendGrid` in your `main.wasp` file.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, get the SendGrid API key and add it to your `.env.server` file.
|
||||
|
||||
### Getting the API key
|
||||
|
||||
1. Go to [SendGrid](https://sendgrid.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.sendgrid.com/settings/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SENDGRID_API_KEY=
|
||||
```
|
@ -1,110 +0,0 @@
|
||||
---
|
||||
title: Testing
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
:::info
|
||||
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future.
|
||||
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
||||
:::
|
||||
|
||||
# Testing your React app
|
||||
|
||||
Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because we already use [Vite](https://vitejs.dev/), we also support testing the web app by giving you an integrated [Vitest](https://vitest.dev/) experience out of the box.
|
||||
|
||||
<details>
|
||||
<summary>Included Libraries</summary>
|
||||
<div>
|
||||
|
||||
[`vitest`](https://www.npmjs.com/package/vitest): Unit test framework with native Vite support.
|
||||
|
||||
[`@vitest/ui`](https://www.npmjs.com/package/@vitest/ui): A nice UI for seeing your test results.
|
||||
|
||||
[`jsdom`](https://www.npmjs.com/package/jsdom): A web browser test environment for Node.js.
|
||||
|
||||
[`@testing-library/react`](https://www.npmjs.com/package/@testing-library/react) / [`@testing-library/jest-dom`](https://www.npmjs.com/package/@testing-library/jest-dom): Testing helpers.
|
||||
|
||||
[`msw`](https://www.npmjs.com/package/msw): A server mocking library.
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
## Test File Structure
|
||||
|
||||
Unit tests should live under your `src/client` directory and have an extension that is compatible with [these glob pattern defaults](https://vitest.dev/config/#include). Some of the files Vitest would pick up automatically: `yourFile.test.ts` or `YourComponent.test.jsx`.
|
||||
|
||||
Within test files, you can import things to test using relative paths. For example, if you made a component called `Counter.jsx`, now you can test it by adding a file alongside it called `Counter.test.jsx`, which would just import from `'./Counter'`.
|
||||
|
||||
## Running Tests
|
||||
|
||||
Running `wasp test client` will execute Vitest in watch mode, and watch your Wasp source tree for any changes to compile as well.
|
||||
|
||||
- If you want to see a live-updating UI, you can pass a `--ui` option, like so: `wasp test client --ui`
|
||||
- If you'd like to just run the tests once and exit (for example, in CI), you can pass `run`, like so: `wasp test client run`
|
||||
|
||||
In fact, anything after `wasp test client` gets passed to Vitest directly, so check out [their CLI docs](https://vitest.dev/guide/cli.html) for more.
|
||||
|
||||
:::warning
|
||||
You should not run `wasp test` while running `wasp start`, as both will attempt to compile and write your project to `.wasp/out`.
|
||||
:::
|
||||
|
||||
## Examples
|
||||
### Unit Tests
|
||||
|
||||
```ts title=src/client/Todo.test.ts
|
||||
import { test, expect } from 'vitest'
|
||||
|
||||
import { areThereAnyTasks } from './Todo'
|
||||
|
||||
test('areThereAnyTasks', () => {
|
||||
expect(areThereAnyTasks([])).toBe(false)
|
||||
})
|
||||
```
|
||||
|
||||
### React Component Tests
|
||||
|
||||
```ts title=src/client/Todo.test.tsx
|
||||
import { test, expect } from 'vitest'
|
||||
import { screen } from '@testing-library/react'
|
||||
|
||||
import { mockServer, renderInContext } from '@wasp/test'
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import Todo from './Todo'
|
||||
|
||||
const { mockQuery } = mockServer()
|
||||
|
||||
const mockTasks = [{
|
||||
id: 1,
|
||||
description: 'test todo 1',
|
||||
isDone: true,
|
||||
userId: 1
|
||||
}]
|
||||
|
||||
test('handles mock data', async () => {
|
||||
mockQuery(getTasks, mockTasks)
|
||||
|
||||
renderInContext(<Todo />)
|
||||
|
||||
await screen.findByText('test todo 1')
|
||||
|
||||
expect(screen.getByRole('checkbox')).toBeChecked()
|
||||
|
||||
screen.debug()
|
||||
})
|
||||
```
|
||||
|
||||
#### React Testing Helpers
|
||||
|
||||
Wasp provides two React testing helpers:
|
||||
- `mockQuery`: Takes a Wasp Query to mock and the JSON data to return.
|
||||
- This is helpful if your Query uses `useQuery`.
|
||||
- Behind the scenes, this uses [`msw`](https://www.npmjs.com/package/msw) to create a server request handler that responds with the provided JSON to an HTTP request for the operation's endpoint.
|
||||
- Request handlers are cleared after each test.
|
||||
- `mockApi`: Similar to `mockQuery`, but for mocking `api`s instead.
|
||||
- Instead of a Wasp Query, it takes a route of the shape: `mockApi({ method: HttpMethod.Get, path: '/foo/bar' }, { res: 'hello' })`.
|
||||
- You can import `HttpMethod` like so: `import { HttpMethod } from '@wasp/types'`.
|
||||
- `renderInContext`: Takes a React component, wraps it inside a `QueryClientProvider` and `Router`, and renders it.
|
||||
|
||||
# Testing your server-side code
|
||||
|
||||
Coming soon!
|
@ -1,181 +0,0 @@
|
||||
---
|
||||
title: Username & Password
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# Username & Password
|
||||
|
||||
### Configuration
|
||||
|
||||
To get started with a simple Username & Password Auth strategy, you'll need to add the Auth object with the following configuration to your `main.wasp` file:
|
||||
```c title="main.wasp"
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
|
||||
title: "Example",
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
|
||||
// Wasp requires the userEntity to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
For more info on the specific fields, check out this [Auth](/docs/language/features#authentication--authorization) section of the docs.
|
||||
|
||||
If you're adding a new entity to your `.wasp` file for the first time, make sure to create the table for it in your database:
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
You'll also need to add these environment variables to your `.env.server` file at the root of your project:
|
||||
|
||||
```bash title=".env.server"
|
||||
JWT_SECRET=random-string-at-least-32-characters-long.
|
||||
```
|
||||
|
||||
With `auth` now defined, Wasp offers a number of handy features out of the box:
|
||||
- ["AuthUI" Login and Signup forms](/docs/guides/auth-ui) located at `@wasp/auth/forms/Login` and `@wasp/auth/forms/Signup` paths, ready to be styled and used.
|
||||
- The `logout()` action.
|
||||
- The `useAuth()` hook to access the logged-in user client-side.
|
||||
- The `context.user` object as an argument server-side within [Operations](/docs/language/features#queries-and-actions-aka-operations).
|
||||
|
||||
:::tip Customizing Auth
|
||||
This is a very high-level API for auth which makes it very easy to get started quickly, but is
|
||||
not very flexible. If you require more control (e.g. want to execute some custom code on the server
|
||||
during signup, check out the [lower-level auth API](/docs/language/features#lower-level-api).
|
||||
:::
|
||||
|
||||
### Client-side
|
||||
|
||||
To access the logged-in user client-side, you have two options:
|
||||
|
||||
1. You can use the `user` object that Wasp passes to all pages by default:
|
||||
|
||||
```jsx
|
||||
const Page = ({ user }) => {
|
||||
const username = user.username
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
2. Use the `useAuth()` hook:
|
||||
|
||||
```jsx
|
||||
import useAuth from '@wasp/auth/useAuth.js'
|
||||
|
||||
const Page = () => {
|
||||
const { data: user, isLoading, isError } = useAuth();
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
You can then do things like displaying some of the user information on the page. Here's an example:
|
||||
|
||||
|
||||
```jsx
|
||||
import useAuth from '@wasp/auth/useAuth.js'
|
||||
import logout from '@wasp/auth/logout.js'
|
||||
import Todo from '../Todo.js'
|
||||
|
||||
const Page = () => {
|
||||
const { data: user } = useAuth()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Welcome {user.username}!</h1>
|
||||
<Todo />
|
||||
<button onClick={logout}>Logout</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
```
|
||||
|
||||
You don't need to use the `useAuth()` hook if you're trying to protect a page from unauthorized users. Wasp takes care of that for you with its [higher-level API](/docs/language/features#authentication--authorization):
|
||||
|
||||
```c title="main.wasp" {28}
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Example",
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
|
||||
// Wasp requires the userEntity to have at least the following fields
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
|
||||
// By adding `authRequired: true` to a page, Wasp will automatically
|
||||
// redirect unauthenticated users to the `onAuthFailedRedirectTo` route
|
||||
route MainRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
authRequired: true,
|
||||
component: import Main from "@client/MainPage"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/LoginPage"
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/SignupPage"
|
||||
}
|
||||
```
|
||||
|
||||
### Server-side
|
||||
|
||||
To access the logged-in user server-side, you can use the `context.user` object within [Operations (i.e. *queries and actions*)](/docs/language/features#queries-and-actions-aka-operations):
|
||||
|
||||
|
||||
```js title="src/server/actions.js" {4}
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (task, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401, 'You need to be logged in to create a task.')
|
||||
}
|
||||
|
||||
return context.entities.Task.create({
|
||||
data: {
|
||||
description: task.description,
|
||||
user: {
|
||||
connect: { id: context.user.id }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
@ -1,167 +0,0 @@
|
||||
---
|
||||
title: WebSockets
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# WebSocket support
|
||||
Wasp provides a fully integrated WebSocket experience by utilizing [Socket.IO](https://socket.io/) on the client and server.
|
||||
|
||||
We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful `useSocket` and `useSocketListener` abstractions for use in React components.
|
||||
|
||||
To get started, you need to:
|
||||
1. Define your WebSocket logic on the server.
|
||||
2. Declare you are using WebSockets in your Wasp file, and connect it with your server logic.
|
||||
3. Use WebSockets on the client, in React, via `useSocket` and `useSocketListener`.
|
||||
4. Optionally, type the WebSocket events and payloads for full-stack type safety.
|
||||
|
||||
We will cover all the steps above, but in an order that makes it easier to explain new concepts.
|
||||
|
||||
## Turn on WebSockets in your Wasp file
|
||||
We specify that we are using WebSockets by adding `webSocket` to our `app` and providing the required `fn`. You can optionally change the auto-connect behavior (on by default).
|
||||
|
||||
```wasp title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/webSocket.js",
|
||||
autoConnect: true, // optional, default: true
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Implement the WebSocket server
|
||||
Let's define the server with all of the events and handler functions.
|
||||
|
||||
:::info Full-stack type safety
|
||||
If you are using TypeScript, you can define event names with the matching payload types on the server and have those types exposed automatically on the client. This helps you avoid mistakes when emitting events or handling them. Read more in the [Typescript guide](/docs/typescript#websocket-full-stack-type-support).
|
||||
:::
|
||||
|
||||
### Defining the events handler
|
||||
On the server, you will get Socket.IO `io: Server` argument and `context` for your WebSocket function, which contains all entities you defined in your Wasp app. You can use this `io` object to register callbacks for all the regular [Socket.IO events](https://socket.io/docs/v4/server-api/).
|
||||
|
||||
Lastly, if a user is logged in, you will have a `socket.data.user` on the server.
|
||||
|
||||
```ts title=src/server/webSocket.ts
|
||||
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const webSocketFn: WebSocketFn = (io, context) => {
|
||||
io.on('connection', (socket) => {
|
||||
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
|
||||
console.log('a user connected: ', username)
|
||||
|
||||
socket.on('chatMessage', async (msg) => {
|
||||
console.log('message: ', msg)
|
||||
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
|
||||
// You can also use your entities here:
|
||||
// await context.entities.SomeEntity.create({ someField: msg })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Typing our WebSocket function with the events and payloads
|
||||
// allows us to get type safety on the client as well
|
||||
|
||||
type WebSocketFn = WebSocketDefinition<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>
|
||||
|
||||
interface ServerToClientEvents {
|
||||
chatMessage: (msg: { id: string, username: string, text: string }) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
chatMessage: (msg: string) => void;
|
||||
}
|
||||
|
||||
interface InterServerEvents {}
|
||||
|
||||
// Data that is attached to the socket.
|
||||
// NOTE: Wasp automatically injects the JWT into the connection,
|
||||
// and if present/valid, the server adds a user to the socket.
|
||||
interface SocketData extends WaspSocketData {}
|
||||
```
|
||||
|
||||
## Using the WebSocket on the client
|
||||
Client access to WebSockets is provided by the `useSocket` hook. It returns:
|
||||
- `socket: Socket` for sending and receiving events.
|
||||
- `isConnected: boolean` for showing a display of the Socket.IO connection status.
|
||||
- Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly `socket.connect()` or `socket.disconnect()`.
|
||||
- If you set `autoConnect: false` in your Wasp file, then you should call these as needed.
|
||||
|
||||
Additionally, there is a `useSocketListener: (event, callback) => void` hook which is used for registering event handlers. It takes care of unregistering on unmount.
|
||||
|
||||
All components using `useSocket` share the same underlying `socket`.
|
||||
|
||||
```tsx title=src/client/ChatPage.tsx
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
useSocket,
|
||||
useSocketListener,
|
||||
ServerToClientPayload,
|
||||
} from '@wasp/webSocket'
|
||||
|
||||
export const ChatPage = () => {
|
||||
const [messageText, setMessageText] = useState<
|
||||
// We are using a helper type to get the payload type for the "chatMessage" event.
|
||||
ClientToServerPayload<'chatMessage'>
|
||||
>('')
|
||||
const [messages, setMessages] = useState<
|
||||
ServerToClientPayload<'chatMessage'>[]
|
||||
>([])
|
||||
// The "socket" instance is typed with the types you defined on the server.
|
||||
const { socket, isConnected } = useSocket()
|
||||
|
||||
// This is a type-safe event handler: "chatMessage" event and its payload type
|
||||
// are defined on the server.
|
||||
useSocketListener('chatMessage', logMessage)
|
||||
|
||||
function logMessage(msg: ServerToClientPayload<'chatMessage'>) {
|
||||
setMessages((priorMessages) => [msg, ...priorMessages])
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
// This is a type-safe event emitter: "chatMessage" event and its payload type
|
||||
// are defined on the server.
|
||||
socket.emit('chatMessage', messageText)
|
||||
setMessageText('')
|
||||
}
|
||||
|
||||
const messageList = messages.map((msg) => (
|
||||
<li key={msg.id}>
|
||||
<em>{msg.username}</em>: {msg.text}
|
||||
</li>
|
||||
))
|
||||
const connectionIcon = isConnected ? '🟢' : '🔴'
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Chat {connectionIcon}</h2>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ul>{messageList}</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Read more about the types that are available when using WebSockets with Typescript in the [Typescript guide](/docs/typescript#websocket-full-stack-type-support).
|
@ -1,111 +0,0 @@
|
||||
---
|
||||
title: CSS Frameworks
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# CSS Frameworks
|
||||
|
||||
## Tailwind
|
||||
|
||||
To enable support for Tailwind in your Wasp project, you simply need to add two config files (`tailwind.config.cjs` and `postcss.config.cjs`) to the root directory. When they are present, Wasp will add the necessary NPM dependencies and copy your config files into the generated project output. You can then start adding [Tailwind CSS directives](https://tailwindcss.com/docs/functions-and-directives#directives) to your CSS files and `className`s to your React components.
|
||||
|
||||
:::tip
|
||||
|
||||
**After adding the required config files, make sure to restart the local Wasp server** via your CLI (just run `wasp start` again). In some cases it is neccesary for Tailwind to become functional in your app.
|
||||
|
||||
:::
|
||||
|
||||
### New project tree overview
|
||||
```bash title="tree ." {6,13-14}
|
||||
.
|
||||
├── main.wasp
|
||||
├── src
|
||||
│ ├── client
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ ├── Main.css
|
||||
│ │ ├── MainPage.js
|
||||
│ │ └── waspLogo.png
|
||||
│ ├── server
|
||||
│ │ └── tsconfig.json
|
||||
│ └── shared
|
||||
│ └── tsconfig.json
|
||||
├── postcss.config.cjs
|
||||
└── tailwind.config.cjs
|
||||
```
|
||||
|
||||
### Tailwind integration steps
|
||||
|
||||
:::caution
|
||||
You need to name the config files with the `.cjs` extension since they are CommonJS modules. If you name them with `.js` extension, Wasp will not be able to find them and Tailwind integration will not work.
|
||||
:::
|
||||
|
||||
#### 1) Add `./tailwind.config.cjs`
|
||||
```js title="./tailwind.config.cjs"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
#### 2) Add `./postcss.config.cjs`
|
||||
```js title="./postcss.config.cjs"
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 3) Update `./src/client/Main.css`
|
||||
```css title="./src/client/Main.css" {1-3}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* rest of content below */
|
||||
```
|
||||
|
||||
#### 4) Start using Tailwind 🥳
|
||||
```html title="./src/client/MainPage.js"
|
||||
<h1 className="text-3xl font-bold underline">
|
||||
Hello world!
|
||||
</h1>
|
||||
```
|
||||
|
||||
### Adding Tailwind plugins
|
||||
To add Tailwind plugins, add them to your `tailwind.config.cjs` file and `main.wasp` files:
|
||||
|
||||
```js title="./tailwind.config.cjs" {10-11}
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
```js title="main.wasp" {4-5}
|
||||
app todoApp {
|
||||
title: "Example App",
|
||||
dependencies: [
|
||||
("@tailwindcss/forms", "^0.5.3"),
|
||||
("@tailwindcss/typography", "^0.5.7")
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
@ -1,85 +0,0 @@
|
||||
---
|
||||
title: GitHub
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# GitHub
|
||||
|
||||
To implement GitHub Auth, you'll need to add the Auth object with the following configuration to your `main.wasp` file:
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Example",
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
gitHub: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
For more info on the specific fields, check out this [Auth](../language/features#social-login-providers-oauth-20) section of the docs.
|
||||
|
||||
If you're adding a new entity to your `.wasp` file, make sure you migrate your database schema:
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
You'll also need to add these environment variables to your `.env.server` file at the root of your project:
|
||||
|
||||
```bash title=".env.server"
|
||||
GITHUB_CLIENT_ID=your-github-client-id
|
||||
GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
|
||||
JWT_SECRET=random-string-at-least-32-characters-long.
|
||||
```
|
||||
We will cover how to get these values in the next section.
|
||||
|
||||
|
||||
## GitHub Auth
|
||||
|
||||
To use GitHub as an authentication method (covered [here](/docs/language/features#social-login-providers-oauth-20)), you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here is how to do so:
|
||||
|
||||
1. Log into your GitHub account and navigate to: https://github.com/settings/developers
|
||||
2. Select "New OAuth App"
|
||||
3. Supply required information
|
||||
|
||||
<img alt="GitHub Applications Screenshot"
|
||||
src={useBaseUrl('img/integrations-github-1.png')}
|
||||
width="400px"
|
||||
/>
|
||||
|
||||
- For "Authorization callback URL", if you just want to test your local app, put in: `http://localhost:3000/auth/login/github`
|
||||
- Once you know on which URL your API server will be deployed, you can create a new app with that URL instead.
|
||||
- For example: `https://someotherhost.com/auth/login/github`
|
||||
4. Hit "Register application"
|
||||
5. Copy your Client ID and Client secret, and paste them into your environment variables named `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`in your `.env.server` file.
|
||||
6. Now when youre user logs in with GitHub, you can access the logged in user on the client via the `useAuth()` hook, and on the server via the `context.user` object as described [here](/docs/language/features#accessing-the-currently-logged-in-user)!
|
||||
|
||||
|
@ -1,123 +0,0 @@
|
||||
---
|
||||
title: Google
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# Google
|
||||
|
||||
To implement Google Auth, you'll need to add the Auth object with the following configuration to your `main.wasp` file:
|
||||
```wasp title="main.wasp"
|
||||
app Example {
|
||||
wasp: {
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Example",
|
||||
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
google: {}
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
// ...
|
||||
externalAuthAssociations SocialLogin[]
|
||||
psl=}
|
||||
|
||||
entity SocialLogin {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
provider String
|
||||
providerId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
@@unique([provider, providerId, userId])
|
||||
psl=}
|
||||
```
|
||||
|
||||
For more info on the specific fields, check out this [Auth](../language/features#social-login-providers-oauth-20) section of the docs.
|
||||
|
||||
If you're adding a new entity to your `.wasp` file, make sure you migrate your database schema:
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
You'll also need to add these environment variables to your `.env.server` file at the root of your project:
|
||||
|
||||
```bash title=".env.server"
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
JWT_SECRET=random-string-at-least-32-characters-long.
|
||||
```
|
||||
We will cover how to get these values in the next section.
|
||||
|
||||
## Google Auth
|
||||
|
||||
To use Google as an authentication method (covered [here](/docs/language/features#social-login-providers-oauth-20)), you'll first need to create a Google project and provide Wasp with your client key and secret. Here is how to do so:
|
||||
|
||||
1. Create a Google Cloud Platform account if you do not already have one: https://cloud.google.com/
|
||||
2. Create and configure a new Google project here: https://console.cloud.google.com/home/dashboard
|
||||
|
||||
![Google Console Screenshot 1](../../static/img/integrations-google-1.jpg)
|
||||
|
||||
![Google Console Screenshot 2](../../static/img/integrations-google-2.jpg)
|
||||
|
||||
3. Search for `OAuth` in the top bar, click on `OAuth consent screen`
|
||||
|
||||
![Google Console Screenshot 3](../../static/img/integrations-google-3.jpg)
|
||||
|
||||
- Select what type of app you want, we will go External
|
||||
|
||||
![Google Console Screenshot 4](../../static/img/integrations-google-4.jpg)
|
||||
|
||||
- Fill out applicable information on Page 1
|
||||
|
||||
![Google Console Screenshot 5](../../static/img/integrations-google-5.jpg)
|
||||
|
||||
- On Page 2, Scopes, you should select `userinfo.profile`. You can optionally search for other things, like `email`.
|
||||
|
||||
![Google Console Screenshot 6](../../static/img/integrations-google-6.jpg)
|
||||
|
||||
![Google Console Screenshot 7](../../static/img/integrations-google-7.jpg)
|
||||
|
||||
![Google Console Screenshot 8](../../static/img/integrations-google-8.jpg)
|
||||
|
||||
- Add any test users you want on Page 3
|
||||
|
||||
![Google Console Screenshot 9](../../static/img/integrations-google-9.jpg)
|
||||
|
||||
4. Next, click `Credentials`
|
||||
|
||||
![Google Console Screenshot 10](../../static/img/integrations-google-10.jpg)
|
||||
|
||||
- Select `+ Create Credentials`
|
||||
- Select `OAuth client ID`
|
||||
|
||||
![Google Console Screenshot 11](../../static/img/integrations-google-11.jpg)
|
||||
|
||||
- Complete the form
|
||||
|
||||
![Google Console Screenshot 12](../../static/img/integrations-google-12.jpg)
|
||||
|
||||
- Under Authorized redirect URIs, put in: `http://localhost:3000/auth/login/google`
|
||||
|
||||
![Google Console Screenshot 13](../../static/img/integrations-google-13.jpg)
|
||||
|
||||
- Once you know on which URL(s) your API server will be deployed, also add those URL(s)
|
||||
- For example: `https://someotherhost.com/auth/login/google`
|
||||
- When you save, you can click the Edit icon and your credentials will be shown
|
||||
|
||||
![Google Console Screenshot 14](../../static/img/integrations-google-14.jpg)
|
||||
|
||||
5. Copy your Client ID and Client secret, and expose them as environment variables named `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in your `.env.server` file.
|
||||
6. Now when your user logs in with Google, you can access the logged in user on the client via the `useAuth()` hook, and on the server via the `context.user` object as described [here](/docs/language/features#accessing-the-currently-logged-in-user)!
|
23
web/docs/introduction/editor-setup.md
Normal file
23
web/docs/introduction/editor-setup.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Editor Setup
|
||||
slug: /editor-setup
|
||||
---
|
||||
|
||||
:::note
|
||||
This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the [Quick Start](/docs/quick-start) guide.
|
||||
:::
|
||||
|
||||
Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.
|
||||
|
||||
## VSCode
|
||||
|
||||
Currently, Wasp only supports integration with VSCode. Install the [Wasp language extension](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp) to get syntax highlighting and integration with the Wasp language server.
|
||||
|
||||
The extension enables:
|
||||
- syntax highlighting for `.wasp` files
|
||||
- scaffolding of new project files
|
||||
- code completion
|
||||
- diagnostics (errors and warnings)
|
||||
- go to definition
|
||||
|
||||
and more!
|
@ -1,13 +1,17 @@
|
||||
---
|
||||
title: Quick Start
|
||||
slug: /quick-start
|
||||
next: /tutorials/todo-app
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
## Installation
|
||||
|
||||
:::tip Try Wasp Without Installing 🤔?
|
||||
Give Wasp a spin in the browser without any setup by running our [Wasp Template for Gitpod](https://github.com/wasp-lang/gitpod-template)
|
||||
:::
|
||||
|
||||
|
||||
Welcome, new Waspeteer 🐝!
|
||||
|
||||
To install Wasp on Linux / OSX / WSL(Win), open your terminal and run:
|
||||
@ -16,27 +20,31 @@ To install Wasp on Linux / OSX / WSL(Win), open your terminal and run:
|
||||
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
ℹ️ Wasp requires `node` and will warn you if it is missing: check below for [more details](#requirements).
|
||||
ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for [more details](#requirements).
|
||||
|
||||
Then, create a new app by running:
|
||||
|
||||
```shell
|
||||
wasp new # Enter the project and choose the template
|
||||
wasp new
|
||||
```
|
||||
and then run the app:
|
||||
|
||||
```shell
|
||||
cd <my-project-name>
|
||||
wasp start # Serves the web app.
|
||||
wasp start
|
||||
```
|
||||
|
||||
That's it :tada:! You have successfully created and served a new web app at <http://localhost:3000> and Wasp is serving both frontend and backend for you.
|
||||
That's it 🎉 You have successfully created and served a new web app at <http://localhost:3000> and Wasp is serving both frontend and backend for you.
|
||||
|
||||
:::info
|
||||
Anything went wrong, or you have additional questions? Check [More Details](#more-details) section below!
|
||||
:::note Something Unclear?
|
||||
Check [More Details](#more-details) section below if anything went wrong, or if you have additional questions.
|
||||
:::
|
||||
|
||||
|
||||
### What next?
|
||||
|
||||
- [ ] 👉 **Check out the [Todo App tutorial](tutorials/todo-app.md) , which will take you through all the core features of Wasp!** 👈
|
||||
- [ ] If you are using VSCode, install our [Wasp language extension](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp).
|
||||
- [ ] 👉 **Check out the [Todo App tutorial](/docs/tutorial/create), which will take you through all the core features of Wasp!** 👈
|
||||
- [ ] [Setup your editor](/docs/editor-setup) for working with Wasp.
|
||||
- [ ] Join us on [Discord](https://discord.gg/rzdnErX)! Any feedback or questions you have, we are there for you.
|
||||
- [ ] Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!
|
||||
|
||||
@ -46,7 +54,7 @@ Anything went wrong, or you have additional questions? Check [More Details](#mor
|
||||
|
||||
### Requirements
|
||||
|
||||
You must have `node` (and `npm`) installed on your machine and available in `PATH`. We rely on the latest Node.js LTS version (currently `v18.14.2`).
|
||||
You must have Node.js (and NPM) installed on your machine and available in `PATH`. We rely on the latest Node.js LTS version (currently `v18.14.2`).
|
||||
|
||||
We recommend using [nvm](https://github.com/nvm-sh/nvm) for managing your Node.js installation version(s).
|
||||
|
||||
@ -56,26 +64,26 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) for managing your Node.j
|
||||
</summary>
|
||||
<div>
|
||||
|
||||
Install nvm via your OS package manager (aptitude, pacman, homebrew, ...) or alternatively via [nvm install script](https://github.com/nvm-sh/nvm#install--update-script).
|
||||
Install nvm via your OS package manager (`apt`, `pacman`, `homebrew`, ...) or via the [nvm](https://github.com/nvm-sh/nvm#install--update-script) install script.
|
||||
|
||||
Then, install a version of node that you need, e.g.:
|
||||
Then, install a version of Node.js that you need:
|
||||
```shell
|
||||
nvm install 18
|
||||
```
|
||||
|
||||
Finally, whenever you need to ensure a specific version of node is used, run e.g.
|
||||
Finally, whenever you need to ensure a specific version of Node.js is used, run:
|
||||
```shell
|
||||
nvm use 18
|
||||
```
|
||||
to set the node version for the current shell session.
|
||||
to set the Node.js version for the current shell session.
|
||||
|
||||
You can run
|
||||
```shell
|
||||
node -v
|
||||
```
|
||||
to check the version of node currently being used in this shell session.
|
||||
to check the version of Node.js currently being used in this shell session.
|
||||
|
||||
Check NVM repo for more details: https://github.com/nvm-sh/nvm .
|
||||
Check NVM repo for more details: https://github.com/nvm-sh/nvm.
|
||||
|
||||
</div>
|
||||
</details>
|
||||
@ -86,13 +94,12 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) for managing your Node.j
|
||||
<Tabs
|
||||
defaultValue='linux/osx'
|
||||
values={[
|
||||
{label: 'Linux / OS X', value: 'linux/osx'},
|
||||
{label: 'Linux / macOS', value: 'linux/osx'},
|
||||
{label: 'Windows', value: 'win'},
|
||||
{label: 'From source', value: 'source'}
|
||||
]}
|
||||
>
|
||||
<TabItem value='linux/osx' >
|
||||
<div style={{borderLeft: 'solid 6px #bf9900', paddingLeft: '10px'}} >
|
||||
<TabItem value='linux/osx'>
|
||||
|
||||
Open your terminal and run:
|
||||
|
||||
@ -100,39 +107,29 @@ Open your terminal and run:
|
||||
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
</div>
|
||||
</TabItem>
|
||||
|
||||
<TabItem value='win'>
|
||||
<div style={{borderLeft: 'solid 6px #bf9900', paddingLeft: '10px'}} >
|
||||
|
||||
With Wasp for Windows, we are almost there: Wasp is successfully compiling and running on Windows but there is a bug or two stopping it from fully working. Check it out [here](https://github.com/wasp-lang/wasp/issues/48) if you are interested in helping.
|
||||
|
||||
In the meantime, the best way to start using Wasp on Windows is by using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). Once you set up Ubuntu on WSL, just follow Linux instructions for installing Wasp. If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX) - we have some community members using WSL that might be able to help you.
|
||||
|
||||
:::caution
|
||||
If you are using WSL2, make sure that your Wasp project is not on Windows file system, but instead on Linux file system. Otherwise, Wasp won't be able to detect file changes, due to the [issue in WSL2](https://github.com/microsoft/WSL/issues/4739).
|
||||
If you are using WSL2, make sure that your Wasp project is not on the Windows file system, but instead on the Linux file system. Otherwise, Wasp won't be able to detect file changes, due to the [issue in WSL2](https://github.com/microsoft/WSL/issues/4739).
|
||||
:::
|
||||
|
||||
</div>
|
||||
</TabItem>
|
||||
|
||||
<TabItem value='source'>
|
||||
<div style={{borderLeft: 'solid 6px #bf9900', paddingLeft: '10px'}} >
|
||||
|
||||
If the installer is not working for you or your OS is not supported, you can try building Wasp from source.
|
||||
If the installer is not working for you or your OS is not supported, you can try building Wasp from the source.
|
||||
|
||||
To install from source, you need to clone the [wasp repo](https://github.com/wasp-lang/wasp), install [cabal](https://cabal.readthedocs.io/en/stable/getting-started.html) on your machine and then run `cabal install` from the `waspc/` dir.
|
||||
To install from source, you need to clone the [wasp repo](https://github.com/wasp-lang/wasp), install [Cabal](https://cabal.readthedocs.io/en/stable/getting-started.html) on your machine and then run `cabal install` from the `waspc/` dir.
|
||||
|
||||
If you have never built Wasp before, this might take some time due to `cabal` downloading dependencies for the first time.
|
||||
|
||||
Check [waspc/](https://github.com/wasp-lang/wasp/tree/main/waspc) for more details on building.
|
||||
Check [waspc/](https://github.com/wasp-lang/wasp/tree/main/waspc) for more details on building Wasp from the source.
|
||||
|
||||
</div>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
:::tip Try Wasp Without Installing 🤔?
|
||||
Give Wasp a spin in the browser without any setup by running our [Wasp Template for Gitpod](https://github.com/wasp-lang/gitpod-template)
|
||||
:::
|
||||
|
@ -170,7 +170,7 @@ export function HomePage({ user }: { user: User }) {
|
||||
|
||||
And voila! We are listing all the recipes in our app 🎉
|
||||
|
||||
This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the [Todo app tutorial](/docs/tutorials/todo-app).
|
||||
This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the [Todo app tutorial](/docs/tutorial/create).
|
||||
|
||||
:::note
|
||||
Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.
|
||||
|
@ -3,6 +3,9 @@ title: Features
|
||||
---
|
||||
|
||||
import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
import OldDocsNote from '@site/docs/OldDocsNote'
|
||||
|
||||
<OldDocsNote />
|
||||
|
||||
## App
|
||||
|
||||
@ -90,7 +93,7 @@ If set to `true`, only authenticated users will be able to access this page. Una
|
||||
|
||||
If `authRequired` is set to `true`, the React component of a page (specified by `component` property) will be provided `user` object as a prop.
|
||||
|
||||
Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/06-auth#updating-main-page-to-check-if-user-is-authenticated) for an example of usage.
|
||||
Check out this [section of our Todo app tutorial](/docs/tutorial/auth#update-the-main-page-to-require-auth) for an example of usage.
|
||||
|
||||
## Route
|
||||
|
||||
@ -1178,7 +1181,7 @@ List of authentication methods that Wasp app supports. Currently supported metho
|
||||
|
||||
#### `onAuthFailedRedirectTo: String` (required)
|
||||
Path where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page).
|
||||
Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/06-auth#updating-main-page-to-check-if-user-is-authenticated) to see an example of usage.
|
||||
Check out this [section of our Todo app tutorial](/docs/tutorial/auth#update-the-main-page-to-require-auth) to see an example of usage.
|
||||
|
||||
#### `onAuthSucceededRedirectTo: String` (optional)
|
||||
Path where a successfully authenticated user will be sent upon successful login/signup.
|
||||
@ -1230,7 +1233,7 @@ The quickest way to get started is by using the following API generated by Wasp:
|
||||
- `useAuth()` React hook
|
||||
**NOTE:** If the signup is successful, the Signup form will automatically log in the user.
|
||||
|
||||
Check our [Todo app tutorial](/docs/tutorials/todo-app/06-auth) to see how it works. See below for detailed specification of each of these methods.
|
||||
Check our [Todo app tutorial](/docs/tutorial/auth) to see how it works. See below for detailed specification of each of these methods.
|
||||
|
||||
#### Lower-level API
|
||||
|
||||
@ -2315,20 +2318,20 @@ We support multiple different providers for sending e-mails: `SMTP`, `SendGrid`
|
||||
|
||||
SMTP e-mail sender uses your SMTP server to send e-mails.
|
||||
|
||||
Read [our guide](/docs/guides/sending-emails#using-the-smtp-provider) for setting up SMTP for more details.
|
||||
Read [our guide](/docs/advanced/email#using-the-smtp-provider) for setting up SMTP for more details.
|
||||
|
||||
|
||||
### SendGrid
|
||||
|
||||
SendGrid is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.
|
||||
|
||||
Check out [our guide](/docs/guides/sending-emails#using-the-sendgrid-provider) for setting up Sendgrid for more details.
|
||||
Check out [our guide](/docs/advanced/email#using-the-sendgrid-provider) for setting up Sendgrid for more details.
|
||||
|
||||
### Mailgun
|
||||
|
||||
Mailgun is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.
|
||||
|
||||
Check out [our guide](/docs/guides/sending-emails#using-the-mailgun-provider) for setting up Mailgun for more details.
|
||||
Check out [our guide](/docs/advanced/email#using-the-mailgun-provider) for setting up Mailgun for more details.
|
||||
|
||||
#### `defaultSender: EmailFromField` (optional)
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
Wasp is a declarative language that recognizes web application-specific terms (e.g. *page* or *route*) as
|
||||
words (types) of the language.
|
||||
|
||||
The basic idea is that the higher-level overview of an app (e.g. pages, routes, database model, ...) is defined in `*.wasp` files (for now just one), while the specific parts (web components, back-end queries, ...) are implemented in specific non-Wasp technologies (React, NodeJS, Prisma) and then referenced in the `*.wasp` files.
|
||||
|
||||
The basic structure of a Wasp project is:
|
||||
- `*.wasp` file
|
||||
- The `src/` folder
|
||||
|
||||
The `src/` folder contains non-Wasp code (JS, CSS, ...). You can structure it however you want, as long as you put it somewhere inside the correct subfolder:
|
||||
- The `src/server` folder - Contains your server code (i.e., executed by Node JS).
|
||||
- The `src/client` folder - Contains your client code (i.e., executed in the browser).
|
||||
- The `src/shared` folder - Contains the code you want to share between the server and the client (e.g., utility functions).
|
||||
|
||||
When referencing code from `src/server` in your `*.wasp` file, you do it as `@server/relative/path/of/file/in/the/server/dir`.
|
||||
|
||||
When referencing code from `src/client` in your `*.wasp` file, you do it as `@client/relative/path/of/file/in/the/client/dir`.
|
||||
|
||||
You can't reference shared code inside the Wasp file, but you can import and use it in all code that lives in `src/client` or `src/server`. Use a relative import to do this. For example, the file `src/server/something.js` can import a shared function from `src/shared/utilities.js` like this:
|
||||
```js
|
||||
import someFunction from '../shared/utilities.js'
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
|
||||
# Simple example
|
||||
We're omitting all pre-generated none-code files Wasp needs to function (e.g., `tsconfig.json` and `.wasproot`) and are focusing only on the files you would write yourself:
|
||||
```yaml
|
||||
TodoApp/
|
||||
- main.wasp
|
||||
- src/
|
||||
- server/
|
||||
- operations.js
|
||||
- client/
|
||||
- pages/
|
||||
- Main.jsx
|
||||
- shared/
|
||||
```
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.6.0"
|
||||
},
|
||||
|
||||
title: "ToDo App"
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import Main from "@client/pages/Main"
|
||||
}
|
||||
|
||||
query getTasks {
|
||||
fn: import { getTasks } from "@server/operations.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/operations.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
```
|
||||
|
||||
You can check out a full working example [here](https://github.com/wasp-lang/wasp/tree/main/waspc/examples/todoApp).
|
||||
|
||||
Each of the basic language features is explained in the following sections.
|
415
web/docs/project/client-config.md
Normal file
415
web/docs/project/client-config.md
Normal file
@ -0,0 +1,415 @@
|
||||
---
|
||||
title: Client Config
|
||||
---
|
||||
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'
|
||||
|
||||
You can configure the client using the `client` field inside the `app` declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.jsx",
|
||||
setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.tsx",
|
||||
setupFn: import mySetupFunction from "@client/myClientSetupCode.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Root Component
|
||||
|
||||
Wasp gives you the option to define a "wrapper" component for your React app.
|
||||
|
||||
It can be used for a variety of purposes, but the most common ones are:
|
||||
|
||||
- Defining a common layout for your application.
|
||||
- Setting up various providers that your application needs.
|
||||
|
||||
### Defining a Common Layout
|
||||
|
||||
Let's define a common layout for your application:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.jsx",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.tsx",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="src/client/Root.tsx"
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Setting up a Provider
|
||||
|
||||
This is how to set up various providers that your application needs:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.jsx",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default function Root({ children }) {
|
||||
return <Provider store={store}>{children}</Provider>
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.tsx",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx title="src/client/Root.tsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return <Provider store={store}>{children}</Provider>
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
As long as you render the children, you can do whatever you want in your root
|
||||
component.
|
||||
|
||||
Read more about the root component in the [API Reference](#rootcomponent-clientimport).
|
||||
|
||||
## Setup Function
|
||||
|
||||
`setupFn` declares a <ShowForTs>Typescript</ShowForTs><ShowForJs>JavaScript</ShowForJs> function that Wasp executes on the client before everything else.
|
||||
|
||||
### Running Some Code
|
||||
|
||||
We can run any code we want in the setup function.
|
||||
|
||||
For example, here's a setup function that logs a message every hour:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/client/myClientSetupCode.js"
|
||||
export default async function mySetupFunction() {
|
||||
let count = 1
|
||||
setInterval(
|
||||
() => console.log(`You have been online for ${count++} hours.`),
|
||||
1000 * 60 * 60
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/client/myClientSetupCode.ts"
|
||||
export default async function mySetupFunction(): Promise<void> {
|
||||
let count = 1
|
||||
setInterval(
|
||||
() => console.log(`You have been online for ${count++} hours.`),
|
||||
1000 * 60 * 60
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Overriding Default Behaviour for Queries
|
||||
|
||||
:::info
|
||||
You can change the options for a **single** Query using the `options` object, as described [here](/docs/data-model/operations/queries#the-usequery-hook-1).
|
||||
:::
|
||||
|
||||
Wasp's `useQuery` hook uses `react-query`'s `useQuery` hook under the hood. Since `react-query` comes configured with aggressive but sane default options, you most likely won't have to change those defaults for all Queries.
|
||||
|
||||
If you do need to change the global defaults, you can do so inside the client setup function.
|
||||
|
||||
Wasp exposes a `configureQueryClient` hook that lets you configure _react-query_'s `QueryClient` object:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/client/myClientSetupCode.js"
|
||||
import { configureQueryClient } from '@wasp/queryClient'
|
||||
|
||||
export default async function mySetupFunction() {
|
||||
// ... some setup
|
||||
configureQueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
// ... some more setup
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/client/myClientSetupCode.ts"
|
||||
import { configureQueryClient } from '@wasp/queryClient'
|
||||
|
||||
export default async function mySetupFunction(): Promise<void> {
|
||||
// ... some setup
|
||||
configureQueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
// ... some more setup
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Make sure to pass in an object expected by the `QueryClient`'s constructor, as
|
||||
explained in
|
||||
[react-query's docs](https://tanstack.com/query/v4/docs/react/reference/QueryClient).
|
||||
|
||||
Read more about the setup function in the [API Reference](#setupfn-clientimport).
|
||||
|
||||
## API Reference
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.jsx",
|
||||
setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.tsx",
|
||||
setupFn: import mySetupFunction from "@client/myClientSetupCode.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Client has the following options:
|
||||
|
||||
- #### `rootComponent: ClientImport`
|
||||
|
||||
`rootComponent` defines the root component of your client application. It is
|
||||
expected to be a React component, and Wasp will use it to wrap your entire app.
|
||||
It must render its children, which are the actual pages of your application.
|
||||
|
||||
Here's an example of a root component that both sets up a provider and
|
||||
renders a custom layout:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Layout>{children}</Layout>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Layout({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/Root.tsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Layout>{children}</Layout>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- #### `setupFn: ClientImport`
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
`setupFn` declares a Typescript function that Wasp executes on the client
|
||||
before everything else. It is expected to be asynchronous, and
|
||||
Wasp will await its completion before rendering the page. The function takes no
|
||||
arguments, and its return value is ignored.
|
||||
</ShowForTs>
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
`setupFn` declares a JavaScript function that Wasp executes on the client
|
||||
before everything else. It is expected to be asynchronous, and
|
||||
Wasp will await its completion before rendering the page. The function takes no
|
||||
arguments, and its return value is ignored.
|
||||
</ShowForJs>
|
||||
|
||||
You can use this function to perform any custom setup (e.g., setting up
|
||||
client-side periodic jobs).
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/client/myClientSetupCode.js"
|
||||
export default async function mySetupFunction() {
|
||||
// Run some code
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/client/myClientSetupCode.ts"
|
||||
export default async function mySetupFunction(): Promise<void> {
|
||||
// Run some code
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
111
web/docs/project/css-frameworks.md
Normal file
111
web/docs/project/css-frameworks.md
Normal file
@ -0,0 +1,111 @@
|
||||
---
|
||||
title: CSS Frameworks
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
## Tailwind
|
||||
|
||||
To enable support for Tailwind in your project, you need to add two config files — [`tailwind.config.cjs`](https://tailwindcss.com/docs/configuration#configuration-options) and `postcss.config.cjs` — to the root directory.
|
||||
|
||||
With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use [Tailwind CSS directives](https://tailwindcss.com/docs/functions-and-directives#directives) in your CSS and Tailwind classes on your React components.
|
||||
|
||||
```bash title="tree ." {13-14}
|
||||
.
|
||||
├── main.wasp
|
||||
├── src
|
||||
│ ├── client
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ ├── Main.css
|
||||
│ │ ├── MainPage.js
|
||||
│ │ └── waspLogo.png
|
||||
│ ├── server
|
||||
│ │ └── tsconfig.json
|
||||
│ └── shared
|
||||
│ └── tsconfig.json
|
||||
├── postcss.config.cjs
|
||||
└── tailwind.config.cjs
|
||||
```
|
||||
|
||||
:::tip Tailwind not working?
|
||||
If you can not use Tailwind after adding the required config files, make sure to restart `wasp start`. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.
|
||||
:::
|
||||
|
||||
### Enabling Tailwind Step-by-Step
|
||||
|
||||
:::caution
|
||||
Make sure to use the `.cjs` extension for these config files, if you name them with a `.js` extension, Wasp will not detect them.
|
||||
:::
|
||||
|
||||
1. Add `./tailwind.config.cjs`.
|
||||
|
||||
```js title="./tailwind.config.cjs"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [ "./src/**/*.{js,jsx,ts,tsx}" ],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
2. Add `./postcss.config.cjs`.
|
||||
|
||||
```js title="./postcss.config.cjs"
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into `Main.css`.
|
||||
|
||||
```css title="./src/client/Main.css" {1-3}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ... */
|
||||
```
|
||||
|
||||
4. Start using Tailwind 🥳
|
||||
|
||||
```jsx title="./src/client/MainPage.jsx"
|
||||
// ...
|
||||
|
||||
<h1 className="text-3xl font-bold underline">
|
||||
Hello world!
|
||||
</h1>
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
### Adding Tailwind Plugins
|
||||
|
||||
To add Tailwind plugins, add it to [dependencies](/docs/project/dependencies) in your `main.wasp` file and to the plugins list in your `tailwind.config.cjs` file:
|
||||
|
||||
```wasp title="./main.wasp" {4-5}
|
||||
app todoApp {
|
||||
// ...
|
||||
dependencies: [
|
||||
("@tailwindcss/forms", "^0.5.3"),
|
||||
("@tailwindcss/typographjy", "^0.5.7"),
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```js title="./tailwind.config.cjs" {5-6}
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// ...
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
140
web/docs/project/customizing-app.md
Normal file
140
web/docs/project/customizing-app.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Customizing the App
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
|
||||
Each Wasp project can have only one `app` type declaration. It is used to configure your app and its components.
|
||||
|
||||
```wasp
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.11.1"
|
||||
},
|
||||
title: "ToDo App",
|
||||
head: [
|
||||
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the [API Reference](#api-reference).
|
||||
|
||||
### Changing the App Title
|
||||
|
||||
You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the `title` field of your `app` declaration:
|
||||
|
||||
```wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.1"
|
||||
},
|
||||
title: "BookFace"
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Additional Lines to the Head
|
||||
|
||||
If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the `head` field of your `app` declaration.
|
||||
|
||||
An example of adding extra style sheets and scripts:
|
||||
|
||||
```wasp
|
||||
app myApp {
|
||||
wasp: {
|
||||
version: "^0.11.1"
|
||||
},
|
||||
title: "My App",
|
||||
head: [ // optional
|
||||
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
|
||||
"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
|
||||
"<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
```wasp
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.11.1"
|
||||
},
|
||||
title: "ToDo App",
|
||||
head: [
|
||||
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
|
||||
],
|
||||
auth: {
|
||||
// ...
|
||||
},
|
||||
client: {
|
||||
// ...
|
||||
},
|
||||
server: {
|
||||
// ...
|
||||
},
|
||||
db: {
|
||||
// ...
|
||||
},
|
||||
dependencies: [
|
||||
// ...
|
||||
],
|
||||
emailSender: {
|
||||
// ...
|
||||
},
|
||||
webSocket: {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `app` declaration has the following fields:
|
||||
|
||||
- `wasp: dict` <Required />
|
||||
Wasp compiler configuration. It is a dictionary with a single field:
|
||||
|
||||
- `version: string` <Required />
|
||||
|
||||
The version specifies which versions of Wasp are compatible with the app. It should contain a valid [SemVer range](https://github.com/npm/node-semver#ranges)
|
||||
|
||||
:::info
|
||||
For now, the version field only supports caret ranges (i.e., `^x.y.z`). Support for the full specification will come in a future version of Wasp
|
||||
:::
|
||||
|
||||
- `title: string` <Required />
|
||||
|
||||
Title of your app. It will appear in the browser tab, next to the favicon.
|
||||
|
||||
- `head: [string]`
|
||||
|
||||
List of additional lines (e.g. `<link>` or `<script>` tags) to be included in the `<head>` of your HTML document.
|
||||
|
||||
The rest of the fields are covered in dedicated sections of the docs:
|
||||
|
||||
- `auth: dict`
|
||||
|
||||
Authentication configuration. Read more in the [authentication section](/docs/auth/overview) of the docs.
|
||||
|
||||
- `client: dict`
|
||||
|
||||
Configuration for the client side of your app. Read more in the [client configuration section](/docs/project/client-config) of the docs.
|
||||
|
||||
- `server: dict`
|
||||
|
||||
Configuration for the server side of your app. Read more in the [server configuration section](/docs/project/server-config) of the docs.
|
||||
|
||||
- `db: dict`
|
||||
|
||||
Database configuration. Read more in the [database configuration section](/docs/data-model/backends) of the docs.
|
||||
|
||||
- `dependencies: [(string, string)]`
|
||||
|
||||
List of npm dependencies for your app. Read more in the [dependencies section](/docs/project/dependencies) of the docs.
|
||||
|
||||
- `emailSender: dict`
|
||||
|
||||
Email sender configuration. Read more in the [email sending section](/docs/advanced/email) of the docs.
|
||||
|
||||
- `webSocket: dict`
|
||||
|
||||
WebSocket configuration. Read more in the [WebSocket section](/docs/advanced/web-sockets) of the docs.
|
32
web/docs/project/dependencies.md
Normal file
32
web/docs/project/dependencies.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Dependencies
|
||||
---
|
||||
|
||||
Specifying npm dependencies in Wasp project is done via the `dependencies` field in the `app` declaration, in the following way:
|
||||
|
||||
```wasp
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
dependencies: [
|
||||
("redux", "^4.0.5"),
|
||||
("react-redux", "^7.1.3")
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You will need to re-run `wasp start` after adding a dependency for Wasp to pick it up.
|
||||
|
||||
The quickest way to find out the latest version of a package is to run:
|
||||
```shell
|
||||
npm view <package-name> version
|
||||
```
|
||||
|
||||
:::note Using Packages that are Already Used by Wasp Internally
|
||||
In the current implementation of Wasp, if Wasp is already internally using a certain npm dependency with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying _a different version_.
|
||||
If you do that, you will get an error message telling you which exact version you have to use for that dependency.
|
||||
This means Wasp _dictates exact versions of certain packages_, so for example you can't choose version of React you want to use.
|
||||
|
||||
|
||||
We are currently working on a restructuring that will solve this and some other quirks that the current dependency system has: check [issue #734](https://github.com/wasp-lang/wasp/issues/734) to follow our progress.
|
||||
:::
|
134
web/docs/project/env-vars.md
Normal file
134
web/docs/project/env-vars.md
Normal file
@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Env Variables
|
||||
---
|
||||
|
||||
**Environment variables** are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.
|
||||
|
||||
For instance, _during development_, you may want your project to connect to a local development database running on your machine, but _in production_, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.
|
||||
|
||||
While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.
|
||||
|
||||
In Wasp, you can use environment variables in both the client and the server code.
|
||||
## Client Env Vars
|
||||
|
||||
Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should **never store secrets in them** (such as secret API keys).
|
||||
|
||||
To enable Wasp to pick them up, client environment variables must be prefixed with `REACT_APP_`, for example: `REACT_APP_SOME_VAR_NAME=...`.
|
||||
|
||||
You can read them from the client code like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/App.js"
|
||||
console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/App.ts"
|
||||
console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
Check below on how to define them.
|
||||
|
||||
## Server Env Vars
|
||||
|
||||
In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as `SOME_VAR_NAME=...`.
|
||||
|
||||
You can read them in the server code like this:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
console.log(process.env.SOME_VAR_NAME)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts
|
||||
console.log(process.env.SOME_VAR_NAME)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Check below on how to define them.
|
||||
|
||||
## Defining Env Vars in Development
|
||||
|
||||
During development, there are two ways to provide env vars to your Wasp project:
|
||||
1. Using `.env` files. **(recommended)**
|
||||
2. Using shell. (useful for overrides)
|
||||
|
||||
### 1. Using .env (dotenv) Files
|
||||
|
||||
![Env vars usage in development](/img/env/prod_dev_fade.svg)
|
||||
|
||||
This is the recommended method for providing env vars to your Wasp project during development.
|
||||
|
||||
In the root of your Wasp project you can create two distinct files:
|
||||
- `.env.server` for env vars that will be provided to the server.
|
||||
|
||||
Variables are defined in these files in the form of `NAME=VALUE`, for example:
|
||||
```shell title=".env.server"
|
||||
DATABASE_URL=postgresql://localhost:5432
|
||||
SOME_VAR_NAME=somevalue
|
||||
```
|
||||
|
||||
- `.env.client` for env vars that will be provided to the client.
|
||||
|
||||
Variables are defined in these files in the form of `NAME=VALUE`, for example:
|
||||
```shell title=".env.client"
|
||||
REACT_APP_SOME_VAR_NAME=somevalue
|
||||
```
|
||||
|
||||
These files should not be committed to version control, and they are already ignored by default in the `.gitignore` file that comes with Wasp.
|
||||
|
||||
<!-- `dotenv` files are a popular method for storing configuration: to learn more about them in general, check out the [README of the lib we use for them](https://github.com/stackbuilders/dotenv-hs). -->
|
||||
|
||||
### 2. Using Shell
|
||||
If you set environment variables in the shell where you run your Wasp commands (e.g., `wasp start`), Wasp will recognize them.
|
||||
|
||||
You can set environment variables in the `.profile` or a similar file, or by defining them at the start of a command:
|
||||
|
||||
```shell
|
||||
SOME_VAR_NAME=SOMEVALUE wasp start
|
||||
```
|
||||
|
||||
This is not specific to Wasp and is simply how environment variables can be set in the shell.
|
||||
|
||||
Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally **overriding** specific environment variables because environment variables set this way **take precedence over those defined in `.env` files**.
|
||||
|
||||
## Defining Env Vars in Production
|
||||
|
||||
While in development, we had the option of using `.env` files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.
|
||||
|
||||
![Env vars usage in development and production](/img/env/prod_dev_fade_2.svg)
|
||||
|
||||
### Client Env Vars
|
||||
|
||||
Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should **never store secrets in them** (such as secret API keys).
|
||||
|
||||
You should provide them to the build command, for example:
|
||||
```shell
|
||||
REACT_APP_SOME_VAR_NAME=somevalue npm run build
|
||||
```
|
||||
|
||||
:::info How it works
|
||||
What happens behind the scenes is that Wasp will replace all occurrences of `import.meta.env.REACT_APP_SOME_VAR_NAME` with the value you provided. This is done during the build process, so the value is embedded into the client code.
|
||||
|
||||
Read more about it in Vite's [docs](https://vitejs.dev/guide/env-and-mode.html#production-replacement).
|
||||
:::
|
||||
|
||||
### Server Env Vars
|
||||
|
||||
The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to [Fly](https://fly.io), you can define them using the `flyctl` CLI tool:
|
||||
|
||||
```shell
|
||||
flyctl secrets set SOME_VAR_NAME=somevalue
|
||||
```
|
||||
|
||||
You can read a lot more details in the [deployment section](/docs/advanced/deployment/manually) of the docs. We go into detail on how to define env vars for each deployment option.
|
250
web/docs/project/server-config.md
Normal file
250
web/docs/project/server-config.md
Normal file
@ -0,0 +1,250 @@
|
||||
---
|
||||
title: Server Config
|
||||
---
|
||||
|
||||
import { ShowForTs, ShowForJs } from "@site/src/components/TsJsHelpers";
|
||||
|
||||
You can configure the behavior of the server via the `server` field of `app` declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
server: {
|
||||
setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
|
||||
middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
server: {
|
||||
setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
|
||||
middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Setup Function
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
`setupFn` declares a Typescript function that will be executed on server start.
|
||||
</ShowForTs>
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
`setupFn` declares a Javascript function that will be executed on server start.
|
||||
</ShowForJs>
|
||||
|
||||
### Adding a Custom Route
|
||||
|
||||
As an example, adding a custom route would look something like:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/myServerSetupCode.ts"
|
||||
export const mySetupFunction = async ({ app }) => {
|
||||
addCustomRoute(app)
|
||||
}
|
||||
|
||||
function addCustomRoute(app) {
|
||||
app.get('/customRoute', (_req, res) => {
|
||||
res.send('I am a custom route')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/myServerSetupCode.ts"
|
||||
import { ServerSetupFn, Application } from '@wasp/types'
|
||||
|
||||
export const mySetupFunction: ServerSetupFn = async ({ app }) => {
|
||||
addCustomRoute(app)
|
||||
}
|
||||
|
||||
function addCustomRoute(app: Application) {
|
||||
app.get('/customRoute', (_req, res) => {
|
||||
res.send('I am a custom route')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Storing Some Values for Later Use
|
||||
|
||||
In case you want to store some values for later use, or to be accessed by the [Operations](/docs/data-model/operations/overview) you do that in the `setupFn` function.
|
||||
|
||||
Dummy example of such function and its usage:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/myServerSetupCode.js"
|
||||
let someResource = undefined
|
||||
|
||||
export const mySetupFunction = async () => {
|
||||
// Let's pretend functions setUpSomeResource and startSomeCronJob
|
||||
// are implemented below or imported from another file.
|
||||
someResource = await setUpSomeResource()
|
||||
startSomeCronJob()
|
||||
}
|
||||
|
||||
export const getSomeResource = () => someResource
|
||||
```
|
||||
|
||||
```js title="src/server/queries.js"
|
||||
import { getSomeResource } from './myServerSetupCode.js'
|
||||
|
||||
...
|
||||
|
||||
export const someQuery = async (args, context) => {
|
||||
const someResource = getSomeResource()
|
||||
return queryDataFromSomeResource(args, someResource)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/myServerSetupCode.ts"
|
||||
import { ServerSetupFn } from '@wasp/types'
|
||||
|
||||
let someResource = undefined
|
||||
|
||||
export const mySetupFunction: ServerSetupFn = async () => {
|
||||
// Let's pretend functions setUpSomeResource and startSomeCronJob
|
||||
// are implemented below or imported from another file.
|
||||
someResource = await setUpSomeResource()
|
||||
startSomeCronJob()
|
||||
}
|
||||
|
||||
export const getSomeResource = () => someResource
|
||||
```
|
||||
|
||||
```ts title="src/server/queries.ts"
|
||||
import { SomeQuery } from '@wasp/queries/types'
|
||||
import { getSomeResource } from './myServerSetupCode.js'
|
||||
|
||||
...
|
||||
|
||||
export const someQuery: SomeQuery<...> = async (args, context) => {
|
||||
const someResource = getSomeResource()
|
||||
return queryDataFromSomeResource(args, someResource)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::note
|
||||
The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.
|
||||
|
||||
This effectively turns your module into a singleton whose construction is performed on server start.
|
||||
:::
|
||||
|
||||
Read more about [server setup function](#setupfn-serverimport) below.
|
||||
|
||||
## Middleware Config Function
|
||||
|
||||
You can configure the global middleware via the `middlewareConfigFn`. This will modify the middleware stack for all operations and APIs.
|
||||
|
||||
Read more about [middleware config function](#middlewareconfigfn-serverimport) below.
|
||||
|
||||
## API Reference
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
server: {
|
||||
setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
|
||||
middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
server: {
|
||||
setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
|
||||
middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`app.server` is a dictionary with the following fields:
|
||||
|
||||
- #### `setupFn: ServerImport`
|
||||
|
||||
`setupFn` declares a <ShowForTs>Typescript</ShowForTs><ShowForJs>Javascript</ShowForJs> function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.
|
||||
|
||||
It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.
|
||||
|
||||
The `setupFn` function receives the `express.Application` and the `http.Server` instances as part of its context. They can be useful for setting up any custom server routes or for example, setting up `socket.io`.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/myServerSetupCode.js"
|
||||
export const mySetupFunction = async () => {
|
||||
await setUpSomeResource()
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
Types for the setup function and its context are as follows:
|
||||
|
||||
```ts title="@wasp/types"
|
||||
export type ServerSetupFn = (context: ServerSetupFnContext) => Promise<void>
|
||||
|
||||
export type ServerSetupFnContext = {
|
||||
app: Application // === express.Application
|
||||
server: Server // === http.Server
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="src/server/myServerSetupCode.ts"
|
||||
import { ServerSetupFn } from '@wasp/types'
|
||||
|
||||
export const mySetupFunction: ServerSetupFn = async () => {
|
||||
await setUpSomeResource()
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- #### `middlewareConfigFn: ServerImport`
|
||||
|
||||
The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the [configuring middleware section](/docs/advanced/middleware-config#1-customize-global-middleware).
|
73
web/docs/project/starter-templates.md
Normal file
73
web/docs/project/starter-templates.md
Normal file
@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Starter Templates
|
||||
---
|
||||
|
||||
We created a few starter templates to help you get started with Wasp. Check out the list [below](#available-templates).
|
||||
|
||||
## Using a Template
|
||||
|
||||
Run `wasp new` to run the interactive mode for creating a new Wasp project.
|
||||
|
||||
It will ask you for the project name, and then for the template to use:
|
||||
|
||||
```
|
||||
$ wasp new
|
||||
Enter the project name (e.g. my-project) ▸ MyFirstProject
|
||||
Choose a starter template
|
||||
[1] basic (default)
|
||||
[2] saas
|
||||
[3] todo-ts
|
||||
▸ 1
|
||||
|
||||
🐝 --- Creating your project from the basic template... ---------------------------
|
||||
|
||||
Created new Wasp app in ./MyFirstProject directory!
|
||||
To run it, do:
|
||||
|
||||
cd MyFirstProject
|
||||
wasp start
|
||||
```
|
||||
|
||||
## Available Templates
|
||||
|
||||
When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.
|
||||
|
||||
### Vector Similarity Search Template
|
||||
|
||||
![Vector Similarity Search Template](/img/starter-templates/embeddings-client.png)
|
||||
|
||||
A template for generating embeddings and performing vector similarity search on your text data!
|
||||
|
||||
**Features:** w/ Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Fullstack Type Safety
|
||||
|
||||
Use this template:
|
||||
|
||||
```
|
||||
wasp new <project-name> -t embeddings
|
||||
```
|
||||
|
||||
### SaaS Template
|
||||
|
||||
![SaaS Template](/img/starter-templates/gptsaastemplate.png)
|
||||
|
||||
A SaaS Template to get your profitable side project started quickly and easily!
|
||||
|
||||
**Features:** w/ Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs
|
||||
|
||||
Use this template:
|
||||
|
||||
```
|
||||
wasp new <project-name> -t saas
|
||||
```
|
||||
|
||||
### Todo App w/ Typescript
|
||||
|
||||
A simple Todo App with Typescript and Fullstack Type Safety.
|
||||
|
||||
**Features:** Auth (username/password), Fullstack Type Safety
|
||||
|
||||
Use this template:
|
||||
|
||||
```
|
||||
wasp new <project-name> -t todo-ts
|
||||
```
|
68
web/docs/project/static-assets.md
Normal file
68
web/docs/project/static-assets.md
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Static Asset Handling
|
||||
---
|
||||
|
||||
import { ShowForJs, ShowForTs } from '@site/src/components/TsJsHelpers'
|
||||
|
||||
## Importing Asset as URL
|
||||
|
||||
Importing a static asset (e.g. an image) will return its URL. For example:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/App.jsx"
|
||||
import imgUrl from './img.png'
|
||||
|
||||
function App() {
|
||||
return <img src={imgUrl} alt="img" />
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```jsx title="src/client/App.tsx"
|
||||
import imgUrl from './img.png'
|
||||
|
||||
function App() {
|
||||
return <img src={imgUrl} alt="img" />
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
For example, `imgUrl` will be `/img.png` during development, and become `/assets/img.2d8efhg.png` in the production build.
|
||||
|
||||
This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.
|
||||
|
||||
We are using Vite under the hood, read more about importing static assets in Vite's [docs](https://vitejs.dev/guide/assets.html#importing-asset-as-url).
|
||||
|
||||
## The `public` Directory
|
||||
|
||||
If you have assets that are:
|
||||
|
||||
- Never referenced in source code (e.g. robots.txt)
|
||||
- Must retain the exact same file name (without hashing)
|
||||
- ...or you simply don't want to have to import an asset first just to get its URL
|
||||
|
||||
Then you can place the asset in a special `public` directory in the `client` folder:
|
||||
|
||||
```
|
||||
src
|
||||
└── client
|
||||
├── public
|
||||
│ ├── favicon.ico
|
||||
│ └── robots.txt
|
||||
└── ...
|
||||
```
|
||||
|
||||
Assets in this directory will be served at root path `/` during dev, and copied to the root of the dist directory as-is.
|
||||
|
||||
For example, if you have a file `favicon.ico` in the `public` directory, and your app is hosted at `https://myapp.com`, it will be made available at `https://myapp.com/favicon.ico`.
|
||||
|
||||
:::info Usage in client code
|
||||
Note that:
|
||||
|
||||
- You should always reference public assets using root absolute path - for example, `src/client/public/icon.png` should be referenced in source code as `/icon.png`.
|
||||
- Assets in the `public` directory **cannot be imported** from <ShowForJs>JavaScript</ShowForJs><ShowForTs>TypeScript</ShowForTs>.
|
||||
:::
|
387
web/docs/project/testing.md
Normal file
387
web/docs/project/testing.md
Normal file
@ -0,0 +1,387 @@
|
||||
---
|
||||
title: Testing
|
||||
---
|
||||
|
||||
:::info
|
||||
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
||||
:::
|
||||
|
||||
## Testing Your React App
|
||||
|
||||
Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses [Vite](https://vitejs.dev/), we support testing web apps through [Vitest](https://vitest.dev/).
|
||||
|
||||
<details>
|
||||
<summary>Included Libraries</summary>
|
||||
<div>
|
||||
|
||||
[`vitest`](https://www.npmjs.com/package/vitest): Unit test framework with native Vite support.
|
||||
|
||||
[`@vitest/ui`](https://www.npmjs.com/package/@vitest/ui): A nice UI for seeing your test results.
|
||||
|
||||
[`jsdom`](https://www.npmjs.com/package/jsdom): A web browser test environment for Node.js.
|
||||
|
||||
[`@testing-library/react`](https://www.npmjs.com/package/@testing-library/react) / [`@testing-library/jest-dom`](https://www.npmjs.com/package/@testing-library/jest-dom): Testing helpers.
|
||||
|
||||
[`msw`](https://www.npmjs.com/package/msw): A server mocking library.
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
### Writing Tests
|
||||
|
||||
For Wasp to pick up your tests, they should be placed within the `src/client` directory and use an extension that matches [these glob patterns](https://vitest.dev/config#include). Some of the file names that Wasp will pick up as tests:
|
||||
|
||||
- `yourFile.test.ts`
|
||||
- `YourComponent.spec.jsx`
|
||||
|
||||
Within test files, you can import your other source files as usual. For example, if you have a component `Counter.jsx`, you test it by creating a file in the same directory called `Counter.test.jsx` and import the component with `import Counter from './Counter'`.
|
||||
|
||||
### Running Tests
|
||||
|
||||
Running `wasp test client` will start Vitest in watch mode and recompile your Wasp project when changes are made.
|
||||
|
||||
- If you want to see a realtime UI, pass `--ui` as an option.
|
||||
- To run the tests just once, use `wasp test client run`.
|
||||
|
||||
All arguments after `wasp test client` are passed directly to the Vitest CLI, so check out [their documentation](https://vitest.dev/guide/cli.html) for all of the options.
|
||||
|
||||
:::warning Be Careful
|
||||
You should not run `wasp test` while `wasp start` is running. Both will try to compile your project to `.wasp/out`.
|
||||
:::
|
||||
|
||||
### React Testing Helpers
|
||||
|
||||
Wasp provides several functions to help you write React tests:
|
||||
|
||||
- `renderInContext`: Takes a React component, wraps it inside a `QueryClientProvider` and `Router`, and renders it. This is the function you should use to render components in your React component tests.
|
||||
|
||||
```js
|
||||
import { renderInContext } from "@wasp/test";
|
||||
|
||||
renderInContext(<MainPage />);
|
||||
```
|
||||
|
||||
- `mockServer`: Sets up the mock server and returns an object containing the `mockQuery` and `mockApi` utilities. This should be called outside of any test case, in each file that wants to use those helpers.
|
||||
|
||||
```js
|
||||
import { mockServer } from "@wasp/test";
|
||||
|
||||
const { mockQuery, mockApi } = mockServer();
|
||||
```
|
||||
|
||||
- `mockQuery`: Takes a Wasp [query](/docs/data-model/operations/queries) to mock and the JSON data it should return.
|
||||
|
||||
```js
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
|
||||
mockQuery(getTasks, []);
|
||||
```
|
||||
|
||||
- Helpful when your component uses `useQuery`.
|
||||
- Behind the scenes, Wasp uses [`msw`](https://npmjs.com/package/msw) to create a server request handle that responds with the specified data.
|
||||
- Mock are cleared between each test.
|
||||
|
||||
- `mockApi`: Similar to `mockQuery`, but for [APIs](/docs/advanced/apis). Instead of a Wasp query, it takes a route containing an HTTP method and a path.
|
||||
|
||||
```js
|
||||
import { HttpMethod } from "@wasp/types";
|
||||
|
||||
mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });
|
||||
```
|
||||
|
||||
## Testing Your Server-Side Code
|
||||
|
||||
Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at [this GitHub issue](https://github.com/wasp-lang/wasp/issues/110) and express your interest by commenting.
|
||||
|
||||
## Examples
|
||||
|
||||
You can see some tests in a Wasp project [here](https://github.com/wasp-lang/wasp/blob/release/waspc/examples/todoApp/src/client/pages/auth/helpers.test.ts).
|
||||
|
||||
### Client Unit Tests
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/client/helpers.js"
|
||||
export function areThereAnyTasks(tasks) {
|
||||
return tasks.length === 0;
|
||||
}
|
||||
```
|
||||
|
||||
```js title="src/client/helpers.test.js"
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
import { areThereAnyTasks } from "./helpers";
|
||||
|
||||
test("areThereAnyTasks", () => {
|
||||
expect(areThereAnyTasks([])).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/client/helpers.ts"
|
||||
import { Task } from "@wasp/entities";
|
||||
|
||||
export function areThereAnyTasks(tasks: Task[]): boolean {
|
||||
return tasks.length === 0;
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="src/client/helpers.test.ts"
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
import { areThereAnyTasks } from "./helpers";
|
||||
|
||||
test("areThereAnyTasks", () => {
|
||||
expect(areThereAnyTasks([])).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### React Component Tests
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/Todo.jsx"
|
||||
import { useQuery } from "@wasp/queries";
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
|
||||
const Todo = (_props) => {
|
||||
const { data: tasks } = useQuery(getTasks);
|
||||
return (
|
||||
<ul>
|
||||
{tasks &&
|
||||
tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<input type="checkbox" value={task.isDone} />
|
||||
{task.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```js title=src/client/Todo.test.jsx
|
||||
import { test, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { mockServer, renderInContext } from "@wasp/test";
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
import Todo from "./Todo";
|
||||
|
||||
const { mockQuery } = mockServer();
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 1,
|
||||
description: "test todo 1",
|
||||
isDone: true,
|
||||
userId: 1,
|
||||
},
|
||||
];
|
||||
|
||||
test("handles mock data", async () => {
|
||||
mockQuery(getTasks, mockTasks);
|
||||
|
||||
renderInContext(<Todo />);
|
||||
|
||||
await screen.findByText("test todo 1");
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||
|
||||
screen.debug();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/Todo.tsx"
|
||||
import { useQuery } from "@wasp/queries";
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
|
||||
const Todo = (_props: {}) => {
|
||||
const { data: tasks } = useQuery(getTasks);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tasks &&
|
||||
tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<input type="checkbox" value={task.isDone} />
|
||||
{task.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```tsx title=src/client/Todo.test.tsx
|
||||
import { test, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { mockServer, renderInContext } from "@wasp/test";
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
import Todo from "./Todo";
|
||||
|
||||
const { mockQuery } = mockServer();
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 1,
|
||||
description: "test todo 1",
|
||||
isDone: true,
|
||||
userId: 1,
|
||||
},
|
||||
];
|
||||
|
||||
test("handles mock data", async () => {
|
||||
mockQuery(getTasks, mockTasks);
|
||||
|
||||
renderInContext(<Todo />);
|
||||
|
||||
await screen.findByText("test todo 1");
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||
|
||||
screen.debug();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Testing With Mocked APIs
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/Todo.jsx"
|
||||
import api from "@wasp/api";
|
||||
|
||||
const Todo = (_props) => {
|
||||
const [tasks, setTasks] = useState([]);
|
||||
useEffect(() => {
|
||||
api
|
||||
.get("/tasks")
|
||||
.then((res) => res.json())
|
||||
.then((tasks) => setTasks(tasks))
|
||||
.catch((err) => window.alert(err));
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tasks &&
|
||||
tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<input type="checkbox" value={task.isDone} />
|
||||
{task.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```jsx title=src/client/Todo.test.jsx
|
||||
import { test, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { mockServer, renderInContext } from "@wasp/test";
|
||||
import Todo from "./Todo";
|
||||
|
||||
const { mockApi } = mockServer();
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 1,
|
||||
description: "test todo 1",
|
||||
isDone: true,
|
||||
userId: 1,
|
||||
},
|
||||
];
|
||||
|
||||
test("handles mock data", async () => {
|
||||
mockApi("/tasks", { res: mockTasks });
|
||||
|
||||
renderInContext(<Todo />);
|
||||
|
||||
await screen.findByText("test todo 1");
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||
|
||||
screen.debug();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/Todo.tsx"
|
||||
import { Task } from "@wasp/entities";
|
||||
import api from "@wasp/api";
|
||||
|
||||
const Todo = (_props: {}) => {
|
||||
const [tasks, setTasks] = useState<Task>([]);
|
||||
useEffect(() => {
|
||||
api
|
||||
.get("/tasks")
|
||||
.then((res) => res.json() as Task[])
|
||||
.then((tasks) => setTasks(tasks))
|
||||
.catch((err) => window.alert(err));
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tasks &&
|
||||
tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<input type="checkbox" value={task.isDone} />
|
||||
{task.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```tsx title=src/client/Todo.test.tsx
|
||||
import { test, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { mockServer, renderInContext } from "@wasp/test";
|
||||
import Todo from "./Todo";
|
||||
|
||||
const { mockApi } = mockServer();
|
||||
|
||||
const mockTasks = [
|
||||
{
|
||||
id: 1,
|
||||
description: "test todo 1",
|
||||
isDone: true,
|
||||
userId: 1,
|
||||
},
|
||||
];
|
||||
|
||||
test("handles mock data", async () => {
|
||||
mockApi("/tasks", mockTasks);
|
||||
|
||||
renderInContext(<Todo />);
|
||||
|
||||
await screen.findByText("test todo 1");
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||
|
||||
screen.debug();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
65
web/docs/tutorial/01-create.md
Normal file
65
web/docs/tutorial/01-create.md
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
title: 1. Creating a New Project
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
:::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](/docs/quick-start) guide!
|
||||
:::
|
||||
|
||||
In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.
|
||||
|
||||
<img alt="How Todo App will work once it is done"
|
||||
src={useBaseUrl('img/todo-app-tutorial-intro.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
If you get stuck at any point (or just want to chat), reach out to us on [Discord](https://discord.gg/rzdnErX) and we will help you!
|
||||
|
||||
You can find the complete code of the app we're about to build [here](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp).
|
||||
|
||||
:::tip See Wasp In Action
|
||||
Prefer videos? We have a YouTube tutorial whick walks you through building this Todo app step by step. [Check it out here!](https://youtu.be/R8uOu6ZEr5s).
|
||||
|
||||
We've also set up an in-browser dev environment for you on Gitpod which allows you to view and edit the completed app with no installation required.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitpod.io/#https://github.com/wasp-lang/gitpod-template">
|
||||
<img src="https://gitpod.io/button/open-in-gitpod.svg" />
|
||||
</a>
|
||||
</p>
|
||||
:::
|
||||
|
||||
## Creating a Project
|
||||
|
||||
To setup a new Wasp proejct, run the following command in your terminal
|
||||
|
||||
```sh
|
||||
$ wasp new TodoApp
|
||||
```
|
||||
|
||||
Enter the newly created directory and start the development server:
|
||||
|
||||
```sh
|
||||
$ cd TodoApp
|
||||
$ wasp start
|
||||
```
|
||||
|
||||
:::note
|
||||
`wasp start` will take a bit of time to start the server the first time you run it in a new project.
|
||||
:::
|
||||
|
||||
You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at `http://localhost:3000` with a simple placeholder plage:
|
||||
|
||||
<img alt="Screenshot of new Wasp app"
|
||||
src={useBaseUrl('img/wasp-new-screenshot.png')}
|
||||
height="400px"
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.
|
114
web/docs/tutorial/02-project-structure.md
Normal file
114
web/docs/tutorial/02-project-structure.md
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
title: 2. Project Structure
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
After creating a new Wasp project, you'll get a file structure that looks like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── .gitignore
|
||||
├── main.wasp # Your Wasp code goes here.
|
||||
├── src
|
||||
│ ├── client # Your client code (JS/CSS/HTML) goes here.
|
||||
│ │ ├── Main.css
|
||||
│ │ ├── MainPage.jsx
|
||||
│ │ ├── vite-env.d.ts
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ └── waspLogo.png
|
||||
│ ├── server # Your server code (Node JS) goes here.
|
||||
│ │ └── tsconfig.json
|
||||
│ ├── shared # Your shared (runtime independent) code goes here.
|
||||
│ │ └── tsconfig.json
|
||||
│ └── .waspignore
|
||||
└── .wasproot
|
||||
```
|
||||
|
||||
By _your code_, we mean the _"the code you write"_, as opposed to the code generated by Wasp. Wasp expects you to separate all of your code—which we call external code—into three folders to make it obvious how each file is executed:
|
||||
|
||||
- `src/client`: Contains the code executed on the client, in the browser.
|
||||
- `src/server`: Contains the code executed on the server, with Node.
|
||||
- `src/shared`: Contains code that may be executed on both the client and server.
|
||||
|
||||
Many of the other files (`tsconfig.json`, `vite-env.d.ts`, etc.) are used by your IDE to improve your development experience with tools like autocompletion, intellisense, and error reporting.
|
||||
|
||||
:::note TypeScript Support
|
||||
Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.
|
||||
|
||||
We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla 🍦 JavaScript and TypeScript.
|
||||
:::
|
||||
|
||||
The most important file in the project is `main.wasp`. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.
|
||||
|
||||
Let's look a bit closer at `main.wasp.`
|
||||
|
||||
## `main.wasp`
|
||||
|
||||
This file, written in our Wasp configuration language, defines your app and lets Wasp take care a ton of features to your app for you. The file contains several _declarations_ which, together, describe all the components of your app.
|
||||
|
||||
The default Wasp file generated via `wasp new` on the previous page looks like:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.11.0" // Pins the version of Wasp to use.
|
||||
},
|
||||
title: "Todo app" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
|
||||
page MainPage {
|
||||
// We specify that the React implementation of the page is the default export
|
||||
// of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
|
||||
// Use `@client` to reference files inside the `src/client` folder.
|
||||
component: import Main from "@client/MainPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.11.0" // Pins the version of Wasp to use.
|
||||
},
|
||||
title: "Todo app" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
|
||||
page MainPage {
|
||||
// We specify that the React implementation of the page is the default export
|
||||
// of `src/client/MainPage.tsx`. This statement uses standard JS import syntax.
|
||||
// Use `@client` to reference files inside the `src/client` folder.
|
||||
component: import Main from "@client/MainPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
:::caution Using TypeScript
|
||||
The default project uses JavaScript. In order to use TypeScript, you'll need to rename `MainPage.jsx` to `MainPage.tsx` and update the import statement in the Wasp file.
|
||||
:::
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
This file uses three declaration types:
|
||||
|
||||
- **app**: Top-level configuration information about your app.
|
||||
|
||||
- **route**: Describes which path each page should be accessible from.
|
||||
|
||||
- **page**: Defines a web page and the React component that will be rendered when the page is loaded.
|
||||
|
||||
In the next section, we'll explore how **route** and **page** work together to build your web app.
|
219
web/docs/tutorial/03-pages.md
Normal file
219
web/docs/tutorial/03-pages.md
Normal file
@ -0,0 +1,219 @@
|
||||
---
|
||||
title: 3. Pages & Routes
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
In the default `main.wasp` file created by `wasp new`, there is a **page** and a **route** declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
|
||||
page MainPage {
|
||||
// We specify that the React implementation of the page is the default export
|
||||
// of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
|
||||
// Use `@client` to reference files inside the `src/client` folder.
|
||||
component: import Main from "@client/MainPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
|
||||
page MainPage {
|
||||
// We specify that the React implementation of the page is the default export
|
||||
// of `src/client/MainPage.tsx`. This statement uses standard JS import syntax.
|
||||
// Use `@client` to reference files inside the `src/client` folder.
|
||||
component: import Main from "@client/MainPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Together, these declarations tell Wasp that when a user navigates to `/`, it should render the default export from `src/client/MainPage`.
|
||||
|
||||
## The MainPage Component
|
||||
|
||||
Let's take a look at the React component referenced by the page declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/MainPage.jsx"
|
||||
import waspLogo from './waspLogo.png'
|
||||
import './Main.css'
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
}
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/MainPage.tsx"
|
||||
import waspLogo from './waspLogo.png'
|
||||
import './Main.css'
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
}
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Since Wasp uses React for the frontend, this is a normal functional React component. It also uses the CSS and logo image that are located next to it in the `src/client` folder.
|
||||
|
||||
That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.
|
||||
|
||||
:::tip
|
||||
`wasp start` automatically picks up the changes you make and restarts the app, so keep it running in the background.
|
||||
:::
|
||||
|
||||
## Adding a Second Page
|
||||
|
||||
To add more pages, you can create another set of **page** and **route** declarations. You can even add parameters to the URL path, using the same syntax as [React Router](https://reactrouter.com/web/). Let's test this out by adding a new page:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route HelloRoute { path: "/hello/:name", to: HelloPage }
|
||||
page HelloPage {
|
||||
component: import Hello from "@client/HelloPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route HelloRoute { path: "/hello/:name", to: HelloPage }
|
||||
page HelloPage {
|
||||
component: import Hello from "@client/HelloPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When a user visits `/hello/their-name`, Wasp will render the component exported from `src/client/HelloPage` and pass the URL parameter the same way as in React Router:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/HelloPage.jsx"
|
||||
const HelloPage = (props) => {
|
||||
return <div>Here's {props.match.params.name}!</div>
|
||||
}
|
||||
|
||||
export default HelloPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/HelloPage.tsx"
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
const HelloPage = (props: RouteComponentProps<{ name: string }>) => {
|
||||
return <div>Here's {props.match.params.name}!</div>
|
||||
}
|
||||
|
||||
export default HelloPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Now you can visit `/hello/johnny` and see "Here's johnny!"
|
||||
|
||||
## Cleaning Up
|
||||
|
||||
Let's prepare for building the Todo app by cleaning up the project and removing files and code we won't need. Start by deleting `Main.css`, `waspLogo.png`, and `HelloPage.{jsx,tsx}` that we just created in the `src/client/` folder.
|
||||
|
||||
Since we deleted `HelloPage.{jsx,tsx}`, we also need to remember to remove the `route` and `page` declarations we wrote for it. Your Wasp file should now look like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Todo app"
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import Main from "@client/MainPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Todo app"
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import Main from "@client/MainPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Next, we'll remove most of the code from the `MainPage` component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/MainPage.jsx"
|
||||
const MainPage = () => {
|
||||
return <div>Hello world!</div>
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/MainPage.tsx"
|
||||
const MainPage = () => {
|
||||
return <div>Hello world!</div>
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
At this point, the main page should look like this:
|
||||
|
||||
<img alt="Todo App - Hello World"
|
||||
src={useBaseUrl('img/todo-app-hello-world.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
In the next section, we'll start implementing some features of the Todo app!
|
48
web/docs/tutorial/04-entities.md
Normal file
48
web/docs/tutorial/04-entities.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 4. Database Entities
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.
|
||||
|
||||
Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
```
|
||||
|
||||
:::note
|
||||
Wasp uses [Prisma](https://www.prisma.io) as a way to talk to the database. You define entities by defining [Prisma models](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model/) using the Prisma Schema Language (PSL) between the `{=psl psl=}` tags.
|
||||
|
||||
Read more in the [Entities](/docs/data-model/entities) section of the docs.
|
||||
:::
|
||||
|
||||
To update the database schema to include this entity, stop the `wasp start` process, if its running, and run:
|
||||
|
||||
```sh
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.
|
||||
|
||||
To take a look at the database and the new `Task` entity, run:
|
||||
|
||||
```sh
|
||||
wasp db studio
|
||||
```
|
||||
|
||||
This will open a new page in your browser to view and edit the data in your database.
|
||||
|
||||
<img alt="Todo App - Db studio showing Task schema"
|
||||
src={useBaseUrl('img/todo-app-db-studio-task-entity.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
Click on the `Task` entity and check out its fields! We don't have any data in our database yet, but we are about to change that.
|
247
web/docs/tutorial/05-queries.md
Normal file
247
web/docs/tutorial/05-queries.md
Normal file
@ -0,0 +1,247 @@
|
||||
---
|
||||
title: 5. Querying the Database
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using [queries and actions](/docs/data-model/operations/overview), collectively known as _operations_.
|
||||
|
||||
Queries are used to read an entity, while actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a query.
|
||||
|
||||
To list tasks we have to:
|
||||
|
||||
1. Create a query that fetches tasks from the database.
|
||||
2. Update the `MainPage.{jsx,tsx}` to use that query and display the results.
|
||||
|
||||
## Defining the Query
|
||||
|
||||
We'll create a new query called `getTasks`. We'll need to declare the query in the Wasp file and write its implementation in <ShowForJs>JS</ShowForJs><ShowForTs>TS</ShowForTs>.
|
||||
|
||||
### Declaring a Query
|
||||
|
||||
We need to add a **query** declaration to `main.wasp` so that Wasp knows it exists:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getTasks {
|
||||
// Specifies where the implementation for the query function is.
|
||||
// Use `@server` to import files inside the `src/server` folder.
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
// Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
|
||||
// will automatically update the results of this query when tasks are modified.
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getTasks {
|
||||
// Specifies where the implementation for the query function is.
|
||||
// Use `@server` to import files inside the `src/server` folder.
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
// Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
|
||||
// will automatically update the results of this query when tasks are modified.
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
:::warning Importing Typescript files
|
||||
Even though you are using TypeScript and plan to implement this query in `src/server/queries.ts`, you still need to import it using a `.js` extension. Wasp internally uses `esnext` module resolution, which requires importing all files with a `.js` extension. This is only needed when importing `@server@` files.
|
||||
|
||||
Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588).
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Implementing a Query
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
Next, create a new file `src/server/queries.js` and define the JavaScript function we've just imported in our `query` declaration:
|
||||
|
||||
</ShowForJs>
|
||||
<ShowForTs>
|
||||
|
||||
Next, create a new file `src/server/queries.ts` and define the TypeScript function we've just imported in our `query` declaration:
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/queries.js"
|
||||
export const getTasks = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```js title="src/server/queries.ts"
|
||||
import { Task } from '@wasp/entities'
|
||||
import { GetTasks } from '@wasp/queries/types'
|
||||
|
||||
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
```
|
||||
|
||||
Wasp automatically generates the types `GetTasks` and `Task` based the contents of `main.wasp`:
|
||||
|
||||
- `Task` is a type corresponding to the `Task` entity we've defined in `main.wasp`.
|
||||
- `GetTasks` is a generic type Wasp automatically generated based the `getTasks` query we've defined in `main.wasp`.
|
||||
|
||||
You can use these types to specify the Query's input and output types. This query doesn't expect any arguments (its input type is `void`), but it does return an array of tasks (its output type is `Task[]`).
|
||||
|
||||
Annotating the queries is optional, but highly recommended because doing so enables **full-stack type safety**. We'll see what this means in the next step.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Query function parameters:
|
||||
|
||||
- `args`: `object`, arguments the query is given by the caller.
|
||||
- `context`: `object`, information provided by Wasp.
|
||||
|
||||
Since we declared in `main.wasp` that our query uses the `Task` entity, Wasp injected a [Prisma client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud) for the `Task` entity as `context.entities.Task` - we used it above to fetch all the tasks from the database.
|
||||
|
||||
:::info
|
||||
Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the `src/server` folder.
|
||||
:::
|
||||
|
||||
## Invoking the Query On the Frontend
|
||||
|
||||
While we implement queries on the server, Wasp generates client-side functions that automatically takes care of serialization, network calls, and chache invalidation, allowing you to call the server code like it's a regular function. This makes it easy for us to use the `getTasks` query we just created in our React component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {1-2,5-14,17-36} title="src/client/MainPage.jsx"
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && 'Loading...'}
|
||||
{error && 'Error: ' + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {1-3,6-15,18-37} title="src/client/MainPage.tsx"
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && 'Loading...'}
|
||||
{error && 'Error: ' + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }: { tasks: Task[] }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Most of this code is regular React, the only exception being the <ShowForJs>two</ShowForJs><ShowForTs>three</ShowForTs> special `@wasp` imports:
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
- `import getTasks from '@wasp/queries/getTasks'` - Imports the client-side query function.
|
||||
- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](/docs/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.
|
||||
|
||||
</ShowForJs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
- `import getTasks from '@wasp/queries/getTasks'` - Imports the client-side query function.
|
||||
- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](/docs/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.
|
||||
- `import { Task } from '@wasp/entities'` - The type for the task entity we defined in `main.wasp`.
|
||||
|
||||
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.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
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.
|
||||
|
||||
With these changes, you should be seeing the text "No tasks" on the screen:
|
||||
|
||||
<img alt="Todo App - No Tasks"
|
||||
src={useBaseUrl('img/todo-app-no-tasks.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
We'll create a form to add tasks in the next step 🪄
|
414
web/docs/tutorial/06-actions.md
Normal file
414
web/docs/tutorial/06-actions.md
Normal file
@ -0,0 +1,414 @@
|
||||
---
|
||||
title: 6. Modifying Data
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
|
||||
import Collapse from '@site/src/components/Collapse';
|
||||
|
||||
In the previous section, we learned about using queries to fetch data and only briefly mentioned that actions can be used to update the database. Let's learn more about actions so we can add and update tasks in the database.
|
||||
|
||||
We have to create:
|
||||
|
||||
1. A Wasp action that creates a new task.
|
||||
2. A React form that calls that action when the user creates a task.
|
||||
|
||||
## Creating a New Action
|
||||
|
||||
Creating an action is very similar to creating a query.
|
||||
|
||||
### Declaring an Action
|
||||
|
||||
We must first declare the action in `main.wasp`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Implementing an Action
|
||||
|
||||
Let's now define a <ShowForJs>JavaScript</ShowForJs><ShowForTs>TypeScript</ShowForTs> function for our `createTask` action:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
export const createTask = async (args, context) => {
|
||||
return context.entities.Task.create({
|
||||
data: { description: args.description },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import { Task } from '@wasp/entities'
|
||||
import { CreateTask } from '@wasp/actions/types'
|
||||
|
||||
type CreateTaskPayload = Pick<Task, 'description'>
|
||||
|
||||
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
return context.entities.Task.create({
|
||||
data: { description: args.description },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Once again, we've annotated the action with the generated `CreateTask` and `Task` types generated by Wasp. Just like with queries, defining the types on the implemention makes them available on the frontend, giving us **full-stack type safety**.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip
|
||||
We put the function in a new file `src/server/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/server`.
|
||||
:::
|
||||
|
||||
## Invoking the Action on the Client
|
||||
|
||||
First, let's define a form that the user can create new tasks with.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2} title="src/client/MainPage.jsx"
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import createTask from '@wasp/actions/createTask'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
|
||||
// ...
|
||||
|
||||
const NewTaskForm = () => {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const target = event.target
|
||||
const description = target.description.value
|
||||
target.reset()
|
||||
await createTask({ description })
|
||||
} catch (err) {
|
||||
window.alert('Error: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="description" type="text" defaultValue="" />
|
||||
<input type="submit" value="Create task" />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {1,4} title="src/client/MainPage.tsx"
|
||||
import { FormEvent } from 'react'
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import createTask from '@wasp/actions/createTask'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
// ...
|
||||
|
||||
const NewTaskForm = () => {
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const target = event.target as HTMLFormElement
|
||||
const description = target.description.value
|
||||
target.reset()
|
||||
await createTask({ description })
|
||||
} catch (err: any) {
|
||||
window.alert('Error: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="description" type="text" defaultValue="" />
|
||||
<input type="submit" value="Create task" />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Unlike queries, you call actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
Finally, because we've previously annotated the Action's backend implementation with the correct type, Wasp knows that the `createTask` action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` action returns a `Task` but are not using it in this example.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
Now, we just need to add this form to the page component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {10} title="src/client/MainPage.tsx"
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import createTask from '@wasp/actions/createTask'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTaskForm />
|
||||
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && 'Loading...'}
|
||||
{error && 'Error: ' + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {12} title="src/client/MainPage.tsx"
|
||||
import { FormEvent } from 'react'
|
||||
import getTasks from '@wasp/queries/getTasks'
|
||||
import createTask from '@wasp/actions/createTask'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { Task } from '@wasp/entities'
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTaskForm />
|
||||
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && 'Loading...'}
|
||||
{error && 'Error: ' + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And now we have a form that creates new tasks.
|
||||
|
||||
Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.
|
||||
|
||||
Try refreshing the page or opening it in another browser, you'll see the tasks are still there!
|
||||
|
||||
<img alt="Todo App - creating new task"
|
||||
src={useBaseUrl('img/todo-app-new-task.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
:::note Automatic Query Invalidation
|
||||
When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! These automatic updates are handled by code that Wasp generates.
|
||||
|
||||
When you declared the `getTasks` and `createTask` operations, you specified that they both use the `Task` entity. So when `createTask` is called, Wasp knows that the data `getTasks` fetches may have changed and automatically updates it in the background. This means that **out of the box, Wasp will make sure that all your queries are kept in-sync with changes made by any actions**.
|
||||
|
||||
This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked [here](https://github.com/wasp-lang/wasp/issues/63).
|
||||
:::
|
||||
|
||||
## A Second Action
|
||||
|
||||
Our Todo app isn't finished if you can't mark a task as done! We'll create a new action to update a task's status and call it from React whenever a task's checkbox is toggled.
|
||||
|
||||
Since we've already created one task together, try to create this one yourself. It should be an action named `updateTask` that takes a task `id` and an `isDone` in its arguments. You can check our implementation below.
|
||||
|
||||
<Collapse title="Solution">
|
||||
|
||||
The action declaration:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The implementation on the server:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
// ...
|
||||
|
||||
export const updateTask = async ({ id, isDone }, context) => {
|
||||
return context.entities.Task.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isDone: isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
// highlight-next-line
|
||||
import { CreateTask, UpdateTask } from '@wasp/actions/types'
|
||||
|
||||
// ...
|
||||
|
||||
type UpdateTaskPayload = Pick<Task, 'id' | 'isDone'>
|
||||
|
||||
export const updateTask: UpdateTask<UpdateTaskPayload, Task> = async (
|
||||
{ id, isDone },
|
||||
context
|
||||
) => {
|
||||
return context.entities.Task.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isDone: isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</Collapse>
|
||||
|
||||
Now, we can call `updateTask` from the React component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2,7-16,24} title="src/client/MainPage.jsx"
|
||||
// ...
|
||||
import updateTask from "@wasp/actions/updateTask"
|
||||
|
||||
// ...
|
||||
|
||||
const Task = ({ task }) => {
|
||||
const handleIsDoneChange = async (event) => {
|
||||
try {
|
||||
await updateTask({
|
||||
id: task.id,
|
||||
isDone: event.target.checked,
|
||||
})
|
||||
} catch (error: any) {
|
||||
window.alert("Error while updating task: " + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleIsDoneChange}
|
||||
/>
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {2,4,9-18,26} title="src/client/MainPage.tsx"
|
||||
// ...
|
||||
import { FormEvent, ChangeEvent } from 'react'
|
||||
// ...
|
||||
import updateTask from '@wasp/actions/updateTask'
|
||||
|
||||
// ...
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
const handleIsDoneChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
try {
|
||||
await updateTask({
|
||||
id: task.id,
|
||||
isDone: event.target.checked,
|
||||
})
|
||||
} catch (error: any) {
|
||||
window.alert('Error while updating task: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleIsDoneChange}
|
||||
/>
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Awesome! Now we can check off this task 🙃 Let's add one more interesting feature to our app.
|
514
web/docs/tutorial/07-auth.md
Normal file
514
web/docs/tutorial/07-auth.md
Normal file
@ -0,0 +1,514 @@
|
||||
---
|
||||
title: 7. Adding Authentication
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
Most apps today require some sort of registration and login flow, so Wasp has first-class support for it. Let's add it to our Todo app!
|
||||
|
||||
First, we'll create a Todo list for what needs to be done (luckily we have an app for this now 😄).
|
||||
|
||||
- [ ] Create a `User` entity.
|
||||
- [ ] Tell Wasp to use the username and password authentication.
|
||||
- [ ] Add login and signup pages.
|
||||
- [ ] Update the main page to require authentication.
|
||||
- [ ] Add a relation between `User` and `Task` entities.
|
||||
- [ ] Modify our queries and actions so that users can only see and modify their tasks.
|
||||
- [ ] Add a logout button.
|
||||
|
||||
## Creating a User Entity
|
||||
|
||||
Since Wasp manages authentication, it expects certain fields to exist on the `User` entity. Specifically, it expects a unique `username` field and a `password` field, both of which should be strings.
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
```
|
||||
|
||||
As we talked about earlier, we have to remember to update the database schema:
|
||||
|
||||
```sh
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
## Adding Auth to the Project
|
||||
|
||||
Next, we want to tell Wasp that we want to use full-stack [authentication](/docs/auth/overview) in our app:
|
||||
|
||||
```wasp {7-16} title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Todo app",
|
||||
|
||||
auth: {
|
||||
// Tells Wasp which entity to use for storing users.
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// Enable username and password auth.
|
||||
usernameAndPassword: {}
|
||||
},
|
||||
// We'll see how this is used a bit later.
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
By doing this, Wasp will create:
|
||||
|
||||
- [Auth UI](/docs/auth/ui) with login and signup forms.
|
||||
- A `logout()` action.
|
||||
- A React hook `useAuth()`.
|
||||
- `context.user` for use in Queries and Actions.
|
||||
|
||||
:::info
|
||||
Wasp also supports authentication using [Google](/docs/auth/social-auth/google), [GitHub](/docs/auth/social-auth/github), and [email](/docs/auth/email), with more on the way!
|
||||
:::
|
||||
|
||||
## Adding Login and Signup Pages
|
||||
|
||||
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:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/SignupPage.jsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/LoginPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/SignupPage.tsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/LoginPage.tsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Great, Wasp now knows these pages exist! Now, the React code for the pages:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/LoginPage.jsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span>
|
||||
I don't have an account yet (<Link to="/signup">go to signup</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/LoginPage.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<LoginForm />
|
||||
<br />
|
||||
<span>
|
||||
I don't have an account yet (<Link to="/signup">go to signup</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The Signup page is very similar to the login one:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/SignupPage.jsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
const SignupPage = () => {
|
||||
return (
|
||||
<>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span>
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/SignupPage.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
const SignupPage = () => {
|
||||
return (
|
||||
<>
|
||||
<SignupForm />
|
||||
<br />
|
||||
<span>
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Update the Main Page to Require Auth
|
||||
|
||||
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 {4} title="main.wasp"
|
||||
// ...
|
||||
|
||||
page MainPage {
|
||||
authRequired: true,
|
||||
component: import Main from "@client/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.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {1} title="src/client/MainPage.jsx"
|
||||
const MainPage = ({ user }) => {
|
||||
// Do something with the user
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {3} title="src/client/MainPage.tsx"
|
||||
import { User } from '@wasp/entities'
|
||||
|
||||
const MainPage = ({ user }: { user: User }) => {
|
||||
// Do something with the user
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
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.
|
||||
|
||||
Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!
|
||||
|
||||
Let's check out what the database looks like. Start the Prisma Studio:
|
||||
|
||||
```shell
|
||||
wasp db studio
|
||||
```
|
||||
|
||||
<img alt="Database demonstration - password hashing"
|
||||
src={useBaseUrl('img/wasp_db_hash_demonstration.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
We see there is a user and that its password is already hashed 🤯
|
||||
|
||||
However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.
|
||||
|
||||
## Defining a User-Task Relation
|
||||
|
||||
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)):
|
||||
|
||||
```wasp {7,16-17} title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
tasks Task[]
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId Int?
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
As always, we have to update the database:
|
||||
|
||||
```sh
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
:::note
|
||||
We made `user` and `userId` in `Task` optional (via `?`) because that allows us to keep the existing tasks, which don't have a user assigned, in the database. This is not recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional. Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.
|
||||
:::
|
||||
|
||||
## Updating Operations to Check Authentication
|
||||
|
||||
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:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js {1,4} title="src/server/queries.js"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getTasks = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.findMany({
|
||||
where: { user: { id: context.user.id } },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts {3,6} title="src/server/queries.ts"
|
||||
import { Task } from '@wasp/entities'
|
||||
import { GetTasks } from '@wasp/queries/types'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.findMany({
|
||||
where: { user: { id: context.user.id } },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js {1,4,8,14,17,18} title="src/server/actions.js"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
user: { connect: { id: context.user.id } },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTask = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id: args.id, user: { id: context.user.id } },
|
||||
data: { isDone: args.isDone },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts {3,8-10,14,22-26} title="src/server/actions.ts"
|
||||
import { Task } from '@wasp/entities'
|
||||
import { CreateTask, UpdateTask } from '@wasp/actions/types'
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
type CreateTaskPayload = Pick<Task, 'description'>
|
||||
|
||||
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
user: { connect: { id: context.user.id } },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateTaskPayload = Pick<Task, 'id' | 'isDone'>
|
||||
|
||||
export const updateTask: UpdateTask<
|
||||
UpdateTaskPayload,
|
||||
{ count: number }
|
||||
> = async ({ id, isDone }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id, user: { id: context.user.id } },
|
||||
data: { isDone },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::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`.
|
||||
:::
|
||||
|
||||
With these changes, each user should have a list of tasks that only they can see and edit.
|
||||
|
||||
Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:
|
||||
|
||||
```sh
|
||||
wasp db studio
|
||||
```
|
||||
|
||||
<img alt="Database demonstration"
|
||||
src={useBaseUrl('img/wasp_db_demonstration.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
You will see that each user has their tasks, just as we specified in our code!
|
||||
|
||||
## Logout Button
|
||||
|
||||
Last, but not least, let's add the logout functionality:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2,10} title="src/client/MainPage.jsx"
|
||||
// ...
|
||||
import logout from '@wasp/auth/logout'
|
||||
//...
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {2,10} title="src/client/MainPage.tsx"
|
||||
// ...
|
||||
import logout from '@wasp/auth/logout'
|
||||
//...
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
This is it, we have a working authentication system, and our Todo app is multi-user!
|
||||
|
||||
## What's Next?
|
||||
|
||||
We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.
|
||||
|
||||
You can find the complete code for the tutorial [here](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp).
|
||||
|
||||
You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨
|
||||
|
||||
Looking for inspiration?
|
||||
|
||||
- Get a jump start on your next project with [Starter Templates](/docs/project/starter-templates)
|
||||
- Make a real-time app with [Web Sockets](/docs/advanced/web-sockets)
|
||||
|
||||
:::note
|
||||
If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next 🙏
|
||||
|
||||
If you would like to contribute or help to build a feature, let us know! You can find more details on contributing [here](contributing.md).
|
||||
:::
|
||||
|
||||
Oh, and do [**subscribe to our newsletter**](/#signup)! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!
|
@ -41,4 +41,4 @@ So, we’ve been able to build a full-stack application with a database and exte
|
||||
/>
|
||||
|
||||
|
||||
P.S: now you're familiar with Wasp and can build full-stack apps, horaay! 🎉 How did it go? Was it fun? Drop us a message at our <DiscordLink />. Now it's time to look at [Todo App in Wasp](/docs/tutorials/todo-app) if you haven't already. It will introduce some additional concepts so you'd be able to become a true Wasp overlord!
|
||||
P.S: now you're familiar with Wasp and can build full-stack apps, horaay! 🎉 How did it go? Was it fun? Drop us a message at our <DiscordLink />. Now it's time to look at [Todo App in Wasp](/docs/tutorial/create) if you haven't already. It will introduce some additional concepts so you'd be able to become a true Wasp overlord!
|
@ -1,41 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
:::info
|
||||
If you want to run Wasp locally and haven't yet set it up, check out [Quick Start](/docs/quick-start) first for installation instructions, and then continue with the tutorial.
|
||||
:::
|
||||
|
||||
Let's build a simple Todo App in Wasp!
|
||||
|
||||
<img alt="How Todo App will work once it is done"
|
||||
src={useBaseUrl('img/todo-app-tutorial-intro.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
This tutorial will take you step by step through the most important features of Wasp.
|
||||
|
||||
If you get stuck at any point (or just want to chat), reach out to us on [Discord](https://discord.gg/rzdnErX) and we will help you!
|
||||
|
||||
You can check out the complete code of the app we are about to build with Wasp [here](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp).
|
||||
|
||||
:::tip
|
||||
If you are interested at any moment in what is Wasp actually generating in the background, take a look at `.wasp/out/` directory in your project.
|
||||
:::
|
||||
|
||||
:::tip See Wasp in Action
|
||||
Prefer Videos? We have a YouTube tutorial which walks you through building this Todo App step by step. Check it out [here](https://youtu.be/R8uOu6ZEr5s).
|
||||
|
||||
We've also set up an in-browser dev environment for you on Gitpod, which allows you to see and edit the finished ToDo app with no setup required.
|
||||
<p align="center">
|
||||
<a href="https://gitpod.io/#https://github.com/wasp-lang/gitpod-template">
|
||||
<img src="https://gitpod.io/button/open-in-gitpod.svg" />
|
||||
</a>
|
||||
</p>
|
||||
:::
|
||||
|
@ -1,217 +0,0 @@
|
||||
---
|
||||
id: 01-creating-new-project
|
||||
title: "Creating a new project"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
Run the following command in your terminal to create a new Wasp project:
|
||||
```shell
|
||||
wasp new TodoApp
|
||||
```
|
||||
Enter the created directory and run:
|
||||
```shell
|
||||
cd TodoApp
|
||||
wasp start
|
||||
```
|
||||
You have just run your app in the development mode!
|
||||
|
||||
:::note
|
||||
`wasp start` might take a little bit longer to finish when you run it for the first time due to the initial setup.
|
||||
:::
|
||||
|
||||
You will be seeing a lot of different output from the client, server and database setting themselves up.
|
||||
Once ready, a new tab should open in your browser at `http://localhost:3000`, with a simple placeholder page:
|
||||
|
||||
<img alt="Screenshot of new Wasp app"
|
||||
src={useBaseUrl('img/wasp-new-screenshot.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
height="400px"
|
||||
/>
|
||||
|
||||
We just set the foundations of our app! We don't have yet the features to show it, but Wasp has already generated for us the full front-end and back-end code of the app. Take a peek at `TodoApp/.wasp/out` if you are curious and want to see what it looks like!
|
||||
|
||||
## Taking a closer look at the code
|
||||
|
||||
Let's inspect the Wasp project we just created:
|
||||
```bash
|
||||
.
|
||||
├── .gitignore
|
||||
├── main.wasp # Your wasp code goes here.
|
||||
├── src
|
||||
│ ├── client # Your client code (JS/CSS/HTML) goes here.
|
||||
│ │ ├── Main.css
|
||||
│ │ ├── MainPage.jsx
|
||||
│ │ ├── vite-env.d.ts
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ └── waspLogo.png
|
||||
│ ├── server # Your server code (Node JS) goes here.
|
||||
│ │ └── tsconfig.json
|
||||
│ ├── shared # Your shared (runtime independent) code goes here .
|
||||
│ │ └── tsconfig.json
|
||||
│ └── .waspignore
|
||||
└── .wasproot
|
||||
```
|
||||
By _your code_, we mean _"the code you write"_ (as opposed to the code generated by Wasp). Wasp expects you to separate all external code into three folders to make it clear which runtime executes what:
|
||||
- `src/server` - Contains the code executed on the server (i.e., in Node)
|
||||
- `src/client` - Contains the code executed on the client (i.e., JS in the browser)
|
||||
- `src/shared` - Contains the code you want to use on both the client and the server (e.g., runtime-independent utility functions)
|
||||
|
||||
You may be wondering what about the rest of the generated files (`tsconfig.json`
|
||||
and `vite-env.d.ts`? Your IDE needs them to improve your development
|
||||
experience (i.e., autocompletion, intellisense, etc.), so it's best to leave
|
||||
them alone (for now).
|
||||
|
||||
:::note Typescript Support
|
||||
Wasp supports Typescript out of the box but you are free to use JavaScript (js/jsx)
|
||||
or TypeScript (ts/tsx) as you see fit. No extra configuration is needed!
|
||||
|
||||
We'll provide you with both JavaScript and TypeScript snippets for each feature we cover.
|
||||
|
||||
Code blocks have a toggle between vanilla 🍦 Javascript/JSX and Typescript/TSX.
|
||||
|
||||
To see how to get the most out of Wasp and TypeScript, take a look at [our
|
||||
TypeScript doc](/docs/typescript). It contains a list of all TypeScript features
|
||||
Wasp currently supports.
|
||||
:::
|
||||
|
||||
Let's start with the `main.wasp` file, which introduces 3 new concepts:
|
||||
[app](language/features.md#app),
|
||||
[page](language/features.md#page) and
|
||||
[route](language/features.md#route).
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp { // Main declaration, defines a new web app.
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Todo app" // Used as a browser tab title.
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/`
|
||||
|
||||
page MainPage {
|
||||
// We specify that ReactJS implementation of our page can be found in
|
||||
// `src/client/MainPage.jsx` as a default export (uses standard js import syntax).
|
||||
// Use '@client' to reference files inside the src/client folder.
|
||||
component: import Main from "@client/MainPage.jsx"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
app TodoApp { // Main declaration, defines a new web app.
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "Todo app" // Used as a browser tab title.
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage } // Render page MainPage on url `/`
|
||||
|
||||
page MainPage {
|
||||
// We specify that ReactJS implementation of our page can be found in
|
||||
// `src/client/MainPage.tsx` as a default export (uses standard JS import syntax).
|
||||
// Use '@client' to reference files inside the src/client folder.
|
||||
component: import Main from "@client/MainPage.tsx"
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
:::warning Using TypeScript
|
||||
If you are using Typescript, you will need to rename `MainPage.jsx` to `MainPage.tsx`.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
Let's now take a look at that React component we referenced in the `page MainPage { ... }` declaration in `main.wasp`:
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/MainPage.jsx"
|
||||
import waspLogo from './waspLogo.png'
|
||||
import './Main.css'
|
||||
|
||||
const MainPage = () => {
|
||||
...
|
||||
}
|
||||
export default MainPage
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/MainPage.tsx"
|
||||
import waspLogo from './waspLogo.png'
|
||||
import './Main.css'
|
||||
|
||||
const MainPage = () => {
|
||||
...
|
||||
}
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
As we can see, this is simply a functional React component that uses the CSS and Wasp logo files sitting next to it in the `src/client` dir.
|
||||
|
||||
This is all the code we need!
|
||||
Wasp quietly takes care of everything else necessary to define, build, and run a web app.
|
||||
|
||||
:::tip
|
||||
`wasp start` automatically picks up the changes you make and restarts the app, so keep it running.
|
||||
:::
|
||||
|
||||
## Cleaning up
|
||||
|
||||
Let's make our first changes!
|
||||
|
||||
To prepare the clean slate for building the TodoApp, delete the files `Main.css`
|
||||
and `waspLogo.png` from the `src/client/` folder (`src/shared` and `src/server`
|
||||
are already clean). Wasp needs the `tsconfig.json` and `vite-env.d.ts` for
|
||||
IDE support, so it's important to keep them.
|
||||
|
||||
Next, let's make the `MainPage` component much simpler:
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/MainPage.jsx"
|
||||
const MainPage = () => {
|
||||
return <div>Hello world!</div>
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/MainPage.tsx"
|
||||
const MainPage = () => {
|
||||
return <div>Hello world!</div>
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
At this point, you should see something like this:
|
||||
|
||||
<img alt="Todo App - Hello World"
|
||||
src={useBaseUrl('img/todo-app-hello-world.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
Ok, time to take the next step - implementing some real Todo app features!
|
@ -1,41 +0,0 @@
|
||||
---
|
||||
id: 02-task-entity
|
||||
title: "Task entity"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
[Entities](language/features.md#entity) are one of the very central concepts in Wasp, and they mainly play the role of data models.
|
||||
|
||||
Since our TodoApp is all about tasks, we will define a Task entity in Wasp:
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
```
|
||||
|
||||
Since Wasp uses [Prisma](https://www.prisma.io) as a database, the definition of an entity comes down to defining a [Prisma model](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model/), using PSL (Prisma Schema Language) inside the `{=psl psl=}` tags.
|
||||
|
||||
After this change and before running `wasp start`, we first need to run:
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
This instructs Prisma to create a new database schema migration (you'll see a new directory `migrations/` appeared in the root dir of our app) and apply it to the database.
|
||||
|
||||
To take a look at the database and the new `Task` schema, run:
|
||||
```shell
|
||||
wasp db studio
|
||||
```
|
||||
|
||||
<img alt="Todo App - Db studio showing Task schema"
|
||||
src={useBaseUrl('img/todo-app-db-studio-task-entity.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
Click on the specific entity (we have only `Task` for now) and check out its fields! We don't have any data yet in our database, but we are about to change that.
|
||||
|
||||
|
@ -1,267 +0,0 @@
|
||||
---
|
||||
id: 03-listing-tasks
|
||||
title: "Listing tasks"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
We want to admire our tasks, so let's list them!
|
||||
|
||||
## Introducing operations (queries and actions)
|
||||
|
||||
The primary way of interacting with entities in Wasp is via [operations (queries and actions)](language/features.md#queries-and-actions-aka-operations).
|
||||
|
||||
Queries are here when we need to fetch/read something, while actions are here when we need to change/update something.
|
||||
We will start with writing a query, since we are just listing tasks and not modifying anything for now.
|
||||
|
||||
To list tasks, we will need two things:
|
||||
1. A Wasp query that fetches all the tasks from the database.
|
||||
2. React logic that calls our query and displays its results.
|
||||
|
||||
## Defining the Query
|
||||
|
||||
Let's implement `getTasks` [query](language/features.md#query).
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
It consists of a declaration in Wasp and implementation in JS (in `src/server/` directory).
|
||||
</ShowForJs>
|
||||
<ShowForTs>
|
||||
|
||||
It consists of a declaration in Wasp and implementation in TS (in `src/server/` directory).
|
||||
</ShowForTs>
|
||||
|
||||
### Wasp declaration
|
||||
Add the following code to `main.wasp`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getTasks {
|
||||
// We specify that JS implementation of the query (which is an async JS function)
|
||||
// can be found in `src/server/queries.js` as the named export `getTasks`.
|
||||
// Use '@server' to reference files inside the src/server folder.
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
// We tell Wasp that this query is doing something with entity `Task`. With that, Wasp will
|
||||
// automatically refresh the results of this query when tasks change.
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
query getTasks {
|
||||
// We specify that JS implementation of the query (which is an async JS function)
|
||||
// can be found in `src/server/queries.js` as the named export `getTasks`.
|
||||
// Use '@server' to reference files inside the src/server folder.
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
// We tell Wasp that this query is doing something with entity `Task`. With that, Wasp will
|
||||
// automatically refresh the results of this query when tasks change.
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
:::caution
|
||||
<!-- This block is mostly duplicated in typescript.md -->
|
||||
Even if you use TypeScript and have the file `queries.ts`, you will still need to import it using the `.js` extension. Wasp internally uses `esnext` module resolution, which always requires specifying the extension as `.js` (i.e., the extension used in the emitted JS file). This applies to all `@server` imports (and files on the server in general). It does not apply to client files.
|
||||
|
||||
Read more about ES modules in TypeScript [here](https://www.typescriptlang.org/docs/handbook/esm-node.html). If you're interested in the discussion and the reasoning behind this, read about it [in this GitHub issue](https://github.com/microsoft/TypeScript/issues/33588).
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
### JavaScript implementation
|
||||
|
||||
Next, create a new file `src/server/queries.ts` and define the JavaScript function we've just imported in our `query` declaration:
|
||||
</ShowForJs>
|
||||
<ShowForTs>
|
||||
|
||||
### TypeScript implementation
|
||||
|
||||
Next, create a new file `src/server/queries.ts` and define the TypeScript function we've just imported in our `query` declaration:
|
||||
</ShowForTs>
|
||||
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/queries.js"
|
||||
export const getTasks = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```js title="src/server/queries.ts"
|
||||
import { Task } from "@wasp/entities"
|
||||
import { GetTasks } from "@wasp/queries/types"
|
||||
|
||||
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
|
||||
return context.entities.Task.findMany({})
|
||||
}
|
||||
```
|
||||
|
||||
Wasp automatically generates the types `GetTasks` and `Task` based the contents of `main.wasp`:
|
||||
- `Task` is a type corresponding to the `Task` entity we've defined in `main.wasp`.
|
||||
- `GetTasks` is a generic type Wasp automatically generated based the `getTasks` query we've defined in `main.wasp`.
|
||||
|
||||
You can use these types to specify the Query's input and output types. This Query doesn't expect any arguments (meaning that its input type is `void`), but it does return an array of tasks (meaning that its output type is `Task[]`)
|
||||
|
||||
Annotating the Queries is optional, but highly recommended because doing so enables **full-stack type safety**. We'll see what this means in the next section.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Query function parameters:
|
||||
- `args`: `object`, arguments the query is invoked with.
|
||||
- `context`: `object`, additional stuff provided by Wasp.
|
||||
|
||||
|
||||
Since we declared in `main.wasp` that our query uses the `Task` entity, Wasp injected a [Prisma client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud) for the `Task` entity as `context.entities.Task` - we used it above to fetch all the tasks from the database.
|
||||
|
||||
:::info
|
||||
Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the `src/server` folder.
|
||||
:::
|
||||
|
||||
## Invoking the Query on the frontend
|
||||
|
||||
We've just said that the queries we write are executed on the server, but Wasp will generate client-side query functions (taking care of serialization, network calls, and cache invalidation in the background). Let's finally use the query we've just created, `getTasks`, in our React component to list the tasks:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {1-2,5-14,17-36} title="src/client/MainPage.jsx"
|
||||
import getTasks from "@wasp/queries/getTasks"
|
||||
import { useQuery } from "@wasp/queries"
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && "Loading..."}
|
||||
{error && "Error: " + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
|
||||
```tsx {1-3,6-15,18-37} title="src/client/MainPage.tsx"
|
||||
import getTasks from "@wasp/queries/getTasks"
|
||||
import { useQuery } from "@wasp/queries"
|
||||
import { Task } from "@wasp/entities"
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && "Loading..."}
|
||||
{error && "Error: " + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }: { tasks: Task[] }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
Most of this code is just regular React, the only exception being the two special `@wasp` imports:
|
||||
- `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query.
|
||||
- `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp.
|
||||
|
||||
</ShowForJs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
Most of this code is just regular React, the only exception being the three special `@wasp` imports:
|
||||
- `import getTasks from '@wasp/queries/getTasks'` - Gives us our freshly defined Wasp query.
|
||||
- `import { useQuery } from '@wasp/queries'` - Gives us Wasp's [useQuery](language/features.md#the-usequery-hook) React hook which is actually just a thin wrapper over [react-query](https://github.com/tannerlinsley/react-query)'s [useQuery](https://react-query.tanstack.com/docs/guides/queries) hook, behaving very similarly while offering some extra integration with Wasp.
|
||||
- `import { Task } from '@wasp/entities'` - The type for Task entity we've defined in `main.wasp`.
|
||||
|
||||
Notice how you didn't need to tell TypeScript anything about the Query's response data - TypeScript inferred it automatically.
|
||||
|
||||
Because we've previously annotated the Query's backend implementation with `GetTasks<void, Task[]>`, Wasp knows the response data's correct type (i.e., `Task[]`) on the frontend. We call this feature **full-stack type safety**.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
We could have called the Query directly with `getTasks()`, but wrapping it with `useQuery(getTasks)` makes it reactive. More precisely, React will re-render the component every time the Query's result changes.
|
||||
|
||||
With these changes, you should be seeing the text "No tasks" on the screen:
|
||||
|
||||
<img alt="Todo App - No Tasks"
|
||||
src={useBaseUrl('img/todo-app-no-tasks.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
Next, let's create some tasks!
|
@ -1,273 +0,0 @@
|
||||
---
|
||||
id: 04-creating-tasks
|
||||
title: "Creating tasks"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
To enable the creation of new tasks, we will need two things:
|
||||
1. A Wasp action that creates a new task.
|
||||
2. A React form that calls that action with the new task's data.
|
||||
|
||||
## Defining the Action
|
||||
Creating an action is very similar to creating a query.
|
||||
|
||||
### Wasp declaration
|
||||
|
||||
We must first declare the Action in `main.wasp`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
### JavaScript implementation
|
||||
Let's now define a JavaScript function for our action:
|
||||
|
||||
</ShowForJs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
### TypeScript implementation
|
||||
Let's now define a TypeScript function for our action:
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
export const createTask = async (args, context) => {
|
||||
return context.entities.Task.create({
|
||||
data: { description: args.description }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
import { Task } from "@wasp/entities"
|
||||
import { CreateTask } from "@wasp/actions/types"
|
||||
|
||||
type CreateTaskPayload = Pick<Task, "description">
|
||||
|
||||
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
return context.entities.Task.create({
|
||||
data: { description: args.description },
|
||||
})
|
||||
}
|
||||
```
|
||||
Once again, we've annotated the Action with proper types (using the types `Task` and `CreateTask` Wasp generated for us). Annotating the Action makes the type information automatically available the frontend, giving us automatic **full-stack type safety**.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::tip
|
||||
We put the function in a new file `src/server/actions.{js,ts}`, but we could have put it anywhere we wanted! There are no limitations here, as long as the import statement in the Wasp file is correct and the source file is inside the `src/server` folder.
|
||||
:::
|
||||
|
||||
## Invoking the Action on the frontend
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {3,10,41-60} title="src/client/MainPage.jsx"
|
||||
import getTasks from "@wasp/queries/getTasks"
|
||||
import createTask from "@wasp/actions/createTask"
|
||||
import { useQuery } from "@wasp/queries"
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTaskForm />
|
||||
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && "Loading..."}
|
||||
{error && "Error: " + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NewTaskForm = () => {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const target = event.target
|
||||
const description = target.description.value
|
||||
target.reset()
|
||||
await createTask({ description })
|
||||
} catch (err) {
|
||||
window.alert("Error: " + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="description" type="text" defaultValue="" />
|
||||
<input type="submit" value="Create task" />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {1,3,12,43-62} title="src/client/MainPage.tsx"
|
||||
import { FormEvent } from "react"
|
||||
import getTasks from "@wasp/queries/getTasks"
|
||||
import createTask from "@wasp/actions/createTask"
|
||||
import { useQuery } from "@wasp/queries"
|
||||
import { Task } from "@wasp/entities"
|
||||
|
||||
const MainPage = () => {
|
||||
const { data: tasks, isLoading, error } = useQuery(getTasks)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTaskForm />
|
||||
|
||||
{tasks && <TasksList tasks={tasks} />}
|
||||
|
||||
{isLoading && "Loading..."}
|
||||
{error && "Error: " + error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
return (
|
||||
<div>
|
||||
<input type="checkbox" id={String(task.id)} checked={task.isDone} />
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksList = ({ tasks }: { tasks: Task[] }) => {
|
||||
if (!tasks?.length) return <div>No tasks</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task, idx) => (
|
||||
<Task task={task} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NewTaskForm = () => {
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const target = event.target as HTMLFormElement
|
||||
const description = target.description.value
|
||||
target.reset()
|
||||
await createTask({ description })
|
||||
} catch (err: any) {
|
||||
window.alert("Error: " + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="description" type="text" defaultValue="" />
|
||||
<input type="submit" value="Create task" />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
We're calling the `createTask` Action directly this time (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.
|
||||
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
Finally, because we've previously annotated the Action's backend implementation with the correct type, Wasp knows that the `createTask` action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` action returns a `Task`, but we don't need it.
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
That's it!
|
||||
|
||||
Try creating a "Build a Todo App in Wasp" task and see it appear in the list below.
|
||||
The task is created on the server and also saved in the database. Try refreshing the page or opening it in another browser - you'll see the tasks are still here!
|
||||
|
||||
<img alt="Todo App - creating new task"
|
||||
src={useBaseUrl('img/todo-app-new-task.png')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
## Side note: Automatic invalidation/updating of queries
|
||||
You will notice that when you create a new task, the list of tasks is automatically updated with that new task, although we have written no code to take care of that! Normally, you would have to do this explicitly, e.g. with `react-query` you would invalidate the `getTasks` query via its key, or would call its `refetch()` method.
|
||||
|
||||
The reason why the `getTasks` query automatically updates when the `createTask` action is executed is that Wasp is aware that both of them are working with the `Task` entity, and therefore assumes that the action that operates on `Task` (in this case `createTask`) might have changed the result of the `getTasks` query. Therefore, in the background, Wasp nudges the `getTasks` query to update. This means that **out of the box, Wasp will make sure that all your queries that deal with entities are always in sync with any changes that the actions might have done**.
|
||||
|
||||
:::note
|
||||
While this kind of approach to automatic invalidation of queries is very convenient, it is in some situations wasteful and could become a performance bottleneck as the app grows. In that case, you will be able to override this default behavior and instead provide more detailed (and performant) instructions on how the specific action should affect queries.
|
||||
|
||||
Overriding the default behavior is not yet supported but it is something we plan to do and you can track the progress [here](https://github.com/wasp-lang/wasp/issues/63) (or even contribute!).
|
||||
:::
|
@ -1,188 +0,0 @@
|
||||
---
|
||||
id: 05-updating-tasks
|
||||
title: "Updating tasks"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
Our Todo app isn't finished if you we can't mark a task as finished!
|
||||
|
||||
To do that, we'll need to do two things:
|
||||
1. Implement a Wasp Action for toggling a task's `isDone` state.
|
||||
2. Call this Action from React whenever the user toggles a checkbox.
|
||||
|
||||
## Defining the Action
|
||||
|
||||
### Wasp declaration
|
||||
|
||||
Let's first define the Action in `main.wasp`:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
||||
<ShowForJs>
|
||||
|
||||
### JavaScript implementation
|
||||
Let's now define the Action's JavaScript implementation in `src/server/actions.js`:
|
||||
|
||||
</ShowForJs>
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
### TypeScript implementation
|
||||
Let's now define the Action's JavaScript implementation in `src/server/actions.ts`:
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="src/server/actions.js"
|
||||
// ...
|
||||
|
||||
export const updateTask = async (args, context) => {
|
||||
return context.entities.Task.update({
|
||||
where: { args.id },
|
||||
data: {
|
||||
isDone: args.isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="src/server/actions.ts"
|
||||
// highlight-next-line
|
||||
import { CreateTask, UpdateTask } from "@wasp/actions/types"
|
||||
|
||||
// ...
|
||||
|
||||
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
export const updateTask: UpdateTask<UpdateTaskPayload, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
return context.entities.Task.update({
|
||||
where: { args.id },
|
||||
data: {
|
||||
isDone: args.isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Invoking the Action on the frontend
|
||||
|
||||
Finally, all that's left to do is call the Action from the React component:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2,7-16,24} title="src/client/MainPage.jsx"
|
||||
// ...
|
||||
import updateTask from '@wasp/actions/updateTask'
|
||||
|
||||
// ...
|
||||
|
||||
const Task = ({ task }) => {
|
||||
const handleIsDoneChange = async (event) => {
|
||||
try {
|
||||
await updateTask({
|
||||
id: task.id,
|
||||
isDone: event.target.checked,
|
||||
})
|
||||
} catch (error: any) {
|
||||
window.alert("Error while updating task: " + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleIsDoneChange}
|
||||
/>
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {2,4,9-18,26} title="src/client/MainPage.tsx"
|
||||
// ...
|
||||
import { FormEvent, ChangeEvent } from "react"
|
||||
// ...
|
||||
import updateTask from '@wasp/actions/updateTask'
|
||||
|
||||
// ...
|
||||
|
||||
const Task = ({ task }: { task: Task }) => {
|
||||
const handleIsDoneChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
try {
|
||||
await updateTask({
|
||||
id: task.id,
|
||||
isDone: event.target.checked,
|
||||
})
|
||||
} catch (error: any) {
|
||||
window.alert("Error while updating task: " + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(task.id)}
|
||||
checked={task.isDone}
|
||||
onChange={handleIsDoneChange}
|
||||
/>
|
||||
{task.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Awesome! We can now tick this task as done ;).
|
@ -1,466 +0,0 @@
|
||||
---
|
||||
id: 06-auth
|
||||
title: "Authentication"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
Most of the apps today require some sort of registration and login flows, and Wasp has support for it out of the box, so let's see how to add it to our Todo app!
|
||||
|
||||
Let's define a Todo list (luckily we have an app for that now 🙃) to get this done:
|
||||
- [ ] Add a new entity called `User`.
|
||||
- [ ] Add `auth` to our `app`.
|
||||
- [ ] Add `Login` and `Signup` pages.
|
||||
- [ ] Modify `src/client/MainPage.{jsx,tsx}` so that it requires authentication.
|
||||
- [ ] Add Prisma relation between `User` and `Task` entities.
|
||||
- [ ] Modify our queries and actions so that they work only with the tasks belonging to the authenticated user.
|
||||
- [ ] Add a logout button.
|
||||
|
||||
## Adding entity User
|
||||
First, let's define the `User` entity:
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
```
|
||||
|
||||
Run:
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
to propagate the schema change (we added User).
|
||||
|
||||
## Defining `app.auth`
|
||||
Next, we want to tell Wasp that we want full-stack [authentication](language/features.md#authentication--authorization) in our app, and that it should use the `User` entity for it:
|
||||
|
||||
```wasp {7-16} title="main.wasp"
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
},
|
||||
title: "Todo app",
|
||||
|
||||
auth: {
|
||||
// Expects entity User to have username and passwords fields.
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// We also support Google, GitHub and email auth, with more on the way!
|
||||
usernameAndPassword: {}
|
||||
},
|
||||
// We'll see how this is used a bit later
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
}
|
||||
}
|
||||
```
|
||||
What this means for us is that Wasp now offers us:
|
||||
- Login and Signup forms located at `@wasp/auth/forms/Login` and `@wasp/auth/forms/Signup` paths, ready to be used.
|
||||
- a `logout()` action.
|
||||
- a React hook `useAuth()`.
|
||||
- `context.user` as an argument within queries/actions.
|
||||
|
||||
This is a very high-level API for auth which makes it very easy to get started quickly, but is
|
||||
not very flexible. If you require more control (e.g. want to execute some custom code on the server
|
||||
during signup, check out the [lower-level auth API](/docs/language/features#lower-level-api).
|
||||
|
||||
Ok, that was easy!
|
||||
|
||||
To recap, so far we have defined:
|
||||
- The `User` entity.
|
||||
- The `app.auth` field, thanks to which Wasp gives us plenty of auth-related functionality.
|
||||
|
||||
## Adding Login and Signup pages
|
||||
|
||||
When we defined `app.auth` we got login and signup forms generated for us, but now we have to create Login and Signup pages that use them. In our `main.wasp` file we'll add the following:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
// ...
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/SignupPage"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/LoginPage"
|
||||
}
|
||||
```
|
||||
|
||||
Great, Wasp now knows how to route these and where to find the pages. Now to the React code of the pages:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/LoginPage.jsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<LoginForm/>
|
||||
<br/>
|
||||
<span>
|
||||
I don't have an account yet (<Link to="/signup">go to signup</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/LoginPage.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { LoginForm } from '@wasp/auth/forms/Login'
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<LoginForm/>
|
||||
<br/>
|
||||
<span>
|
||||
I don't have an account yet (<Link to="/signup">go to signup</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The Signup page is very similar to the login one:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/SignupPage.jsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
const SignupPage = () => {
|
||||
return (
|
||||
<>
|
||||
<SignupForm/>
|
||||
<br/>
|
||||
<span>
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/SignupPage.tsx"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
|
||||
const SignupPage = () => {
|
||||
return (
|
||||
<>
|
||||
<SignupForm/>
|
||||
<br/>
|
||||
<span>
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Updating `MainPage` page to check if the user is authenticated
|
||||
|
||||
Now, let's see how we're going to handle the situation when the user is not logged in.
|
||||
`MainPage` page is a private page and we want users to be able to see it only if they are authenticated.
|
||||
Wasp allows you to simply enforce private pages using the `authRequired` field:
|
||||
|
||||
```wasp {3} title="main.wasp"
|
||||
// ...
|
||||
page MainPage {
|
||||
authRequired: true,
|
||||
component: import Main from "@client/MainPage"
|
||||
}
|
||||
```
|
||||
|
||||
With `authRequired: true` we declared that page `MainPage` is accessible only to authenticated users.
|
||||
If an unauthenticated user tries to access route `/` where our page `MainPage` is, they will be redirected to `/login` as specified with the `onAuthFailedRedirectTo` property in `app.auth`.
|
||||
|
||||
Also, when `authRequired` is set to `true`, the React component of a page (specified by `component` property within `page`) will be provided `user` object as a prop. It can be accessed like this:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {1} title="src/client/MainPage.jsx"
|
||||
const MainPage = ({ user }) => {
|
||||
// Do something with the user
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {3} title="src/client/MainPage.tsx"
|
||||
import { User } from "@wasp/entities"
|
||||
|
||||
const MainPage = ({ user }: { user: User }) => {
|
||||
// Do something with the user
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Ok, time to try out how this works!
|
||||
|
||||
Now, we can start the app again (if it's not still running):
|
||||
```shell
|
||||
wasp start
|
||||
```
|
||||
|
||||
Try going to the main page (`/`) of our web app. It will now redirect you to `/login`, where you'll be asked to authenticate. Once you log in or sign up, you will be sent back to `/` and you will see the todo list.
|
||||
|
||||
Let's now see how things look in the database! Run:
|
||||
```shell
|
||||
wasp db studio
|
||||
```
|
||||
<img alt="Database demonstration - password hashing"
|
||||
src={useBaseUrl('img/wasp_db_hash_demonstration.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
We see there is a user and that its password is already hashed! Wasp took care of this for us.
|
||||
|
||||
However, you will notice that if you try logging in with different users and creating tasks, all users are still sharing tasks.
|
||||
That is because we did not yet update queries and actions to work only on the current user's tasks, so let's do that next!
|
||||
|
||||
## Defining User-Task relation in entities
|
||||
|
||||
First, let's define a one-to-many relation between User and Task (check the [prisma docs on relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)):
|
||||
```wasp {6,13-14} title="main.wasp"
|
||||
// ...
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
tasks Task[]
|
||||
psl=}
|
||||
// ...
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId Int?
|
||||
psl=}
|
||||
// ...
|
||||
```
|
||||
|
||||
We modified entities by adding the User-Task relation, so let's run
|
||||
```shell
|
||||
wasp db migrate-dev
|
||||
```
|
||||
to create a database schema migration and apply it to the database.
|
||||
|
||||
:::note
|
||||
We made `user` and `userId` in `Task` optional (via `?`) because that allows us to keep the existing tasks, which don't have a user assigned, in the database.
|
||||
This is not recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional.
|
||||
Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all.
|
||||
However, for this tutorial, for the sake of simplicity, we will stick with this.
|
||||
:::
|
||||
|
||||
## Updating operations to forbid access to non-authenticated users
|
||||
|
||||
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:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js {1,4} title="src/server/queries.js"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getTasks = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.findMany(
|
||||
{ where: { user: { id: context.user.id } } }
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts {3,6} title="src/server/queries.ts"
|
||||
import { Task } from "@wasp/entities"
|
||||
import { GetTasks } from "@wasp/queries/types"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const getTasks: GetTasks<void, Task[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.findMany(
|
||||
{ where: { user: { id: context.user.id } } }
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js {1,4,8,14,17,18} title="src/server/actions.js"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
export const createTask = async (args, context) => {
|
||||
if (!context.user) { throw new HttpError(401) }
|
||||
return context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
user: { connect: { id: context.user.id } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTask = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id: args.id, user: { id: context.user.id } },
|
||||
data: { isDone: args.isDone }
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts {3,11,17,28,31,32} title="src/server/actions.ts"
|
||||
import { Task } from "@wasp/entities"
|
||||
import { CreateTask, UpdateTask } from "@wasp/actions/types"
|
||||
import HttpError from '@wasp/core/HttpError.js'
|
||||
|
||||
type CreateTaskPayload = Pick<Task, "description">
|
||||
|
||||
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.create({
|
||||
data: {
|
||||
description: args.description,
|
||||
user: { connect: { id: context.user.id } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
export const updateTask: UpdateTask<UpdateTaskPayload, { count: number }> = async (
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id: args.id, user: { id: context.user.id } },
|
||||
data: { isDone: args.isDone }
|
||||
})
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::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`.
|
||||
:::
|
||||
|
||||
Right, that should be it!
|
||||
|
||||
Run (or just continue running):
|
||||
```shell
|
||||
wasp start
|
||||
```
|
||||
and everything should work as expected now! Each user has their own tasks only they can see and edit.
|
||||
|
||||
Try playing around with our app, adding a few users and some tasks. Then run:
|
||||
```shell
|
||||
wasp db studio
|
||||
```
|
||||
<img alt="Database demonstration"
|
||||
src={useBaseUrl('img/wasp_db_demonstration.gif')}
|
||||
style={{ border: "1px solid black" }}
|
||||
/>
|
||||
|
||||
You will see that each user has their own tasks, just as we specified in our code!
|
||||
|
||||
## Logout button
|
||||
|
||||
Last, but not least, let's add the logout functionality:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2,10} title="src/client/MainPage.jsx"
|
||||
// ...
|
||||
import logout from '@wasp/auth/logout'
|
||||
//...
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {2,10} title="src/client/MainPage.tsx"
|
||||
// ...
|
||||
import logout from '@wasp/auth/logout'
|
||||
//...
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
<button onClick={logout}>Logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
This is it, we have a working authentication system, and our Todo app is multi-user!
|
@ -1,142 +0,0 @@
|
||||
---
|
||||
id: 07-dependencies
|
||||
title: "Dependencies"
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
|
||||
What is a Todo app without some clocks!? Well, still a Todo app, but certainly not as fun as one with clocks!
|
||||
|
||||
So, let's add a couple of clocks to our app, to help us track time while we perform our tasks (and to demonstrate the `app.dependencies` feature).
|
||||
|
||||
For this, we will use the `react-clock` library from NPM. We can add it to our project as a [dependency](language/features.md#dependencies) like this:
|
||||
```wasp {4-6} title="main.wasp"
|
||||
app TodoApp {
|
||||
// ...
|
||||
|
||||
dependencies: [
|
||||
("react-clock", "4.2.0")
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Run
|
||||
```shell
|
||||
wasp start
|
||||
```
|
||||
to have Wasp download and install the new dependency. If `wasp start` is already running, Wasp will detect the dependency change, and restart automatically.
|
||||
|
||||
Next, let's create a new component `Clocks` where we can play with the clocks.
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx title="src/client/Clocks.jsx"
|
||||
import { useEffect, useState } from 'react'
|
||||
import Clock from 'react-clock'
|
||||
import 'react-clock/dist/Clock.css'
|
||||
|
||||
const Clocks = () => {
|
||||
const [time, setTime] = useState(new Date())
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTime(new Date()), 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Clock value={time} />
|
||||
<Clock value={new Date(time.getTime() + 60 * 60000)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clocks
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx title="src/client/Clocks.tsx"
|
||||
import { useEffect, useState } from 'react'
|
||||
import Clock from 'react-clock'
|
||||
import 'react-clock/dist/Clock.css'
|
||||
|
||||
const Clocks = () => {
|
||||
const [time, setTime] = useState(new Date())
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTime(new Date()), 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Clock value={time} />
|
||||
<Clock value={new Date(time.getTime() + 60 * 60000)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clocks
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And let's import it into our `MainPage.{jsx,tsx}` component.
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```jsx {2,11} title="src/client/MainPage.jsx"
|
||||
// ...
|
||||
import Clocks from './Clocks'
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
|
||||
<div><Clocks /></div>
|
||||
|
||||
// ...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```tsx {2,11} title="src/client/MainPage.tsx"
|
||||
// ...
|
||||
import Clocks from './Clocks'
|
||||
|
||||
const MainPage = () => {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div>
|
||||
// ...
|
||||
|
||||
<div><Clocks /></div>
|
||||
|
||||
// ...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
As you can see, importing other files from `src/client` is completely normal, just use the relative path. The same goes for all files under `src/server`. You can't (and shouldn't) import files from `src/client` into `src/server` and vice versa. If you want to share code between the two runtimes, you can use a relative import to import anything from `src/shared` into both the client code and the server code.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
The `src/shared` is especially handy for full-stack type definitions when using TypeScript.
|
||||
</ShowForTs>
|
||||
|
||||
That is it! We added a dependency and used it in our project.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user