Merge branch 'release'
@ -41,11 +41,14 @@ All that's required is to create an app. And make a tutorial or a blog post to h
|
||||
|
||||
Or you can re-build your existing pet project with Wasp. That would be cool!
|
||||
|
||||
## Documentation
|
||||
## Documentation & Blog
|
||||
|
||||
It may sound like the simplest one, but it's super valuable! If you've found an issue, a broken link or if something was unclear on our [website](https://wasp-lang.dev/) - please, feel free to fix it :)
|
||||
|
||||
Please make sure to **base your feature branches and PRs on the `release` branch** instead of `main`, since that's the one that is deployed to the website.
|
||||
|
||||
[**Documentation issues for beginners can be found here.**](https://github.com/wasp-lang/wasp/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3Adocumentation)
|
||||
|
||||
If you'd like to write a blog post about Wasp, please contact us via [Discord](https://discord.gg/zKFDFrsHa9) to discuss the topic and the details.
|
||||
|
||||
Happy hacking!
|
@ -10,9 +10,9 @@ The backend is hosted on Fly.io at https://waspello.fly.dev.
|
||||
# Development
|
||||
|
||||
### Database
|
||||
Wasp needs the Postgres database running. Check out the docs for details on [how to setup PostgreSQL](https://wasp-lang.dev/docs/language/features#postgresql)
|
||||
Wasp needs the Postgres database running.
|
||||
|
||||
You can use `wasp start db` to start a PostgreSQL locally using Docker.
|
||||
Easiest way to do this is to use `wasp start db` to start a PostgreSQL locally using Docker.
|
||||
|
||||
### Env variables
|
||||
Copy `env.server` to `.env.server` and fill in the values.
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Waspleau
|
||||
|
||||
Welcome to the Waspleau example! This is a small Wasp project that tracks status of wasp-lang/wasp repo via a nice looking dashboard.
|
||||
It pulls in data via [Jobs](https://wasp-lang.dev/docs/language/features#jobs) and stores them in the database.
|
||||
It pulls in data via [Jobs](https://wasp-lang.dev/docs/advanced/jobs) and stores them in the database.
|
||||
|
||||
This example project can serve as a good starting point for building your own dashboard with Wasp, that regularly pulls in external data by using Jobs Wasp feature.
|
||||
|
||||
|
@ -372,6 +372,7 @@ If it happens just once every so it is probably nothing to worry about. If it ha
|
||||
- If you modified ChangeLog.md or waspc.cabal, create a PR, wait for approval and all the checks (CI) to pass, then squash and merge mentioned PR into `main`.
|
||||
- Update your local repository state to have all remote changes (`git fetch`).
|
||||
- Update `main` to contain changes from `release` by running `git merge release` while on the `main` branch. Resolve any conflicts.
|
||||
- Take a versioned "snapshot" of the current docs by running `npm run docusaurus docs:version {version}` in the [web](/web) dir. Check the README in the `web` dir for more details. Commit this change to `main`.
|
||||
- Fast-forward `release` to this new, updated `main` by running `git merge main` while on the `release` branch.
|
||||
- Make sure you are on `release` and then run `./new-release 0.x.y.z`.
|
||||
- This will do some checks, tag it with new release version, and push it.
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -319,7 +319,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092"
|
||||
"607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -333,7 +333,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092"
|
||||
"607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -627,7 +627,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092"
|
||||
"607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -375,7 +375,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092"
|
||||
"607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -333,7 +333,7 @@
|
||||
"file",
|
||||
"web-app/src/actions/index.ts"
|
||||
],
|
||||
"3afb54edb61cbc95a9b2133f9b3bdc460ca97580aca700adad988bf0515ab092"
|
||||
"607c3311861456ae47c246a950c8e29593f9837a9f5c48923d99cd7fac1ce0bb"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -42,7 +42,7 @@ export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData:
|
||||
|
||||
/**
|
||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
*/
|
||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
||||
|
||||
@ -116,7 +116,7 @@ type InternalAction<Input, Output> = Action<Input, Output> & {
|
||||
*
|
||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
||||
* object that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns An internally-used optimistic update definition object.
|
||||
*/
|
||||
function translateToInternalDefinition<Item, CachedData>(
|
||||
@ -260,7 +260,7 @@ function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
||||
*
|
||||
* @param querySpecifier A query specifier that's a part of the public API:
|
||||
* https://wasp-lang.dev/docs/language/features#the-useaction-hook.
|
||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
||||
* @returns A cache key React Query internally uses for addressing queries.
|
||||
*/
|
||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
||||
|
@ -2,6 +2,12 @@
|
||||
|
||||
This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
|
||||
|
||||
It consists of three main parts:
|
||||
- Landing page ([src/pages/index.js](src/pages/index.js))
|
||||
- Blog ([blog/](blog/))
|
||||
- Docs ([docs/](docs/))
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
@ -14,7 +20,8 @@ $ npm install
|
||||
$ npm start
|
||||
```
|
||||
|
||||
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
|
||||
This command starts a local development server and opens up a browser window.
|
||||
Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
@ -36,4 +43,52 @@ First, ensure you are on the `release` branch. Next, run:
|
||||
$ GIT_USER=<Your GitHub username> USE_SSH=true npm run deploy
|
||||
```
|
||||
|
||||
This command will build the website and push it to the `gh-pages` branch.
|
||||
This command will build the website and push it to the `gh-pages` branch,
|
||||
which will get it deployed to https://wasp-lang.dev !
|
||||
|
||||
### Multiple documentation versions
|
||||
|
||||
We maintain docs for multiple versions of Wasp.
|
||||
|
||||
Docusaurus docs on this: https://docusaurus.io/docs/versioning .
|
||||
|
||||
Docusaurus recognizes "current" docs, which are docs in ./docs dir, and also
|
||||
individual versioned docs, which are docs under versioned_docs/{version}/.
|
||||
So we have 1 "current" docs and a number of versioned docs.
|
||||
|
||||
We stick with Docusaurus' recommended/default approach for multiple doc versions, which says that "current" docs, which reside in `docs/` dir and are served under `/docs/next` URL, are work in progress (even though they are named "current", which is a bit misleading).
|
||||
So "current" docs are docs for the version of Wasp that is currently in development, not for the last released version, and they are not meant to be consumed by typical Wasp user.
|
||||
|
||||
Each versioned documentation consists of versioned_docs/{version} and
|
||||
versioned_sidebars/{version}.
|
||||
There is also versions.json file which is just a list of versioned docs.
|
||||
|
||||
By default, "current" docs are served under URL {baseUrl}/docs/next,
|
||||
each versioned doc is served under URL {baseUrl}/docs/{version},
|
||||
and last versioned docs (first version in versions.json)
|
||||
are served under URL {baseUrl}/docs/, as the default/latest docs.
|
||||
|
||||
Since we don't want our users to read `docs/next` ("current" docs), we don't publish these when we deploy docs, instead we build them only during development.
|
||||
|
||||
#### When/how do we create new version of docs from "current" docs?
|
||||
|
||||
When releasing new version of Wasp, what we do is run `npm run docusaurus docs:version {version}` to create new versioned docs from the current docs. We do this on every new Wasp release.
|
||||
|
||||
This command does everything for us, and since we use Docusaurus' default settings for versions,
|
||||
there is nothing else we need to do, it will be picked up as the lastest version by default.
|
||||
|
||||
#### Which version of docs should I be editing?
|
||||
|
||||
If you are writing/updating docs on `main` for the new release of Wasp, you should edit "current" docs (docs/).
|
||||
|
||||
If you are (hot)fixing currently published docs on `release`, then you should edit the versioned docs (versioned_docs/{version}), for whatever version you want to do this for. If you want this change to also be present in all the new docs, then you should also do it for the "current" docs (docs/) (yes, that means duplicating the same change).
|
||||
|
||||
Prefer doing doc edits on `main`, as that keeps the whole process simpler, and do changes to docs on `release` only if it really matters to fix the already published versioned docs.
|
||||
|
||||
#### Deleting versions
|
||||
|
||||
We should not keep too many versions of documentation, especially now in Beta when we are moving fast.
|
||||
|
||||
Therefore, we should be quite liberal with deleting the older versions of docs.
|
||||
|
||||
Also, it might make sense to delete the previous version of docs if only bug fixes were done in the latest version.
|
||||
|
@ -60,7 +60,7 @@ This is one of the features we are most excited about! Now, when you define an e
|
||||
|
||||
This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started!
|
||||
|
||||
For more details, [check out our docs on reusing entity types on both a client and a server](/docs/typescript#entity-types).
|
||||
For more details, [check out our entity docs](/docs/data-model/entities).
|
||||
|
||||
## 🗓 We set a date for the next launch - April 11th! 🚀
|
||||
|
||||
|
@ -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](/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/advanced/web-sockets), 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.
|
||||
|
||||
@ -885,4 +885,4 @@ And if you know of a better, cooler, sleeker way of implementing WebSockets into
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 7.png"
|
||||
width="550px"
|
||||
/>
|
||||
/>
|
||||
|
189
web/blog/2023-11-21-guide-windows-development-wasp-wsl.md
Normal file
@ -0,0 +1,189 @@
|
||||
---
|
||||
title: 'A Guide to Windows Development with Wasp & WSL'
|
||||
authors: [martinovicdev]
|
||||
image: /img/wsl-guide/wsl-guide-banner.jpeg
|
||||
tags: [wsl, windows, tutorial]
|
||||
---
|
||||
|
||||
import Link from '@docusaurus/Link';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import ImgWithCaption from './components/ImgWithCaption'
|
||||
|
||||
<ImgWithCaption
|
||||
alt="WSL Guide Banner"
|
||||
source="/img/wsl-guide/wsl-guide-banner.jpeg"
|
||||
/>
|
||||
|
||||
If you are having a hard time with Wasp development on Windows, don't be afraid! We will go through all necessary steps to set up your dev environment and get you started with Wasp development in Windows in no time.
|
||||
|
||||
## What is WSL and why should I be interested in it?
|
||||
|
||||
Windows Subsystem for Linux (or WSL) lets developers run a fully functional and native GNU/Linux environment directly on Windows. In other words, we can run Linux directly without using a virtual machine or dual-booting the system.
|
||||
|
||||
**The first cool thing about it is that WSL allows you to never switch OS’s, but still have the best of both worlds inside your OS.**
|
||||
What does that mean for us regular users? When you look at the way WSL works in practice, it can be considered a Windows feature that runs a Linux OS directly inside Windows 10 or 11, with a fully functional Linux file system, Linux command line tools, and Linux GUI apps (_really cool, btw_). Besides that, it uses much fewer resources for running when compared to a virtual machine and also doesn’t require a separate tool for creating and managing those virtual machines.
|
||||
|
||||
WSL is mainly catered to developers, so this article will be focused on developer usage and how to set up a fully working dev environment with VS Code. Inside this article, we’ll go through some of the cool features and how they can be used in practice. Plus, the best way to understand new things is to actually start using them.
|
||||
|
||||
## Installing WSL on the Windows operating system
|
||||
|
||||
In order to install WSL on your Windows, first enable [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v) architecture is Microsoft’s hardware virtualization solution. To install it, right-click on the Windows Terminal/Powershell and open it in Administrator mode.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6wm5xniz2nehrccczeh6.png)
|
||||
|
||||
Then, run the following command:
|
||||
|
||||
```bash
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
|
||||
```
|
||||
|
||||
That will ensure that you have all the prerequisites for the installation. Then, open the Powershell (best done in Windows Terminal) in the Administrator mode. Then, run
|
||||
|
||||
```bash
|
||||
wsl —install
|
||||
```
|
||||
|
||||
There is a plethora of Linux distributions to be installed, but Ubuntu is the one installed by default. This guide will feature many console commands, but most of them will be a copy-paste process.
|
||||
|
||||
If you have installed Docker before, there is a decent chance that you have WSL 2 installed on your system already. In that case, you will get a prompt to install the distribution of your choice. Since this tutorial will be using Ubuntu, I suggest running.
|
||||
|
||||
```bash
|
||||
wsl --install -d Ubuntu
|
||||
```
|
||||
|
||||
After installing Ubuntu (or another distro of your choice), you will enter your Linux OS and be prompted with a welcome screen. There, you will enter some basic info. First, you will enter your username and after that your password. Both of those will be Linux-specific, so you don’t necessarily have to repeat your Windows credentials. After we’ve done this, the installation part is over! You have successfully installed Ubuntu on your Windows machine! It still feels weird to say this, right?
|
||||
|
||||
### Cool WSL featues to help you along the way
|
||||
|
||||
But before we get down to our dev environment setup, I want to show you a couple of cool tricks that will make your life easier and help you understand why WSL is actually a game-changer for Windows users.
|
||||
|
||||
The first cool thing with WSL is that you don’t have to give up the current way of managing files through Windows Explorer. In your sidebar in Windows Explorer, you can find the Linux option now right under the network tab.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/647jdnzilrucsijtye3v.png)
|
||||
|
||||
From there, you can access and manage your Linux OS’s file system directly from the Windows Explorer. What is really cool with this feature is that you can basically copy, paste, and move files between different operating systems without any issues, which opens up a whole world of possibilities. Effectively, you don’t have to change much in your workflow with files and you can move many projects and files from one OS to another effortlessly. If you download an image for your web app on your Windows browser, just copy and paste it to your Linux OS.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqjsd1oz5a4alu6q08re.png)
|
||||
|
||||
Another very important thing, which we will use in our example is WSL2 virtual routes. As you now have OS inside your OS, they have a way of communicating. When you want to access your Linux OS’s network (for example, when you want to access your web app running locally in Linux), you can use _${PC-name}.local_. For me, since my PC name is Boris-PC, my network address is boris-pc.local. That way you don’t have to remember different IP addresses, which is really cool. If you want your address for whatever reason, you can go to your Linux distro’s terminal, and type ipconfig. Then, you can see your Windows IP and Linux’s IP address. With that, you can communicate with both operating systems without friction.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkhcfiybnobuoziitwtm.png)
|
||||
|
||||
The final cool thing I want to highlight is Linux GUI apps. It is a very cool feature that helps make WSL a more attractive proposal for regular users as well. You can install any app you want on your Linux system using popular package managers, such as apt (default on Ubuntu) or flatpak. Then you can launch them as well from the command line and the app will start and be visible inside your Windows OS. But that can cause some friction and is not user-friendly. The really ground-breaking part of this feature is that you can launch them directly from your Windows OS without even starting WSL yourself. Therefore, you can create shortcuts and pin them to the Start menu or taskbar without any friction and really have no need to think about where your app comes from. For the showcase, I have installed Dolphin File Manager and run it through Windows OS. You can see it action below side by side with Windows Explorer.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yq1nxj244jd1fci13oay.png)
|
||||
|
||||
## Getting started with development on WSL
|
||||
|
||||
After hearing all about the cool features of WSL, let’s slowly get back on track with our tutorial. Next up is setting up our dev environment and starting our first app. I’ll be setting up a web dev environment and we’ll use [Wasp](https://wasp-lang.dev/) as an example.
|
||||
|
||||
If you aren’t familiar with it, Wasp is a Rails-like framework for React, Node.js, and Prisma. It’s a fast and easy way to develop and deploy your full-stack web apps. For our tutorial, Wasp is a perfect candidate, since it doesn’t support Windows development natively, but only through WSL as it requires a Unix environment.
|
||||
|
||||
Let’s get started with installing Node.js first. At the moment, Wasp requires users to use the Node v18 (version requirement will be relaxed very soon), so we want to start with both Node.js and NVM installation.
|
||||
|
||||
But first things first, let’s start with Node.js. In WSL, run:
|
||||
|
||||
```jsx
|
||||
sudo apt install nodejs
|
||||
```
|
||||
|
||||
in order to install Node on your Linux environment. Next up is NVM. I suggest going to https://github.com/nvm-sh/nvm and getting the latest install script from there. The current download is:
|
||||
|
||||
```bash
|
||||
curl -o- [https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh](https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh) | bash
|
||||
```
|
||||
|
||||
After this, we have both Node.js and NVM set up in our system.
|
||||
|
||||
### Installing Wasp
|
||||
|
||||
Next up is installing Wasp on our Linux environment. Wasp installation is also pretty straightforward and easy. So just copy and paste this command:
|
||||
|
||||
```bash
|
||||
curl -sSL [https://get.wasp-lang.dev/installer.sh](https://get.wasp-lang.dev/installer.sh) | sh
|
||||
```
|
||||
|
||||
and wait for the installer to finish up its thing. Great! But, if you did your WSL setup from 0, you will notice the following warning underneath: It looks like '/home/boris/.local/bin' is not on your PATH! You will not be able to invoke wasp from the terminal by its name.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/em932e89tlzajv4rm6up.png)
|
||||
|
||||
Let’s fix this quickly. In order to do this, let’s run
|
||||
|
||||
```bash
|
||||
code ~/.profile
|
||||
```
|
||||
|
||||
If we don’t already have VS Code, it will automatically set up everything needed and boot up so you can add the command to the end of your file. It will be different for everyone depending on their system name. For example, mine is:
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:/home/boris/.local/bin
|
||||
```
|
||||
|
||||
Great! Now we just need to swap node version to v18.14.2 to ensure full compatibility with Wasp. We’ll install and switch to Node 18 in one go! To do this, simply run:
|
||||
|
||||
```bash
|
||||
nvm install v18.14.2 && nvm use v18.14.2
|
||||
```
|
||||
|
||||
### Setting up VS Code
|
||||
|
||||
After setting up Wasp, we want to see how to run the app and access it from VS Code. Under the hood, you will still be using WSL for our development, but we’ll be able to use our VS Code from Host OS (Windows) for most of the things.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/orifa202sph4swgbir2d.png)
|
||||
|
||||
To get started, download the [WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) to your VS Code in Windows. Afterward, let’s start a new Wasp project to see how it works in action. Open your VS Code Command Palette (ctrl + shift + P) and select the option to “Open Folder in WSL”.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1le8xvk6a8a8teog8eo.png)
|
||||
|
||||
The folder that I have opened is
|
||||
|
||||
```bash
|
||||
\\wsl.localhost\Ubuntu\home\boris\Projects
|
||||
```
|
||||
|
||||
That is the “Projects” folder inside my home folder in WSL. There are 2 ways for us to know that we are in WSL: The top bar and in the bottom left corner of VS Code. In both places, we have WSL: Ubuntu written, as is shown on screenshots.
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzhu765415sravn3vypu.png)
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cpy4kggtsobod1vk1dqn.png)
|
||||
|
||||
Once inside this folder, I will open a terminal. It will also be already connected to the proper folder in WSL, so we can get down to business! Let’s run the
|
||||
|
||||
```bash
|
||||
wasp new
|
||||
```
|
||||
|
||||
command to create a new Wasp application. I have chosen the basic template, but you are free to create a project of your choosing, e.g. [SaaS starter](https://github.com/wasp-lang/SaaS-Template-GPT) with GPT, Stripe and more preconfigured. As shown in the screenshot, we should change the current directory of our project to the proper one and then run our project with it.
|
||||
|
||||
```bash
|
||||
wasp start
|
||||
```
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l453mcae56kfa3yrm7j4.png)
|
||||
|
||||
And just like that, a new screen will open on my Windows machine, showcasing that my Wasp app is open. Cool! My address is still the default localhost:3000, but it is being run from the WSL. Congratulations, you’ve successfully started your first Wasp app through WSL. That wasn’t hard, was it?
|
||||
|
||||
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vfyfok2eg0xjhqcqhgoe.png)
|
||||
|
||||
For our final topic, I want to highlight Git workflow with WSL, as it is relatively painless to set up. You can always do the manual git config setup, but I have something cooler for you: Sharing credentials between Windows and WSL. To set up sharing Git credentials, we have to do the following. In Powershell (on Windows), configure the credential manager on Windows.
|
||||
|
||||
```bash
|
||||
git config --global credential.helper wincred
|
||||
```
|
||||
|
||||
And let’s do the same inside WSL.
|
||||
|
||||
```bash
|
||||
git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"
|
||||
```
|
||||
|
||||
This allows us to share our Git username and password. Anything set up in Windows will work in WSL (and vice-versa) and we can use Git inside WSL as we prefer (via VS Code GUI or via shell).
|
||||
|
||||
## Conclusion
|
||||
|
||||
Through our journey here, we have learned what WSL is, how it can be useful for enhancing our workflow with our Windows PC, but also how to set up your initial development environment on it. Microsoft has done a fantastic job with this tool and has really made Windows OS a much more approachable and viable option for all developers. We went through how to install the dev tools needed to kickstart development and how to get a handle on a basic dev workflow. Here are some important links if you want to dive deeper into the topic:
|
||||
|
||||
- [https://github.com/microsoft/WSL](https://github.com/microsoft/WSL)
|
||||
- [https://learn.microsoft.com/en-us/windows/wsl/install](https://learn.microsoft.com/en-us/windows/wsl/install)
|
||||
- [https://code.visualstudio.com/docs/remote/wsl](https://code.visualstudio.com/docs/remote/wsl)
|
190
web/blog/2023-12-05-writing-rfcs.md
Normal file
@ -0,0 +1,190 @@
|
||||
---
|
||||
title: "On the Importance of RFCs in Programming"
|
||||
authors: [matijasos]
|
||||
image: /img/writing-rfcs/rfc-prophet.png
|
||||
tags: [programming, clean-code]
|
||||
---
|
||||
|
||||
import ImgWithCaption from './components/ImgWithCaption'
|
||||
|
||||
Imagine you’ve been tasked to implement a sizeable new feature for the product you’re working on. That’s the opportunity you’ve been waiting for - everybody will see what a 10x developer you are! You open a list of the coolest new libraries and design patterns you’ve wanted to try out and get right into it, full “basement” mode. One week later, you victoriously emerge and present your perfect pull request!
|
||||
|
||||
**But then, the senior dev in a team immediately rejects it - *“Too complex, you should have simply used library X and reused Y.”***. What!? Before you know it, you’re looking at 100 comments on your PR and days of refactoring to follow.
|
||||
|
||||
If only there were **a way of knowing about X and Y before implementing everything**. Well, it is, and it’s called RFC!
|
||||
|
||||
|
||||
<ImgWithCaption
|
||||
alt="The revelation of RFC"
|
||||
source="img/writing-rfcs/rfc-prophet.png"
|
||||
/>
|
||||
|
||||
We’ll learn about it through the example of [RFC about implementing an authentication system in a web framework Wasp](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4). [Wasp](https://github.com/wasp-lang/wasp) is a full-stack web framework built on top of React, Node.js and Prisma. It is used by [MAGE](https://usemage.ai/), a free GPT-powered codebase generator, which has been used to start over 30,000 applications.
|
||||
|
||||
Let's dive in!
|
||||
|
||||
## So, what is an RFC?
|
||||
|
||||
RFC (*Request For Comments*) is, simply explained, a document proposing a codebase change to solve a specific problem. **Its main purpose is to find the best way to solve a problem, as a team effort, before the implementation starts**. RFCs were first adopted by the open-source community, but today, they are used in almost any type of developer organization.
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC overivew"
|
||||
source="img/writing-rfcs/rfc-overview.png"
|
||||
caption="A simplified schema of a typical RFC."
|
||||
/>
|
||||
|
||||
There are other names for this type of document you might encounter in the industry, like TDD (*Technical Design Document*) or SDD (*Software Design Document*). Some people argue over the distinction between them, but we won’t.
|
||||
|
||||
**Fun fact**: RFCs were invented by IETF (*Internet Engineering Task Force*), the engineering organization behind some of the most important internet standards and protocols we use today, like TCP/IP! Not too shabby, right?
|
||||
|
||||
## When should I write RFC, and when can I skip it?
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC overivew"
|
||||
source="img/writing-rfcs/rfc-meme-when.png"
|
||||
/>
|
||||
|
||||
So, why bother writing about what you will eventually code, instead of saving time and simply doing it? **If you’re dealing with a bug or a relatively simple feature, where it’s very clear what you must do and doesn’t affect project structure, then there’s no need for an RFC - fire up that IDE and get cracking!**
|
||||
|
||||
But, if you are introducing a completely new concept (e.g., introducing a role-based permission system) or altering the project’s architecture (e.g., adding support for running background jobs), then you might want to take a step back before typing `git checkout -b my-new-feature` and diving into that sweet coding zone.
|
||||
|
||||
All the above being said, sometimes it's not easy to figure out if you should write an RFC or not. Maybe it’s a more prominent feature, but you’ve done something similar before, and you’ve already mapped everything out in your head and pretty much have no questions. To help with that, here’s a simple heuristic I like to use: **Is there more than one obvious way to implement this feature? Is there a new library/service we have to pick?** If the answer to both of these is “No", you probably don’t need an RFC. Otherwise, there’s a discussion to be had, and RFC is the way to do it.
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC decision flowchart"
|
||||
source="img/writing-rfcs/rfc-flowchart.png"
|
||||
/>
|
||||
|
||||
## It sounds useful. But what’s in it for me?
|
||||
|
||||
We’ve established how to decide *when* to write an RFC, but here is also *why* you should do it:
|
||||
|
||||
- **You will organize your thoughts and get clarity**. If you’ve decided to write an RFC, that means you’re dealing with a non-trivial, open-ended problem. Writing things down will help distill your thoughts and have an objective look at them.
|
||||
- **You will learn more** than if you just jumped into coding. You will give yourself space to explore different approaches and oftentimes discover something you haven’t even thought of initially.
|
||||
- **You will crowdsource your team’s knowledge.** By asking your team for feedback (hence Request For Comments), you will get a complete picture of the problem you’re solving and fill in any remaining gaps.
|
||||
- **You will advance your team’s understanding of the codebase.** By collaborating on your RFC, everybody on the team will understand what you’re doing and how you eventually did it. That means next time somebody has to touch that part of the code, they will need to ask you much less questions (=== more uninterrupted coding time!).
|
||||
- **PR reviews will go *much* smoother**. Remember that situation from the beginning of this article, when your PR got rejected as "too complex"? That’s because the reviewer is missing the context, and you made a sizeable change without a previous buy-in from the rest of the team. By writing an RFC first, you’ll never encounter this type of situation again.
|
||||
- **Your documentation is already 50% done!** To be clear, RFC is not the final documentation, and you cannot simply point to it, but you can likely reuse a lot - images, diagrams, paragraphs, etc.
|
||||
|
||||
Wow, this sounds so good that I want to come up with a new feature right now just so I can write an RFC for it! Joke aside, going through with the RFC first makes the coding part so much more enjoyable - you know exactly what you need to do, and you don’t need to question your approach and how it will be received once you create that PR.
|
||||
|
||||
## Ok, ok, I’m sold! So, how do I go about writing one?
|
||||
|
||||
Glad you asked! Many different formats are being used, more or less formal, but I prefer to keep it simple. RFCs that we write at Wasp don’t follow a strict format, but there are some common parts:
|
||||
|
||||
- **Metadata** - Title, date, reviewers, etc…
|
||||
- **Problem / Goal**
|
||||
- **Proposed solution** (or more of them)
|
||||
- **Implementation overview**
|
||||
- **Remarks / open questions**
|
||||
|
||||
That’s pretty much the gist of it! Each of these can be further broken down and refined, but this is the basic outline you can start with.
|
||||
|
||||
Let’s now go over each of these and see what they look like in practice, on our [Authentication in Wasp](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4) example.
|
||||
|
||||
## Metadata ⌗
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC metadata"
|
||||
source="img/writing-rfcs/rfc-metadata.png"
|
||||
/>
|
||||
|
||||
This one is pretty self-explanatory - you will want to track some basic info about your RFCs - status, date of creation, etc.
|
||||
|
||||
Some templates also explicitly list the reviewers and the status of their “approval” of the RFC, similar to the PR review process - we don’t have it since we’re a small team where communication happens fast, but it can be handy for larger teams where not everybody knows everybody, and you want to have a bit more of a process in place (e.g. when mentoring junior developers).
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC reviewer status"
|
||||
source="img/writing-rfcs/rfc-reviewer-status-example.png"
|
||||
caption="Some RFCs require explicit approval by each reviewer."
|
||||
/>
|
||||
|
||||
## The problem 🤔
|
||||
|
||||
This is where things get interesting. **The better you define the problem or the goal/feature you need to implement, and why you need to do it, the easier all the following steps will be**. So this is something worth investing in even before you start writing your RFC - make sure you talk to all the involved parties (e.g., product owner, other developers, and even users) to refine your understanding of the issue you’re about to tackle.
|
||||
|
||||
By doing this, you will also very likely get first hints and pointers on the possible solutions, and develop a rough sense of the problem space you’re in.
|
||||
|
||||
<ImgWithCaption
|
||||
alt="RFC problem definition"
|
||||
source="img/writing-rfcs/rfc-problem.png"
|
||||
/>
|
||||
|
||||
Here are a few tips from the example above:
|
||||
|
||||
- **Start with a high-level summary** - that way, readers can quickly decide if this is relevant to them or not and whether they should keep reading.
|
||||
- **Provide some context** - Explain a bit about the current state of the world, as it is right now. This can be a single sentence or a whole chapter, depending on the intended audience.
|
||||
- **Clearly state the problem/goal** - explain why there is a problem and connect it with the user’s/company’s pain, so that motivation is clear.
|
||||
- **Provide extra details if possible** - diagrams, code examples, … → anything that can help the reader get faster to that “aha” moment. Extra points for using collapsible sections, so the central part of the RFC remains of digestible length.
|
||||
|
||||
If you did all this, you’re already well on your way to the excellent RFC! Since defining the problem well is essential, don’t be afraid to add more to it and break things down further.
|
||||
|
||||
### Non-goals 🛑
|
||||
|
||||
This is the sub-section of the "Problem" or "Goal" section that can sometimes be super valuable. Writing what we don't want or will not be doing in this codebase change can help set the expectations and better define its scope.
|
||||
|
||||
For example, if we are working on adding a role-based authentication system to our app, people might assume that we will also build some sort of an admin panel for it to manage users and add/remove roles. By explicitly stating it won't be done (and briefly explaining why - not needed, it would take too long, it will be done in the next iteration, ...), reviewers will get a better understanding of what your goal is and you will skip unnecessary discussion.
|
||||
|
||||
## Solution & Implementation 🛠️
|
||||
|
||||
Once we know what we want to do, we have to figure out the best way of doing it! You might have already hinted at the possible solution in the Problem section, but now is the moment to dive deeper - research different approaches, evaluate their pros and cons, and sketch how they could fit into the existing system.
|
||||
|
||||
This section is probably the most free-form of all - since it highly depends on the nature of what you are doing, it doesn’t make sense to impose many restrictions here. You may want to stay at the higher level of, e.g., system architecture, or you may need to dive deep into the code and start writing parts of the code you will need. Due to that, I don’t have an exact format for you to follow, but rather a set of guidelines:
|
||||
|
||||
### Write pseudocode
|
||||
|
||||
The purpose of RFC is to convey ideas and principles, not production-grade code that compiles and covers all the edge cases. Feel free to invent/imagine/sketch whatever you need (e.g., imagine you already have a function that sends an email and just use it, even if you don’t), and don’t encumber yourself or the reader with the implementation details (unless that’s exactly what the RFC is about).
|
||||
|
||||
It’s better to start at the higher level, and then go deeper when you realize you need it or if one of the reviewers suggests it.
|
||||
|
||||
### Find out how are others doing it
|
||||
|
||||
<ImgWithCaption
|
||||
alt="See what others are doing"
|
||||
source="img/writing-rfcs/existing-solutions.png"
|
||||
/>
|
||||
|
||||
How you find this out may differ depending on the type of product you’re developing, but there is almost always a way to do it. If you’re developing an open-source tool like [Wasp](https://github.com/wasp-lang/wasp) you can simply check out other popular solutions (that are also open-source) and learn how they did it. If you’re working on a SaaS and need to figure out whether to use cookies or JWTs for the authentication, you likely have some friends who have done it before, and you can ask them. Lastly, simply Google/GPT it.
|
||||
|
||||
Why is this so helpful? **The reason is that it gives you (and the reviewers) confidence in your solution. If somebody else did it successfully this way, it might be a promising direction.** It also might help you discover approaches you haven’t thought of before, or serve as a basis on top of which you can build. Of course, never take anything for granted and take into account the specific needs of your situation, but definitely make use of the knowledge and expertise of others.
|
||||
|
||||
### Leave things unfinished & don't make it perfect
|
||||
|
||||
The main point of RFC is the “C” part, so collaboration (yes, I know it actually stands for "_comments_"). **RFC is not a test where you have to get the perfect score and have no questions asked - if that happens, you probably shouldn’t have written it in the first place.**
|
||||
|
||||
Solving a problem is a team effort, and you’re just the person taking the first stab at it and pushing things forward. Your task is to lay as much groundwork as you reasonably can (refine the problem, explore multiple approaches to solving it, identify new subproblems that came to light) so the reviewers can quickly grasp the status and provide efficient feedback, directed where it’s needed the most.
|
||||
|
||||
**The main job of your RFC is to identify the most important problems and direct the reviewer’s attention to them, not solve them.**
|
||||
|
||||
The RFC you’re writing should be looked at as a discussion area and a work-in-progress, not a piece of art that has to be perfected before it’s displayed in front of the audience.
|
||||
|
||||
## Remarks & open questions 🎯
|
||||
|
||||
In this final section of the document, you can summarise the main thoughts and highlight the biggest open questions. After going through everything, it can be helpful for the reader to be reminded of where his attention can be most valuable.
|
||||
|
||||
## Now I know when and how to write an RFC! Do you have any templates I could use as a starting point?
|
||||
|
||||
Of course! As mentioned, our format is extremely lightweight, but feel free to take a look at [the RFC we used as an example](https://wasp-lang.notion.site/RFC-Auth-without-user-defined-entities-6d2925439627456ab01b74ff4b4cd087?pvs=4) to get inspired. Your company could also already have a ready template they recommend.
|
||||
|
||||
Here are a few you can use and/or adapt to your needs:
|
||||
|
||||
- [Squarespace RFC template](https://engineering.squarespace.com/s/Squarespace-RFC-Template.pdf)
|
||||
- _Do you have a template you would recommend? I'm happy to list it here!_
|
||||
|
||||
## What tool should I use to write my RFCs? There are so many choices!
|
||||
|
||||
The exact tool you’re using is probably the least important part of RFC-ing, but it still matters since it sets the workflow around it. If your company has already selected a tool, then of course stick with that. If not, here are the most common choices I’ve come across, along with quick comments:
|
||||
|
||||
- **Google Docs** - the classic choice. Super easy to comment on any part of the doc, which is the most important feature.
|
||||
- **Notion** - also great for collaboration, plus offers some markdown components such as collapsibles and tables, which can make your RFC more readable.
|
||||
- **GitHub issues / PRs** - this is sometimes used, especially for OSS projects. The drawback is that it is harder to comment on the specific part of the document (you can only comment on the whole line), plus inserting diagrams is also quite clunky. The pro is that everything (code and RFCs) stays on the same platform
|
||||
|
||||
We currently use Notion, but any of the above can be a good choice.
|
||||
|
||||
## Summary
|
||||
|
||||
Just as it is the best practice to write a summary at the end of your RFC, we will do the same here! This article came out longer than I expected, but there were so many things to mention - I hope you'll find it useful!
|
||||
|
||||
Finally, **being able to clearly express your thoughts, formulate the problem, and objectively analyze the possible solutions, with feedback from the team, is what will help you develop the right thing, which is the ultimate productivity hack**. This is how you become a 10x engineer.
|
||||
|
||||
And don't forget: *Weeks of coding can save you hours of planning.*
|
@ -3,12 +3,14 @@ martinsos:
|
||||
title: Co-founder & CTO @ Wasp
|
||||
url: https://github.com/martinsos
|
||||
image_url: https://github.com/martinsos.png
|
||||
email: martin@wasp-lang.dev
|
||||
|
||||
matijasos:
|
||||
name: Matija Sosic
|
||||
title: Co-founder & CEO @ Wasp
|
||||
url: https://github.com/matijasos
|
||||
image_url: https://github.com/matijasos.png
|
||||
email: matija@wasp-lang.dev
|
||||
|
||||
shayneczyzewski:
|
||||
name: Shayne Czyzewski
|
||||
@ -21,6 +23,7 @@ sodic:
|
||||
title: Founding Engineer @ Wasp
|
||||
url: https://github.com/sodic
|
||||
image_url: https://github.com/sodic.png
|
||||
email: filip@wasp-lang.dev
|
||||
|
||||
maksym36ua:
|
||||
name: Maksym Khamrovskyi
|
||||
@ -37,9 +40,17 @@ vinny:
|
||||
title: DevRel @ Wasp
|
||||
url: https://vincanger.github.io
|
||||
image_url: https://vincanger.github.io/assets/vince_smiley.jpg
|
||||
email: vince@wasp-lang.dev
|
||||
|
||||
miho:
|
||||
name: Mihovil Ilakovac
|
||||
title: Founding Engineer @ Wasp
|
||||
url: https://ilakovac.com
|
||||
image_url: https://github.com/infomiho.png
|
||||
email: miho@wasp-lang.dev
|
||||
|
||||
martinovicdev:
|
||||
name: Boris Martinović
|
||||
title: Contributor @ Wasp
|
||||
url: https://martinovic.dev
|
||||
image_url: https://github.com/martinovicdev.png
|
||||
|
@ -1,25 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -5,7 +5,7 @@ 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.
|
||||
In Wasp, the default client-server interaction mechanism is through [Operations](../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
|
||||
|
||||
@ -231,11 +231,11 @@ export const apiMiddleware: MiddlewareConfigFn = (config) => {
|
||||
|
||||
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)
|
||||
For more information about middleware configuration, please see: [Middleware Configuration](../advanced/middleware-config)
|
||||
|
||||
## Using Entities in APIs
|
||||
|
||||
In many cases, resources used in APIs will be [Entities](/docs/data-model/entities.md).
|
||||
In many cases, resources used in APIs will be [Entities](../data-model/entities.md).
|
||||
To use an Entity in your API, add it to the `api` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
@ -340,4 +340,4 @@ The `api` declaration has the following fields:
|
||||
|
||||
- `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.
|
||||
The import statement to an Express middleware config function for this API. See more in [middleware section](../advanced/middleware-config) of the docs.
|
@ -1,4 +1,3 @@
|
||||
:::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 additionally set the necessary environment variables specifically required by these method(s).
|
||||
If your app is using an external authentication method(s) supported by Wasp (such as [Google](../../auth/social-auth/google#4-adding-environment-variables) or [GitHub](../../auth/social-auth/github#4-adding-environment-variables)), make sure to additionally set the necessary environment variables specifically required by these method(s).
|
||||
:::
|
||||
|
@ -35,7 +35,7 @@ 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.
|
||||
You'll have to [switch to PostgreSQL](../../data-model/backends#migrating-from-sqlite-to-postgresql) before deploying to production.
|
||||
:::
|
||||
|
||||
### 2. Deploying the API Server (backend)
|
||||
@ -98,7 +98,7 @@ We'll cover a few different deployment providers below:
|
||||
We will show how to deploy the server and provision a database for it on 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).
|
||||
If you want to do all of the work below with one command, you can use the [Wasp CLI](../../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.
|
||||
@ -559,7 +559,7 @@ 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.
|
||||
If you wish to deploy an app leveraging [Jobs](../../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
|
||||
:::
|
||||
|
@ -11,7 +11,7 @@ Wasp apps are full-stack apps that consist of:
|
||||
|
||||
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).
|
||||
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](../../advanced/deployment/cli).
|
||||
|
||||
<DeploymentOptionsGrid />
|
||||
|
||||
|
@ -94,7 +94,7 @@ Let's write an example Job that will print a message to the console and return a
|
||||
`MySpecialJob` is a generic type Wasp generates to help you correctly type the Job's worker function, ensuring type information about the function's arguments and return value. Read more about type-safe jobs in the [Javascript API section](#javascript-api).
|
||||
</ShowForTs>
|
||||
|
||||
3. After successfully defining the job, you can submit work to be done in your [Operations](/docs/data-model/operations/overview) or [setupFn](/docs/project/server-config#setup-function) (or any other NodeJS code):
|
||||
3. After successfully defining the job, you can submit work to be done in your [Operations](../data-model/operations/overview) or [setupFn](../project/server-config#setup-function) (or any other NodeJS code):
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
@ -333,7 +333,7 @@ The Job declaration has the following fields:
|
||||
|
||||
- `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)).
|
||||
A list of entities you wish to use inside your Job (similar to [Queries and Actions](../data-model/operations/queries#using-entities-in-queries)).
|
||||
|
||||
### JavaScript API
|
||||
|
||||
|
@ -19,7 +19,7 @@ Wasp's Express server has the following middleware by default:
|
||||
- [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.
|
||||
JSON middlware is required for [Operations](../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.
|
||||
|
@ -240,7 +240,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx
|
||||
### 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).
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
@ -418,7 +418,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</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).
|
||||
We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](../auth/ui).
|
||||
|
||||
### 5. Set up an Email Sender
|
||||
|
||||
@ -461,15 +461,15 @@ app myApp {
|
||||
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).
|
||||
If you are not sure how to get a SendGrid API key, read more [here](../advanced/email#getting-the-api-key).
|
||||
|
||||
Read more about setting up email senders in the [sending emails docs](/docs/advanced/email).
|
||||
Read more about setting up email senders in the [sending emails 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).
|
||||
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](../auth/overview).
|
||||
|
||||
## Login and Signup Flows
|
||||
|
||||
@ -500,7 +500,7 @@ Some of the behavior you get out of the box:
|
||||
|
||||
4. Password validation
|
||||
|
||||
Read more about the default password validation rules and how to override them in [using auth docs](/docs/auth/overview).
|
||||
Read more about the default password validation rules and how to override them in [using auth docs](../auth/overview).
|
||||
|
||||
## Email Verification Flow
|
||||
|
||||
@ -597,7 +597,7 @@ The content of the e-mail can be customized, read more about it [here](#password
|
||||
|
||||
## 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).
|
||||
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](../auth/overview).
|
||||
|
||||
## API Reference
|
||||
|
||||
|
@ -5,7 +5,13 @@ 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 🙃.
|
||||
Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box.
|
||||
|
||||
Here's a 1-minute tour of how full-stack auth works in Wasp:
|
||||
|
||||
<div className='video-container'>
|
||||
<iframe src="https://www.youtube.com/embed/Qiro77q-ulI?si=y8Rejsbjb1HJC6FA" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
Enabling auth for your app is optional and can be done by configuring the `auth` field of the `app` declaration.
|
||||
|
||||
@ -72,11 +78,11 @@ Wasp supports the following auth methods:
|
||||
|
||||
<AuthMethodsGrid />
|
||||
|
||||
Let's say we enabled the [Username & password](/docs/auth/username-and-pass) authentication.
|
||||
Let's say we enabled the [Username & password](../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 get an auth backend with signup and login endpoints. We also get the `user` object in our [Operations](../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 would also get the [Auth UI](../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.
|
||||
|
||||
@ -295,7 +301,7 @@ Since the `user` prop is only available in a page's React component: use the `us
|
||||
|
||||
#### 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.
|
||||
When authentication is enabled, all [queries and actions](../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">
|
||||
@ -355,7 +361,7 @@ export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
|
||||
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).
|
||||
When using WebSockets, the `user` object is also available on the `socket.data` object. Read more in the [WebSockets section](../advanced/web-sockets#websocketfn-function).
|
||||
|
||||
## User entity
|
||||
|
||||
@ -415,7 +421,7 @@ 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:
|
||||
If you use [Username & password](../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
|
||||
@ -424,7 +430,7 @@ Note that `username`s are stored in a **case-sensitive** manner.
|
||||
|
||||
#### Email
|
||||
|
||||
If you use [Email](/docs/auth/email) authentication, the default validations are:
|
||||
If you use [Email](../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
|
||||
@ -663,7 +669,7 @@ Now that we defined the fields, Wasp knows how to:
|
||||
1. Validate the data sent from the client
|
||||
2. Save the data to the database
|
||||
|
||||
Next, let's see how to customize [Auth UI](/docs/auth/ui) to include those fields.
|
||||
Next, let's see how to customize [Auth UI](../auth/ui) to include those fields.
|
||||
|
||||
### 2. Customizing the Signup Component
|
||||
|
||||
@ -673,8 +679,8 @@ If you are not using Wasp's Auth UI, you can skip this section. Just make sure t
|
||||
|
||||
Read more about using the signup actions for:
|
||||
|
||||
- email auth [here](/docs/auth/email#fields-in-the-email-dict) <!-- TODO: these docs are not great at explaining using signup and login actions: https://github.com/wasp-lang/wasp/issues/1438 -->
|
||||
- username & password auth [here](/docs/auth/username-and-pass#customizing-the-auth-flow)
|
||||
- email auth [here](../auth/email#fields-in-the-email-dict) <!-- TODO: these docs are not great at explaining using signup and login actions: https://github.com/wasp-lang/wasp/issues/1438 -->
|
||||
- username & password auth [here](../auth/username-and-pass#customizing-the-auth-flow)
|
||||
:::
|
||||
|
||||
If you are using Wasp's Auth UI, you can customize the `SignupForm` component by passing the `additionalFields` prop to it. It can be either a list of extra fields or a render function.
|
||||
@ -983,7 +989,7 @@ psl=}
|
||||
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.
|
||||
See [Google docs](../auth/social-auth/google) and [GitHub docs](../auth/social-auth/github) for more details.
|
||||
|
||||
#### `methods: dict` <Required />
|
||||
|
||||
@ -994,7 +1000,7 @@ A dictionary of auth methods enabled for the app.
|
||||
#### `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.
|
||||
Check out these [essentials docs on auth](../tutorial/auth#adding-auth-to-the-project) to see an example of usage.
|
||||
|
||||
#### `onAuthSucceededRedirectTo: String`
|
||||
|
||||
@ -1002,7 +1008,7 @@ The route to which Wasp will send a successfully authenticated after a successfu
|
||||
The default value is `"/"`.
|
||||
|
||||
:::note
|
||||
Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](/docs/auth/ui).
|
||||
Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](../auth/ui).
|
||||
:::
|
||||
|
||||
#### `signup: SignupOptions`
|
||||
|
@ -5,6 +5,6 @@ Provider-specific behavior comes down to implementing two functions.
|
||||
|
||||
The reference shows how to define both.
|
||||
|
||||
For behavior common to all providers, check the general [API Reference](/docs/auth/overview.md#api-reference).
|
||||
For behavior common to all providers, check the general [API Reference](../../auth/overview.md#api-reference).
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
||||
|
@ -1,3 +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).
|
||||
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](../../auth/overview).
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
||||
|
@ -159,7 +159,7 @@ psl=}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity).
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](../../auth/social-auth/overview#social-login-entity).
|
||||
|
||||
### 3. Creating a GitHub OAuth App
|
||||
|
||||
@ -231,7 +231,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx
|
||||
### 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).
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../../project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
@ -295,7 +295,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</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).
|
||||
We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components [here](../../auth/ui).
|
||||
|
||||
### Conclusion
|
||||
|
||||
@ -304,7 +304,7 @@ 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).
|
||||
To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](../../auth/overview).
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
|
@ -96,7 +96,7 @@ app myApp {
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](/docs/auth/social-auth/overview#social-login-entity).
|
||||
`externalAuthEntity` and `userEntity` are explained in [the social auth overview](../../auth/social-auth/overview#social-login-entity).
|
||||
|
||||
### 2. Adding the Entities
|
||||
|
||||
@ -271,7 +271,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx
|
||||
### 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).
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../../project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's now create a `auth.{jsx,tsx}` file in the `client/pages`.
|
||||
@ -337,7 +337,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</Tabs>
|
||||
|
||||
:::info Auth UI
|
||||
Our pages use an automatically-generated Auth UI component. Read more about Auth UI components [here](/docs/auth/ui).
|
||||
Our pages use an automatically-generated Auth UI component. Read more about Auth UI components [here](../../auth/ui).
|
||||
:::
|
||||
|
||||
### Conclusion
|
||||
@ -347,7 +347,7 @@ 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).
|
||||
To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on [using auth](../../auth/overview).
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
|
@ -258,7 +258,7 @@ export const getUserFields: GetUserFieldsFn = async (_context, _args) => {
|
||||
|
||||
#### 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.
|
||||
You can query the user's `isSignupComplete` flag on the client with the [`useAuth()`](../../auth/overview) hook.
|
||||
Depending on the flag's value, you can redirect users to the appropriate signup step.
|
||||
|
||||
For example:
|
||||
@ -317,7 +317,7 @@ Each provider has their own rules for defining the `getUserFieldsFn` and `config
|
||||
## 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.
|
||||
[Auth UI](../../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.
|
||||
|
@ -218,7 +218,7 @@ export function SignupPage() {
|
||||
|
||||
It will automatically show the correct authentication providers based on your `main.wasp` file.
|
||||
|
||||
Read more about customizing the signup process like adding additional fields or extra UI in the [Using Auth](/docs/auth/overview#customizing-the-signup-process) section.
|
||||
Read more about customizing the signup process like adding additional fields or extra UI in the [Using Auth](../auth/overview#customizing-the-signup-process) section.
|
||||
|
||||
### Forgot Password Form
|
||||
|
||||
|
@ -157,7 +157,7 @@ We'll define the React components for these pages in the `client/pages/auth.{jsx
|
||||
### 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).
|
||||
We are using [Tailwind CSS](https://tailwindcss.com/) to style the pages. Read more about how to add it [here](../project/css-frameworks).
|
||||
:::
|
||||
|
||||
Let's create a `auth.{jsx,tsx}` file in the `client/pages` folder and add the following to it:
|
||||
@ -255,19 +255,19 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</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).
|
||||
We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components [here](../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).
|
||||
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](../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 in the [using auth docs](/docs/auth/overview#default-validations).
|
||||
Read more about the default username and password validation rules in the [using auth docs](../auth/overview#default-validations).
|
||||
|
||||
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.
|
||||
@ -594,7 +594,7 @@ We suggest using the built-in field validators for your authentication flow. You
|
||||
|
||||
## 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).
|
||||
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](../auth/overview).
|
||||
|
||||
## API Reference
|
||||
|
||||
@ -705,4 +705,4 @@ app myApp {
|
||||
`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.
|
||||
You can read about the rest of the `auth` options in the [using auth](../auth/overview) section of the docs.
|
||||
|
@ -4,7 +4,7 @@ sidebar_label: Contributing
|
||||
slug: /contributing
|
||||
---
|
||||
|
||||
import DiscordLink from '../blog/components/DiscordLink';
|
||||
import DiscordLink from '@site/blog/components/DiscordLink';
|
||||
|
||||
Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the [CONTRIBUTING.md](https://github.com/wasp-lang/wasp/blob/main/CONTRIBUTING.md) file in our Github repo. All the requirements and instructions are there, so please check [CONTRIBUTING.md](https://github.com/wasp-lang/wasp/blob/main/CONTRIBUTING.md) for more details.
|
||||
|
||||
@ -16,4 +16,4 @@ Some side notes to make your journey easier:
|
||||
|
||||
3. If there's something you'd like to bring to our attention, go to [docs GitHub repo](https://github.com/wasp-lang/wasp) and make an issue/PR!
|
||||
|
||||
Happy hacking!
|
||||
Happy hacking!
|
||||
|
@ -4,7 +4,7 @@ title: Databases
|
||||
|
||||
import { Required } from '@site/src/components/Required'
|
||||
|
||||
[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.
|
||||
[Entities](../data-model/entities.md), [Operations](../data-model/operations/overview) and [Automatic CRUD](../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
|
||||
|
||||
@ -77,7 +77,7 @@ Also, make sure that:
|
||||
|
||||
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).
|
||||
The easiest way to set the necessary `DATABASE_URL` environment variable is by adding it to the [.env.server](../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):
|
||||
|
||||
|
@ -4,13 +4,13 @@ title: Automatic CRUD
|
||||
|
||||
import { Required } from '@site/src/components/Required';
|
||||
import { ShowForTs } from '@site/src/components/TsJsHelpers';
|
||||
import ImgWithCaption from '../../blog/components/ImgWithCaption'
|
||||
import ImgWithCaption from '@site/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.
|
||||
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](../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.
|
||||
@ -62,7 +62,7 @@ Keep reading for an example of Automatic CRUD in action, or skip ahead for the [
|
||||
|
||||
## 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.
|
||||
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](../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"/>
|
||||
|
||||
@ -328,7 +328,7 @@ export const MainPage = () => {
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And here are the login and signup pages, where we are using Wasp's [Auth UI](/docs/auth/ui) components:
|
||||
And here are the login and signup pages, where we are using Wasp's [Auth UI](../auth/ui) components:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
@ -692,7 +692,7 @@ export const getAllOverride: GetAllQuery<Input, Output> = async (
|
||||
|
||||
</ShowForTs>
|
||||
|
||||
For a usage example, check the [example guide](/docs/data-model/crud#adding-crud-to-the-task-entity-).
|
||||
For a usage example, check the [example guide](../data-model/crud#adding-crud-to-the-task-entity-).
|
||||
|
||||
#### Using the CRUD operations in client code
|
||||
|
||||
@ -742,7 +742,7 @@ 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)
|
||||
All CRUD operations are implemented with [Queries and Actions](../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)
|
||||
|
||||
---
|
||||
|
||||
|
@ -62,11 +62,11 @@ 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)).
|
||||
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](../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.
|
||||
Most of the time, you will be working with Entities within the context of [Operations (Queries & Actions)](../data-model/operations/overview). We'll see how that's done on the next page.
|
||||
|
||||
#### Using Entities directly
|
||||
|
||||
|
@ -8,7 +8,7 @@ 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 are quite similar to [Queries](../../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.
|
||||
|
||||
@ -373,7 +373,7 @@ export const createTask: CreateTask = async (args, context) => {
|
||||
|
||||
### Using Entities in Actions
|
||||
|
||||
In most cases, resources used in Actions will be [Entities](/docs/data-model/entities.md).
|
||||
In most cases, resources used in Actions will be [Entities](../../data-model/entities.md).
|
||||
To use an Entity in your Action, add it to the `action` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
@ -626,7 +626,7 @@ Since both arguments are positional, you can name the parameters however you wan
|
||||
|
||||
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.
|
||||
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](../../auth/overview#using-the-contextuser-object) to see how to use the `user` object.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
@ -704,7 +704,7 @@ In this case, the Action expects to receive an object with a `bar` field of type
|
||||
|
||||
### 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.
|
||||
Make sure you understand how [Queries](../../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).
|
||||
|
@ -6,7 +6,7 @@ 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,
|
||||
There are two kinds of Operations: [Queries](../../data-model/operations/queries.md) and [Actions](../../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.
|
||||
|
@ -15,7 +15,7 @@ Fetching all comments on a blog post, a list of users that liked a video, inform
|
||||
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.
|
||||
We instead recommend skipping ahead and only reading [the differences between Queries and Actions](../../data-model/operations/actions#differences-between-queries-and-actions), and consulting the [API Reference](#api-reference) as needed.
|
||||
:::
|
||||
|
||||
## Working with Queries
|
||||
@ -395,7 +395,7 @@ To prevent information leakage, the server won't forward these fields for any ot
|
||||
|
||||
### Using Entities in Queries
|
||||
|
||||
In most cases, resources used in Queries will be [Entities](/docs/data-model/entities.md).
|
||||
In most cases, resources used in Queries will be [Entities](../../data-model/entities.md).
|
||||
To use an Entity in your Query, add it to the `query` declaration in Wasp:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
@ -552,7 +552,7 @@ Since both arguments are positional, you can name the parameters however you wan
|
||||
|
||||
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.
|
||||
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](../../auth/overview#using-the-contextuser-object) to see how to use the `user` object.
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
@ -651,6 +651,6 @@ Wasp's `useQuery` hook accepts three arguments:
|
||||
[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).
|
||||
so in the [client setup function](../../project/client-config.md#overriding-default-behaviour-for-queries).
|
||||
|
||||
For an example of usage, check [this section](#the-usequery-hook).
|
||||
|
@ -1,31 +0,0 @@
|
||||
---
|
||||
title: Examples
|
||||
---
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features.
|
||||
|
||||
The full list of examples can be found [here](https://github.com/wasp-lang/wasp/tree/release/examples/). Here is a few of them:
|
||||
|
||||
## Todo App
|
||||
- **Features**: Auth ([username/password](language/features#authentication--authorization)), [Queries & Actions](language/features#queries-and-actions-aka-operations), [Entities](language/features#entity), [Routes](language/features#route)
|
||||
- JS source code: [GitHub](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp)
|
||||
- TS source code: [GitHub](https://github.com/wasp-lang/wasp/tree/release/examples/todo-typescript)
|
||||
- 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](/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'>
|
||||
<img src={useBaseUrl('img/wespello-new.png')} width='75%'/>
|
||||
</p>
|
||||
|
||||
## Waspleau (Realtime Statistics Dashboard)
|
||||
- **Features**: Cron [Jobs](language/features#jobs), [Server Setup](language/features#server-configuration)
|
||||
- Source code: [GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/waspleau)
|
||||
- Hosted at [https://waspleau-app-client.fly.dev/](https://waspleau-app-client.fly.dev/)
|
||||
<p align='center'>
|
||||
<img src={useBaseUrl('img/waspleau.png')} width='75%'/>
|
||||
</p>
|
@ -5,7 +5,7 @@ This guide provides an overview of the Wasp CLI commands, arguments, and options
|
||||
|
||||
## Overview
|
||||
|
||||
Once [installed](/docs/quick-start), you can use the wasp command from your command line.
|
||||
Once [installed](../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:
|
||||
|
||||
@ -101,13 +101,13 @@ Newsletter: https://wasp-lang.dev/#signup
|
||||
Deleted .wasp/ directory.
|
||||
```
|
||||
|
||||
- `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 build` generates the complete web app code, which is ready for [deployment](../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). 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).
|
||||
Read more about automatic deployment [here](../advanced/deployment/cli).
|
||||
|
||||
- `wasp telemetry` displays the status of [telemetry](https://wasp-lang.dev/docs/telemetry).
|
||||
|
||||
|
@ -4,7 +4,7 @@ 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.
|
||||
This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the [Quick Start](./quick-start.md) guide.
|
||||
:::
|
||||
|
||||
Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.
|
||||
|
@ -3,10 +3,10 @@ title: Introduction
|
||||
slug: /
|
||||
---
|
||||
|
||||
import ImgWithCaption from '../../blog/components/ImgWithCaption'
|
||||
import ImgWithCaption from '@site/blog/components/ImgWithCaption'
|
||||
|
||||
:::note
|
||||
If you are looking for the installation instructions, check out the [Quick Start](/docs/quick-start) section.
|
||||
If you are looking for the installation instructions, check out the [Quick Start](./quick-start.md) section.
|
||||
:::
|
||||
|
||||
We will give a brief overview of what Wasp is, how it works on a high level and when to use it.
|
||||
@ -42,13 +42,13 @@ Define your app in the Wasp config and get:
|
||||
- async processing jobs,
|
||||
- React Query powered data fetching,
|
||||
- security best practices,
|
||||
- and more.
|
||||
- and more.
|
||||
|
||||
You don't need to write any code for these features, Wasp will take care of it for you 🤯 And what's even better, Wasp also maintains the code for you, so you don't have to worry about keeping up with the latest security best practices. As Wasp updates, so does your app.
|
||||
|
||||
## So what does the code look like?
|
||||
|
||||
Let's say you want to build a web app that allows users to **create and share their favorite recipes**.
|
||||
Let's say you want to build a web app that allows users to **create and share their favorite recipes**.
|
||||
|
||||
Let's start with the main.wasp file: it is the central file of your app, where you describe the app from the high level.
|
||||
|
||||
@ -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/tutorial/create).
|
||||
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](../tutorial/01-create.md).
|
||||
|
||||
:::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.
|
@ -45,8 +45,8 @@ Check [More Details](#more-details) section below if anything went wrong with th
|
||||
|
||||
### What next?
|
||||
|
||||
- [ ] 👉 **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.
|
||||
- [ ] 👉 **Check out the [Todo App tutorial](../tutorial/01-create.md), which will take you through all the core features of Wasp!** 👈
|
||||
- [ ] [Setup your editor](./editor-setup.md) 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](https://github.com/matijaSos) does his best to unleash his creativity to make them engaging and fun to read :D!
|
||||
|
||||
@ -110,14 +110,22 @@ Open your terminal and run:
|
||||
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
:::note Running Wasp on Mac with Mx chip (arm64)
|
||||
**Experiencing the 'Bad CPU type in executable' issue on a device with arm64 (Apple Silicon)?**
|
||||
Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install [Rosetta on your Mac](https://support.apple.com/en-us/HT211861) if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal
|
||||
```bash
|
||||
softwareupdate --install-rosetta
|
||||
```
|
||||
Once Rosetta is installed, you should be able to run Wasp without any issues.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value='win'>
|
||||
|
||||
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.
|
||||
|
||||
In the meantime, the best way to start using Wasp on Windows is by using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Once you set up Ubuntu on WSL, just follow Linux instructions for installing Wasp. You can refer to this [article](https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wsl) if you prefer a step by step guide to using Wasp in WSL environment. 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 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).
|
||||
:::
|
@ -211,7 +211,7 @@ export default async function mySetupFunction(): Promise<void> {
|
||||
### 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).
|
||||
You can change the options for a **single** Query using the `options` object, as described [here](../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.
|
||||
|
@ -85,7 +85,7 @@ Make sure to use the `.cjs` extension for these config files, if you name them w
|
||||
|
||||
### 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:
|
||||
To add Tailwind plugins, add it to [dependencies](../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 {
|
||||
|
@ -113,28 +113,28 @@ 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.
|
||||
Authentication configuration. Read more in the [authentication section](../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.
|
||||
Configuration for the client side of your app. Read more in the [client configuration section](../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.
|
||||
Configuration for the server side of your app. Read more in the [server configuration section](../project/server-config) of the docs.
|
||||
|
||||
- `db: dict`
|
||||
|
||||
Database configuration. Read more in the [database configuration section](/docs/data-model/backends) of the docs.
|
||||
Database configuration. Read more in the [database configuration section](../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.
|
||||
List of npm dependencies for your app. Read more in the [dependencies section](../project/dependencies) of the docs.
|
||||
|
||||
- `emailSender: dict`
|
||||
|
||||
Email sender configuration. Read more in the [email sending section](/docs/advanced/email) of the docs.
|
||||
Email sender configuration. Read more in the [email sending section](../advanced/email) of the docs.
|
||||
|
||||
- `webSocket: dict`
|
||||
|
||||
WebSocket configuration. Read more in the [WebSocket section](/docs/advanced/web-sockets) of the docs.
|
||||
WebSocket configuration. Read more in the [WebSocket section](../advanced/web-sockets) of the docs.
|
||||
|
@ -131,4 +131,4 @@ The way you provide env vars to your Wasp project in production depends on where
|
||||
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.
|
||||
You can read a lot more details in the [deployment section](../advanced/deployment/manually) of the docs. We go into detail on how to define env vars for each deployment option.
|
||||
|
@ -90,7 +90,7 @@ function addCustomRoute(app: Application) {
|
||||
|
||||
### 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.
|
||||
In case you want to store some values for later use, or to be accessed by the [Operations](../data-model/operations/overview) you do that in the `setupFn` function.
|
||||
|
||||
Dummy example of such function and its usage:
|
||||
|
||||
@ -247,4 +247,4 @@ app MyApp {
|
||||
|
||||
- #### `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).
|
||||
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](../advanced/middleware-config#1-customize-global-middleware).
|
||||
|
@ -69,7 +69,7 @@ Wasp provides several functions to help you write React tests:
|
||||
const { mockQuery, mockApi } = mockServer();
|
||||
```
|
||||
|
||||
- `mockQuery`: Takes a Wasp [query](/docs/data-model/operations/queries) to mock and the JSON data it should return.
|
||||
- `mockQuery`: Takes a Wasp [query](../data-model/operations/queries) to mock and the JSON data it should return.
|
||||
|
||||
```js
|
||||
import getTasks from "@wasp/queries/getTasks";
|
||||
@ -81,7 +81,7 @@ Wasp provides several functions to help you write React tests:
|
||||
- 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.
|
||||
- `mockApi`: Similar to `mockQuery`, but for [APIs](../advanced/apis). Instead of a Wasp query, it takes a route containing an HTTP method and a path.
|
||||
|
||||
```js
|
||||
import { HttpMethod } from "@wasp/types";
|
||||
|
@ -5,7 +5,7 @@ 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!
|
||||
You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the [QuickStart](../quick-start) guide!
|
||||
:::
|
||||
|
||||
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.
|
||||
|
@ -35,7 +35,7 @@ By _your code_, we mean the _"the code you write"_, as opposed to the code gener
|
||||
|
||||
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.
|
||||
The file `vite.config.ts` is used to configure [Vite](https://vitejs.dev/guide/), Wasp's build tool of choice.
|
||||
We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in [custom Vite config docs](/docs/project/custom-vite-config.md).
|
||||
We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in [custom Vite config docs](../project/custom-vite-config.md).
|
||||
|
||||
:::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.
|
||||
|
@ -142,7 +142,7 @@ Now you can visit `/hello/johnny` and see "Here's johnny!"
|
||||
<ShowForTs>
|
||||
|
||||
:::tip Type-safe links
|
||||
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](/docs/advanced/links) for more details.
|
||||
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](../advanced/links) for more details.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
|
@ -21,7 +21,7 @@ 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.
|
||||
Read more in the [Entities](../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:
|
||||
|
@ -5,7 +5,7 @@ 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_.
|
||||
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](../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.
|
||||
|
||||
@ -225,14 +225,14 @@ Most of this code is regular React, the only exception being the <ShowForJs>two<
|
||||
<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.
|
||||
- `import { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](../data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name.
|
||||
|
||||
</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 { useQuery } from '@wasp/queries'` - Imports Wasp's [useQuery](../data-model/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name.
|
||||
- `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.
|
||||
|
@ -39,7 +39,7 @@ 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:
|
||||
Next, we want to tell Wasp that we want to use full-stack [authentication](../auth/overview) in our app:
|
||||
|
||||
```wasp {7-16} title="main.wasp"
|
||||
app TodoApp {
|
||||
@ -65,13 +65,13 @@ app TodoApp {
|
||||
|
||||
By doing this, Wasp will create:
|
||||
|
||||
- [Auth UI](/docs/auth/ui) with login and signup forms.
|
||||
- [Auth UI](../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!
|
||||
Wasp also supports authentication using [Google](../auth/social-auth/google), [GitHub](../auth/social-auth/github), and [email](../auth/email), with more on the way!
|
||||
:::
|
||||
|
||||
## Adding Login and Signup Pages
|
||||
@ -216,7 +216,7 @@ export default SignupPage
|
||||
<ShowForTs>
|
||||
|
||||
:::tip Type-safe links
|
||||
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](/docs/advanced/links) for more details.
|
||||
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out the [type-safe links docs](../advanced/links) for more details.
|
||||
:::
|
||||
</ShowForTs>
|
||||
|
||||
@ -515,8 +515,8 @@ You should be ready to learn about more complicated features and go more in-dept
|
||||
|
||||
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)
|
||||
- Get a jump start on your next project with [Starter Templates](../project/starter-templates)
|
||||
- Make a real-time app with [Web Sockets](../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 🙏
|
||||
|
@ -1,521 +0,0 @@
|
||||
---
|
||||
title: TypeScript Support
|
||||
---
|
||||
|
||||
import OldDocsNote from '@site/docs/OldDocsNote'
|
||||
|
||||
# Using Wasp with TypeScript
|
||||
|
||||
<OldDocsNote />
|
||||
|
||||
TypeScript is a programming language that brings static type analysis to JavaScript. It is a superset of JavaScript (i.e., all valid JavaScript programs are valid TypeScript programs) and compiles to JavaScript before running. TypeScript's type system detects common errors at build time (reducing the chance of runtime errors in production) and enables type-based auto-completion in IDEs.
|
||||
|
||||
This document assumes you are familiar with TypeScript and primarily focuses on how to use it with Wasp. To learn more about TypeScript itself, we recommend reading [the official docs](https://www.typescriptlang.org/docs/).
|
||||
|
||||
The document also assumes a basic understanding of core Wasp features (e.g., Queries, Actions, Entities). You can read more about these features in [our feature docs](/docs/language/features).
|
||||
|
||||
Besides allowing you to write your code in TypeScript, Wasp also supports:
|
||||
|
||||
- Importing and using Wasp Entity types (on both the server and the client).
|
||||
- Automatic full-stack type support for Queries and Actions - frontend types are automatically inferred from backend definitions.
|
||||
- Type-safe generic hooks (`useQuery` and `useAction`) with the accompanying type inference.
|
||||
- Type-safe optimistic update definitions.
|
||||
|
||||
We'll dig into the details of each feature in the following sections. But first, let's see how you can introduce TypeScript to an existing Wasp project.
|
||||
|
||||
:::info
|
||||
To get the best IDE experience, make sure to leave `wasp start` running in the background. Wasp will track the working directory and ensure the generated code/types are up to date with your changes.
|
||||
|
||||
Your editor may sometimes report type and import errors even while `wasp start` is running. This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting _"TypeScript: Restart TS Server."_
|
||||
:::
|
||||
|
||||
## Migrating your project to TypeScript
|
||||
|
||||
Wasp supports TypeScript out of the box!
|
||||
|
||||
Our scaffolding already includes TypeScript, so migrating your project to TypeScript is as simple as changing file extensions and using the language. This approach allows you to gradually migrate your project on a file-by-file basis.
|
||||
|
||||
### Example
|
||||
|
||||
Let's first assume your Wasp file contains the following definitions:
|
||||
|
||||
```wasp title=main.wasp
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
psl=}
|
||||
|
||||
query getTaskInfo {
|
||||
fn: import { getTaskInfo } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
Let's now assume that your `queries.js` file looks something like this:
|
||||
|
||||
```javascript title="src/server/queries.js"
|
||||
import HttpError from "@wasp/core/HttpError.js"
|
||||
|
||||
function getInfoMessage(task) {
|
||||
const isDoneText = task.isDone ? "is done" : "is not done"
|
||||
return `Task '${task.description}' is ${isDoneText}.`
|
||||
}
|
||||
|
||||
export const getTaskInfo = async ({ id }, context) => {
|
||||
const Task = context.entities.Task
|
||||
const task = await Task.findUnique({ where: { id } })
|
||||
if (!task) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
return getInfoMessage(task)
|
||||
}
|
||||
```
|
||||
To migrate this file to TypeScript, all you have to do is:
|
||||
|
||||
1. Change the filename from `queries.js` to `queries.ts`.
|
||||
2. Write some types.
|
||||
|
||||
Let's start by only providing a basic `getInfoMessage` function. We'll see how to properly type the rest of the file in the following sections.
|
||||
|
||||
```typescript title=src/server/queries.ts
|
||||
import HttpError from "@wasp/core/HttpError.js"
|
||||
|
||||
// highlight-next-line
|
||||
function getInfoMessage(task: {
|
||||
isDone: boolean
|
||||
description: string
|
||||
}): string {
|
||||
const isDoneText = task.isDone ? "is done" : "is not done"
|
||||
return `Task '${task.description}' is ${isDoneText}.`
|
||||
}
|
||||
|
||||
export const getTaskInfo = async ({ id }, context) => {
|
||||
const Task = context.entities.Task
|
||||
const task = await Task.findUnique({ where: { id } })
|
||||
if (!task) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
return getInfoMessage(task)
|
||||
}
|
||||
```
|
||||
|
||||
You don't need to change anything inside the `.wasp` file.
|
||||
:::caution
|
||||
|
||||
<!-- This block is mostly duplicated in 03-listing-tasks.md -->
|
||||
|
||||
Even when you use TypeScript, and your file is called `queries.ts`, you still need to import it using the `.js` extension:
|
||||
|
||||
```wasp
|
||||
query getTaskInfo {
|
||||
fn: import { getTaskInfo } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
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). This quirk does not apply to client files (the transpiler takes care of it).
|
||||
|
||||
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).
|
||||
:::
|
||||
|
||||
## Entity Types
|
||||
|
||||
Instead of manually specifying the types for `isDone` and `description`, we can get them from the `Task` entity type. Wasp will generate types for all entities and let you import them from `"@wasp/entities"`:
|
||||
|
||||
```typescript title=src/server/queries.ts
|
||||
import HttpError from "@wasp/core/HttpError.js"
|
||||
// highlight-next-line
|
||||
import { Task } from "@wasp/entities"
|
||||
|
||||
// highlight-next-line
|
||||
function getInfoMessage(task: Pick<Task, "isDone" | "description">): string {
|
||||
const isDoneText = task.isDone ? "is done" : "is not done"
|
||||
return `Task '${task.description}' is ${isDoneText}.`
|
||||
}
|
||||
|
||||
export const getTaskInfo = async ({ id }, context) => {
|
||||
const Task = context.entities.Task
|
||||
const task = await Task.findUnique({ where: { id } })
|
||||
if (!task) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
return getInfoMessage(task)
|
||||
}
|
||||
```
|
||||
|
||||
By doing this, we've connected the argument type of the `getInfoMessage` function with the `Task` entity. This coupling removes duplication and ensures the function keeps the correct signature even if we change the entity. Of course, the function might throw type errors depending on how we change the entity, but that's precisely what we want!
|
||||
|
||||
Don't worry about typing the query function for now. We'll see how to handle this in the next section.
|
||||
|
||||
Entity types are also available on the client under the same import:
|
||||
|
||||
```tsx title=src/client/Main.jsx
|
||||
import { Task } from "@wasp/entities"
|
||||
|
||||
export function ExamplePage() {}
|
||||
const task: Task = {
|
||||
id: 123,
|
||||
description: "Some random task",
|
||||
isDone: false,
|
||||
}
|
||||
return <div>{task.description}</div>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The mentioned type safety mechanisms also apply here: changing the task entity in our `.wasp` file changes the imported type, which might throw a type error and warn us that our task definition is outdated.
|
||||
|
||||
## Backend type support for Queries and Actions
|
||||
|
||||
Wasp automatically generates the appropriate types for all Operations (i.e., Actions and Queries) you define inside your `.wasp` file. Assuming your `.wasp` file contains the following definition:
|
||||
|
||||
```wasp title=main.wasp
|
||||
// ...
|
||||
|
||||
query GetTaskInfo {
|
||||
fn: import { getTaskInfo } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
Wasp will generate a type called `GetTaskInfo`, which you can use to type the Query's implementation. By assigning the `GetTaskInfo` type to your function, you get the type information for its context. In this case, TypeScript will know the `context.entities` object must include the `Task` entity. If the Query had auth enabled, it would also know that `context` includes user information.
|
||||
|
||||
`GetTaskInfo` can is a generic type that takes two (optional) type arguments:
|
||||
|
||||
1. `Input` - The argument (i.e., payload) received by the query function.
|
||||
2. `Output` - The query function's return type.
|
||||
|
||||
Suppose you don't care about typing the Query's inputs and outputs. In that case, you can omit both type arguments, and TypeScript will infer the most general types (i.e., `never` for the input, `unknown` for the output.).
|
||||
|
||||
```typescript title=src/server/queries.ts
|
||||
import HttpError from "@wasp/core/HttpError.js"
|
||||
import { Task } from "@wasp/entities"
|
||||
// highlight-next-line
|
||||
import { GetTaskInfo } from "@wasp/queries/types"
|
||||
|
||||
function getInfoMessage(task: Pick<Task, "isDone" | "description">): string {
|
||||
const isDoneText = task.isDone ? "is done" : "is not done"
|
||||
return `Task '${task.description}' is ${isDoneText}.`
|
||||
}
|
||||
|
||||
// Use the type parameters to specify the Query's argument and return types.
|
||||
// highlight-next-line
|
||||
export const getTaskInfo: GetTaskInfo<Pick<Task, "id">, string> = async ({ id }, context) => {
|
||||
// Thanks to the definition in your .wasp file, the compiler knows the type of
|
||||
// `context` (and that it contains the `Task` entity).
|
||||
const Task = context.entities.Task
|
||||
|
||||
// Thanks to the first type argument in `GetTaskInfo`, the compiler knows `args`
|
||||
// is of type `Pick<Task, "id">`.
|
||||
const task = await Task.findUnique({ where: { id } })
|
||||
if (!task) {
|
||||
throw new HttpError(404)
|
||||
}
|
||||
|
||||
// Thanks to the second type argument in `GetTaskInfo`, the compiler knows the
|
||||
// function must return a value of type `string`.
|
||||
return getInfoMessage(task)
|
||||
}
|
||||
```
|
||||
Everything described above applies to Actions as well.
|
||||
:::tip
|
||||
|
||||
If don't want to define a new type for the Query's return value, the new `satisfies` keyword will allow TypeScript to infer it automatically:
|
||||
```typescript
|
||||
const getFoo = (async (_args, context) => {
|
||||
const foos = await context.entities.Foo.findMany()
|
||||
return {
|
||||
foos,
|
||||
message: "Here are some foos!",
|
||||
queriedAt: new Date(),
|
||||
}
|
||||
}) satisfies GetFoo
|
||||
```
|
||||
From the snippet above, TypeScript knows:
|
||||
1. The correct type for `context`.
|
||||
2. The Query's return type is `{ foos: Foo[], message: string, queriedAt: Date }`.
|
||||
|
||||
If you don't need the context, you can skip specifying the Query's type (and arguments):
|
||||
```typescript
|
||||
const getFoo = () => {{ name: 'Foo', date: new Date() }}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Frontend type support for Queries and Actions
|
||||
|
||||
Wasp supports automatic full-stack type safety à la tRPC. You only need to define the Operation's type on the backend, and the frontend will automatically know how to call it.
|
||||
|
||||
### Frontend type support for Queries
|
||||
The examples assume you've defined the Query `getTaskInfo` from the previous sections:
|
||||
|
||||
```typescript title="src/server/queries.ts"
|
||||
export const getTaskInfo: GetTaskInfo<Pick<Task, "id">, string> =
|
||||
async ({ id }, context) => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Wasp will use the type of `getTaskInfo` to infer the Query's types on the frontend:
|
||||
|
||||
```tsx title="src/client/TaskInfo.tsx"
|
||||
import { useQuery } from "@wasp/queries"
|
||||
// Wasp knows the type of `getTaskInfo` thanks to your backend definition.
|
||||
// highlight-next-line
|
||||
import getTaskInfo from "@wasp/queries/getTaskInfo"
|
||||
|
||||
export const TaskInfo = () => {
|
||||
const {
|
||||
// TypeScript knows `taskInfo` is a `string | undefined` thanks to the
|
||||
// backend definition.
|
||||
data: taskInfo,
|
||||
// TypeScript also knows `isError` is a `boolean`.
|
||||
isError,
|
||||
// TypeScript knows `error` is of type `Error`.
|
||||
error,
|
||||
// TypeScript knows `id` must be a `Task["id"]` (i.e., a number) thanks to
|
||||
// your backend definition.
|
||||
// highlight-next-line
|
||||
} = useQuery(getTaskInfo, { id: 1 })
|
||||
|
||||
if (isError) {
|
||||
return <div> Error during fetching tasks: {error.message || "unknown"}</div>
|
||||
}
|
||||
|
||||
// TypeScript forces you to perform this check.
|
||||
return taskInfo === undefined ? (
|
||||
<div>Waiting for info...</div>
|
||||
) : (
|
||||
<div>{taskInfo}</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend type support for Actions
|
||||
|
||||
Assuming the following action definition in your `.wasp` file
|
||||
|
||||
```wasp title=main.wasp
|
||||
action addTask {
|
||||
fn: import { addTask } from "@server/actions.js"
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
And its corresponding implementation in `src/server/actions.ts`:
|
||||
|
||||
```typescript title=src/server/actions.ts
|
||||
import { AddTask } from "@wasp/actions/types"
|
||||
|
||||
type TaskPayload = Pick<Task, "description" | "isDone">
|
||||
|
||||
const addTask: AddTask<TaskPayload, Task> = async (args, context) => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Here's how to use it on the frontend:
|
||||
```tsx title=src/client/AddTask.tsx
|
||||
import { useAction } from "@wasp/actions"
|
||||
// TypeScript knows `addTask` is a function that expects a value of type
|
||||
// `TaskPayload` and returns a value of type `Promise<Task>`.
|
||||
import addTask from "@wasp/queries/addTask"
|
||||
|
||||
const AddTask = ({ description }: Pick<Task, "description">) => {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => addTask({ description, isDone: false })}>
|
||||
Add unfinished task
|
||||
</button>
|
||||
<button onClick={() => addTask({ description, isDone: true })}>
|
||||
Add finished task
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
#### Type support for the `useAction` hook
|
||||
Type inference also works if you decide to use the action via the `useAction` hook:
|
||||
```typescript
|
||||
// addTaskFn is of type (args: TaskPayload) => Task
|
||||
const addTaskFn = useAction(addTask)
|
||||
```
|
||||
|
||||
The `useAction` hook also includes support for optimistic updates. Read [the feature docs](/docs/language/features#the-useaction-hook) to understand more about optimistic updates and how to define them in Wasp.
|
||||
|
||||
Here's an example that shows how you can use static type checking in their definitions (the example assumes an appropriate action defined in the `.wasp` file and implemented on the server):
|
||||
|
||||
```tsx title=Task.tsx
|
||||
import { useQuery } from "@wasp/queries"
|
||||
import { OptimisticUpdateDefinition, useAction } from "@wasp/actions"
|
||||
import updateTaskIsDone from "@wasp/actions/updateTaskIsDone"
|
||||
|
||||
type TaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
const Task = ({ taskId }: Pick<Task, "id">) => {
|
||||
const updateTaskIsDoneOptimistically = useAction(
|
||||
updateTaskIsDone,
|
||||
{
|
||||
optimisticUpdates: [
|
||||
{
|
||||
getQuerySpecifier: () => [getTask, { id: taskId }],
|
||||
// This query's cache should should never be empty
|
||||
updateQuery: ({ isDone }, oldTask) => ({ ...oldTask!, isDone }),
|
||||
// highlight-next-line
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task>,
|
||||
{
|
||||
getQuerySpecifier: () => [getTasks],
|
||||
updateQuery: (updatedTask, oldTasks) =>
|
||||
oldTasks &&
|
||||
oldTasks.map((task) =>
|
||||
task.id === updatedTask.id ? { ...task, ...updatedTask } : task
|
||||
),
|
||||
// highlight-next-line
|
||||
} as OptimisticUpdateDefinition<TaskPayload, Task[]>,
|
||||
],
|
||||
}
|
||||
)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Database seeding
|
||||
|
||||
When implementing a seed function in TypeScript, you can import a `DbSeedFn` type via
|
||||
|
||||
```ts
|
||||
import type { DbSeedFn } from "@wasp/dbSeed/types.js"
|
||||
```
|
||||
|
||||
and use it to type your seed function like this:
|
||||
|
||||
```ts
|
||||
export const devSeedSimple: DbSeedFn = async (prismaClient) => { ... }
|
||||
```
|
||||
|
||||
## CRUD operations on entities
|
||||
|
||||
For a specific [Entity](/docs/language/features#entity), you can tell Wasp to automatically instantiate server-side logic ([Queries](/docs/language/features#query) and [Actions](/docs/language/features#action)) for creating, reading, updating and deleting such entities.
|
||||
|
||||
Read more about CRUD operations in Wasp [here](/docs/language/features#crud-operations).
|
||||
|
||||
### Using types for CRUD operations overrides
|
||||
|
||||
If you writing the override implementation in Typescript, you'll have access to generated types. The overrides are functions that take the following arguments:
|
||||
- `args` - The arguments of the operation i.e. the data that's sent from the client.
|
||||
- `context` - Context containing the `user` making the request and the `entities` object containing the entity that's being operated on.
|
||||
|
||||
You can types for each of the functions you want to override from `@wasp/crud/{crud name}`. The types that are available 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) => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket full-stack type support
|
||||
|
||||
|
||||
Defining event names with the matching payload types on the server makes those types exposed automatically on the client. This helps you avoid mistakes when emitting events or handling them.
|
||||
|
||||
### 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 type the `webSocketFn` function like this:
|
||||
|
||||
```ts title=src/server/webSocket.ts
|
||||
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
|
||||
|
||||
// Using the generic WebSocketDefinition type to define the WebSocket function.
|
||||
type WebSocketFn = WebSocketDefinition<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>
|
||||
|
||||
interface ServerToClientEvents {
|
||||
// The type for the payload of the "chatMessage" event.
|
||||
chatMessage: (msg: { id: string, username: string, text: string }) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
// The type for the payload of the "chatMessage" event.
|
||||
chatMessage: (msg: string) => void;
|
||||
}
|
||||
|
||||
interface InterServerEvents {}
|
||||
|
||||
interface SocketData extends WaspSocketData {}
|
||||
|
||||
// Use the WebSocketFn to type the webSocketFn function.
|
||||
export const webSocketFn: WebSocketFn = (io, context) => {
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('chatMessage', async (msg) => {
|
||||
io.emit('chatMessage', { ... })
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Using the WebSocket on the client
|
||||
|
||||
After you have defined the WebSocket function on the server, you can use it on the client. The `useSocket` hook will give you the `socket` instance and the `isConnected` boolean. The `socket` instance is typed with the types you defined on the server.
|
||||
|
||||
The `useSocketListener` hook will give you a type-safe event handler. The event name and its payload type are defined on the server.
|
||||
|
||||
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,
|
||||
ClientToServerPayload,
|
||||
} 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<
|
||||
// We are using a helper type to get the payload type for the "chatMessage" event.
|
||||
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'>) {
|
||||
// ...
|
||||
}
|
||||
|
||||
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('')
|
||||
}
|
||||
|
||||
return (
|
||||
...
|
||||
)
|
||||
}
|
||||
```
|
@ -56,12 +56,16 @@ module.exports = {
|
||||
},
|
||||
items: [
|
||||
{
|
||||
to: 'docs/',
|
||||
activeBasePath: 'docs',
|
||||
label: 'Docs',
|
||||
type: 'docsVersion',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
className: 'navbar-item-docs navbar-item-outside',
|
||||
},
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'left',
|
||||
className: 'navbar-item-docs-version-dropdown',
|
||||
},
|
||||
{
|
||||
to: 'blog',
|
||||
label: 'Blog',
|
||||
@ -101,11 +105,7 @@ module.exports = {
|
||||
{
|
||||
label: 'Todo app tutorial',
|
||||
to: 'docs/tutorial/create',
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
to: 'docs/language/features',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -137,10 +137,10 @@ module.exports = {
|
||||
appId: 'RG0JSZOWH4',
|
||||
apiKey: 'feaa2a25dc596d40418c82cd040e2cbe',
|
||||
indexName: 'wasp-lang',
|
||||
// TODO: contextualSearch is useful when you are doing versioning,
|
||||
// it searches only in v1 docs if you are searching from v1 docs.
|
||||
// We should enable it if we start doing versioning.
|
||||
// contextualSearch: true
|
||||
// ContextualSearch is useful when you are doing versioning,
|
||||
// it searches only in v1 docs if you are searching from v1 docs.
|
||||
// Therefore we have it enabled, since we have multiple doc versions.
|
||||
contextualSearch: true
|
||||
},
|
||||
image: 'img/wasp_twitter_cover.png',
|
||||
metadata: [{ name: 'twitter:card', content: 'summary_large_image' }],
|
||||
@ -156,13 +156,44 @@ module.exports = {
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
sidebarCollapsible: true,
|
||||
// Please change this to your repo.
|
||||
editUrl: 'https://github.com/wasp-lang/wasp/edit/release/web',
|
||||
remarkPlugins: [autoImportTabs, fileExtSwitcher],
|
||||
|
||||
// ------ Configuration for multiple docs versions ------ //
|
||||
|
||||
// "current" docs (under /docs) are in-progress docs, so we show them only in development.
|
||||
includeCurrentVersion: process.env.NODE_ENV === 'development',
|
||||
|
||||
// Uncomment line below to build and show only current docs, for faster build times
|
||||
// during development, if/when needed.
|
||||
// onlyIncludeVersions: process.env.NODE_ENV === 'development' ? ["current"] : undefined,
|
||||
|
||||
// "versions" option here enables us to customize each version of docs individually,
|
||||
// and there are also other options if we ever need to customize versioned docs further.
|
||||
versions: {
|
||||
...(
|
||||
(process.env.NODE_ENV === 'development') ? {
|
||||
"current": {
|
||||
path: "next", // Token used in the URL to address this version of docs: {baseUrl}/docs/{path}.
|
||||
label: "Next", // Label shown in the documentation to address this version of docs.
|
||||
noIndex: true, // these are un-released docs, we don't want search engines indexing them.
|
||||
}
|
||||
} : {}
|
||||
),
|
||||
// Configuration example:
|
||||
// "0.11.1": {
|
||||
// path: "0.11.1", // default, but can be anything.
|
||||
// label: "0.11.1", // default, but can be anything.
|
||||
// banner: "unmaintained"
|
||||
// // and more!
|
||||
// },
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ //
|
||||
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
blogSidebarCount: 'ALL',
|
||||
blogSidebarTitle: 'All our posts',
|
||||
postsPerPage: 'ALL',
|
||||
|
@ -6,8 +6,8 @@ module.exports = {
|
||||
collapsed: false,
|
||||
collapsible: false,
|
||||
items: [
|
||||
'introduction/what-is-wasp',
|
||||
'introduction/getting-started',
|
||||
'introduction/introduction',
|
||||
'introduction/quick-start',
|
||||
'introduction/editor-setup',
|
||||
],
|
||||
},
|
||||
|
@ -132,3 +132,29 @@
|
||||
font-weight: bold;
|
||||
font-size: var(--ifm-h6-font-size);
|
||||
}
|
||||
|
||||
/******** DOCS NAVBAR *********/
|
||||
|
||||
/* This hides Docs version dropdown when not on docs (i.e. when we are on blog). */
|
||||
.navbar__item:has(.navbar-item-docs-version-dropdown:not(.active)) {
|
||||
display: none
|
||||
}
|
||||
|
||||
/******************************/
|
||||
|
||||
/******* OTHER ********/
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
|
||||
.video-container iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
BIN
web/static/img/writing-rfcs/existing-solutions.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
web/static/img/writing-rfcs/rfc-flowchart.png
Normal file
After Width: | Height: | Size: 234 KiB |
BIN
web/static/img/writing-rfcs/rfc-meme-when.png
Normal file
After Width: | Height: | Size: 706 KiB |
BIN
web/static/img/writing-rfcs/rfc-metadata.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
web/static/img/writing-rfcs/rfc-overview.png
Normal file
After Width: | Height: | Size: 967 KiB |
BIN
web/static/img/writing-rfcs/rfc-problem.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
web/static/img/writing-rfcs/rfc-prophet.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
web/static/img/writing-rfcs/rfc-reviewer-status-example.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
web/static/img/wsl-guide/wsl-guide-banner.jpeg
Normal file
After Width: | Height: | Size: 461 KiB |
@ -0,0 +1,7 @@
|
||||
:::info Sending emails while developing
|
||||
|
||||
When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.
|
||||
|
||||
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/versioned_docs/version-0.11.8/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](../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](../advanced/middleware-config)
|
||||
|
||||
## Using Entities in APIs
|
||||
|
||||
In many cases, resources used in APIs will be [Entities](../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](../advanced/middleware-config) of the docs.
|
@ -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);
|
||||
}
|
@ -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](../../auth/social-auth/google#4-adding-environment-variables) or [GitHub](../../auth/social-auth/github#4-adding-environment-variables)), make sure to set the necessary environment variables.
|
||||
:::
|
@ -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.
|
249
web/versioned_docs/version-0.11.8/advanced/deployment/cli.md
Normal file
@ -0,0 +1,249 @@
|
||||
---
|
||||
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`). Read more about Fly.io regions [here](#flyio-regions).
|
||||
|
||||
:::caution Unique Name
|
||||
Your app name must be unique across all of Fly or deployment will fail.
|
||||
:::
|
||||
|
||||
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`
|
||||
|
||||
You'll notice that Wasp creates two new files in your project root directory:
|
||||
- `fly-server.toml`
|
||||
- `fly-client.toml`
|
||||
|
||||
You should include these files in your version control so that you can deploy your app with a single command in the future.
|
||||
|
||||
### 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.
|
||||
|
||||
You **can edit the `fly-server.toml` and `fly-client.toml` files** to further configure your Fly deployments. Wasp will use the TOML files when you run `deploy`.
|
||||
|
||||
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`.
|
@ -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](../../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](../../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](../../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
|
||||
:::
|
@ -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](../../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/versioned_docs/version-0.11.8/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
|
426
web/versioned_docs/version-0.11.8/advanced/jobs.md
Normal file
@ -0,0 +1,426 @@
|
||||
---
|
||||
title: Recurring Jobs
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required'
|
||||
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers'
|
||||
|
||||
In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.
|
||||
|
||||
What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.
|
||||
|
||||
Wasp supports background jobs that can help you with this:
|
||||
- Jobs persist between server restarts,
|
||||
- Jobs can be retried if they fail,
|
||||
- Jobs can be delayed until a future time,
|
||||
- Jobs can have a recurring schedule.
|
||||
|
||||
## Using Jobs
|
||||
|
||||
### Job Definition and Usage
|
||||
|
||||
Let's write an example Job that will print a message to the console and return a list of tasks from the database.
|
||||
|
||||
1. Start by creating a Job declaration in your `.wasp` file:
|
||||
|
||||
<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"
|
||||
},
|
||||
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"
|
||||
},
|
||||
entities: [Task],
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
2. After declaring the Job, implement its worker function:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="bar.js"
|
||||
export const foo = async ({ name }, context) => {
|
||||
console.log(`Hello ${name}!`)
|
||||
const tasks = await context.entities.Task.findMany({})
|
||||
return { tasks }
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="bar.ts"
|
||||
import type { MySpecialJob } from '@wasp/jobs/mySpecialJob'
|
||||
import type { Task } from '@wasp/entities'
|
||||
|
||||
type Input = { name: string; }
|
||||
type Output = { tasks: Task[]; }
|
||||
|
||||
export const foo: MySpecialJob<Input, Output> = async ({ name }, context) => {
|
||||
console.log(`Hello ${name}!`)
|
||||
const tasks = await context.entities.Task.findMany({})
|
||||
return { tasks }
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info The worker function
|
||||
The worker function must be an `async` function. The function's return value represents the Job's result.
|
||||
|
||||
The worker function accepts two arguments:
|
||||
- `args`: The data passed into the job when it's submitted.
|
||||
- `context: { entities }`: The context object containing entities you put in the Job declaration.
|
||||
:::
|
||||
|
||||
<ShowForTs>
|
||||
|
||||
`MySpecialJob` is a generic type Wasp generates to help you correctly type the Job's worker function, ensuring type information about the function's arguments and return value. Read more about type-safe jobs in the [Javascript API section](#javascript-api).
|
||||
</ShowForTs>
|
||||
|
||||
3. After successfully defining the job, you can submit work to be done in your [Operations](../data-model/operations/overview) or [setupFn](../project/server-config#setup-function) (or any other NodeJS code):
|
||||
|
||||
<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: "Johnny" })
|
||||
|
||||
// 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({ name: "Johnny" })
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="someAction.ts"
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
|
||||
const submittedJob = await mySpecialJob.submit({ job: "Tony" })
|
||||
|
||||
// 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({ name: "Tony" })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
And that'is it. Your job will be executed by `PgBoss` as if you called `foo({ name: "Johnny" })`.
|
||||
|
||||
In our example, `foo` takes an argument, but passing arguments to jobs is not a requirement. It 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 _don't_ need to invoke anything in <ShowForJs>JavaScript</ShowForJs><ShowForTs>Typescript</ShowForTs>. You can imagine `foo({ job: "args" })` getting automatically scheduled and invoked for you every hour.
|
||||
|
||||
<!-- TODO: write this piece after we complete https://github.com/wasp-lang/wasp/issues/1412 -->
|
||||
<!-- ### Getting the Job's Result
|
||||
|
||||
When you submit a job, you get a `SubmittedJob` object back. It has a `jobId` field, which you can use to get the job's result. -->
|
||||
|
||||
## API Reference
|
||||
|
||||
### Declaring Jobs
|
||||
|
||||
<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_.
|
||||
|
||||
`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` function that performs the work. Since Wasp executes Jobs on the server, you must import it from `@server`.
|
||||
- It receives the following arguments:
|
||||
- `args: Input`: The data passed to the job when it's submitted.
|
||||
- `context: { entities: Entities }`: The context object containing any declared entities.
|
||||
|
||||
Here's an example of a `perform.fn` function:
|
||||
|
||||
<Tabs groupId="js-ts">
|
||||
<TabItem value="js" label="JavaScript">
|
||||
|
||||
```js title="bar.js"
|
||||
export const foo = async ({ name }, context) => {
|
||||
console.log(`Hello ${name}!`)
|
||||
const tasks = await context.entities.Task.findMany({})
|
||||
return { tasks }
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="TypeScript">
|
||||
|
||||
```ts title="bar.ts"
|
||||
import { MySpecialJob } from '@wasp/jobs/mySpecialJob'
|
||||
|
||||
type Input = { name: string; }
|
||||
type Output = { tasks: Task[]; }
|
||||
|
||||
export const foo: MySpecialJob<Input, Output> = async (args, context) => {
|
||||
console.log(`Hello ${name}!`)
|
||||
const tasks = await context.entities.Task.findMany({})
|
||||
return { tasks }
|
||||
}
|
||||
```
|
||||
|
||||
Read more about type-safe jobs in the [Javascript API section](#javascript-api).
|
||||
</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](../data-model/operations/queries#using-entities-in-queries)).
|
||||
|
||||
### JavaScript API
|
||||
|
||||
- Importing a Job:
|
||||
|
||||
<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, type MySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
```
|
||||
|
||||
:::info Type-safe jobs
|
||||
Wasp generates a generic type for each Job declaration, which you can use to type your `perform.fn` function. The type is named after the job declaration, and is available in the `@wasp/jobs/{jobName}` module. In the example above, the type is `MySpecialJob`.
|
||||
|
||||
The type takes two type arguments:
|
||||
- `Input`: The type of the `args` argument of the `perform.fn` function.
|
||||
- `Output`: The type of the return value of the `perform.fn` function.
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
- `submit(jobArgs, executorOptions)`
|
||||
- `jobArgs: Input`
|
||||
- `executorOptions: object`
|
||||
|
||||
Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, 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 has the following fields:
|
||||
- `jobId`: The ID for the job in that executor.
|
||||
- `jobName`: The name of the job you used in your `.wasp` file.
|
||||
- `executorName`: The 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`
|
||||
- `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)
|
134
web/versioned_docs/version-0.11.8/advanced/links.md
Normal file
@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Type-Safe Links
|
||||
---
|
||||
|
||||
import { Required } from '@site/src/components/Required'
|
||||
|
||||
If you are using Typescript, you can use Wasp's custom `Link` component to create type-safe links to other pages on your site.
|
||||
|
||||
## Using the `Link` Component
|
||||
|
||||
After you defined a route:
|
||||
|
||||
```wasp title="main.wasp"
|
||||
route TaskRoute { path: "/task/:id", to: TaskPage }
|
||||
page TaskPage { ... }
|
||||
```
|
||||
|
||||
You can get the benefits of type-safe links by using the `Link` component from `@wasp/router`:
|
||||
|
||||
```jsx title="TaskList.tsx"
|
||||
import { Link } from '@wasp/router'
|
||||
|
||||
export const TaskList = () => {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tasks.map((task) => (
|
||||
<Link
|
||||
key={task.id}
|
||||
to="/task/:id"
|
||||
{/* 👆 You must provide a valid path here */}
|
||||
params={{ id: task.id }}>
|
||||
{/* 👆 All the params must be correctly passed in */}
|
||||
{task.description}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Using Search Query & Hash
|
||||
|
||||
You can also pass `search` and `hash` props to the `Link` component:
|
||||
|
||||
```tsx title="TaskList.tsx"
|
||||
<Link
|
||||
to="/task/:id"
|
||||
params={{ id: task.id }}
|
||||
search={{ sortBy: 'date' }}
|
||||
hash="comments"
|
||||
>
|
||||
{task.description}
|
||||
</Link>
|
||||
```
|
||||
|
||||
This will result in a link like this: `/task/1?sortBy=date#comments`. Check out the [API Reference](#link-component) for more details.
|
||||
|
||||
## The `routes` Object
|
||||
|
||||
You can also get all the pages in your app with the `routes` object:
|
||||
|
||||
```jsx title="TaskList.tsx"
|
||||
import { routes } from '@wasp/router'
|
||||
|
||||
const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })
|
||||
```
|
||||
|
||||
This will result in a link like this: `/task/1`.
|
||||
|
||||
You can also pass `search` and `hash` props to the `build` function. Check out the [API Reference](#routes-object) for more details.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### `Link` Component
|
||||
|
||||
The `Link` component accepts the following props:
|
||||
- `to` <Required />
|
||||
|
||||
- A valid Wasp Route path from your `main.wasp` file.
|
||||
|
||||
- `params: { [name: string]: string | number }` <Required /> (if the path contains params)
|
||||
|
||||
- An object with keys and values for each param in the path.
|
||||
- For example, if the path is `/task/:id`, then the `params` prop must be `{ id: 1 }`. Wasp supports required and optional params.
|
||||
|
||||
- `search: string[][] | Record<string, string> | string | URLSearchParams`
|
||||
|
||||
- Any valid input for `URLSearchParams` constructor.
|
||||
- For example, the object `{ sortBy: 'date' }` becomes `?sortBy=date`.
|
||||
|
||||
- `hash: string`
|
||||
- all other props that the `react-router-dom`'s [Link](https://v5.reactrouter.com/web/api/Link) component accepts
|
||||
|
||||
|
||||
### `routes` Object
|
||||
|
||||
The `routes` object contains a function for each route in your app.
|
||||
|
||||
```ts title="router.tsx"
|
||||
export const routes = {
|
||||
// RootRoute has a path like "/"
|
||||
RootRoute: {
|
||||
build: (options?: {
|
||||
search?: string[][] | Record<string, string> | string | URLSearchParams
|
||||
hash?: string
|
||||
}) => // ...
|
||||
},
|
||||
|
||||
// DetailRoute has a path like "/task/:id/:something?"
|
||||
DetailRoute: {
|
||||
build: (
|
||||
options: {
|
||||
params: { id: ParamValue; something?: ParamValue; },
|
||||
search?: string[][] | Record<string, string> | string | URLSearchParams
|
||||
hash?: string
|
||||
}
|
||||
) => // ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `params` object is required if the route contains params. The `search` and `hash` parameters are optional.
|
||||
|
||||
You can use the `routes` object like this:
|
||||
|
||||
```tsx
|
||||
import { routes } from '@wasp/router'
|
||||
|
||||
const linkToRoot = routes.RootRoute.build()
|
||||
const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
|
||||
```
|
280
web/versioned_docs/version-0.11.8/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](../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/versioned_docs/version-0.11.8/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 additionally 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/versioned_docs/version-0.11.8/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/versioned_docs/version-0.11.8/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/versioned_docs/version-0.11.8/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](../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](../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](../advanced/email#getting-the-api-key).
|
||||
|
||||
Read more about setting up email senders in the [sending emails 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](../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](../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](../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).
|
1306
web/versioned_docs/version-0.11.8/auth/overview.md
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);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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](../../auth/overview.md#api-reference).
|
||||
|
||||
<!-- This snippet is used in google.md and github.md -->
|
@ -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 -->
|
@ -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 -->
|
@ -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 -->
|
@ -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 -->
|