From 62d9eac49be254e39639ba9b3803bf5a367bb037 Mon Sep 17 00:00:00 2001 From: Alexander Sandor Date: Wed, 18 Dec 2024 09:36:42 +0100 Subject: [PATCH] feat: Create documentation version 2.3.0 --- .../version-2.3.0/01-get-started.md | 302 +++++++ .../version-2.3.0/02-get-started-with-mini.md | 114 +++ .../version-2.3.0/03-capabilities.md | 53 ++ versioned_docs/version-2.3.0/04-support.md | 23 + .../05-tutorials/01-first-app.mdx | 828 ++++++++++++++++++ .../02-real-time-communication.md | 403 +++++++++ .../05-tutorials/03-code-example.md | 8 + .../05-tutorials/04-authentication.md | 8 + .../05-tutorials/_category_.json | 4 + .../06-concepts/01-working-with-endpoints.md | 92 ++ .../version-2.3.0/06-concepts/02-models.md | 346 ++++++++ .../06-concepts/03-serialization.md | 179 ++++ .../06-concepts/04-exceptions.md | 74 ++ .../version-2.3.0/06-concepts/05-sessions.md | 26 + .../06-concepts/06-database/01-connection.md | 107 +++ .../06-concepts/06-database/02-models.md | 55 ++ .../06-database/03-relations/01-one-to-one.md | 193 ++++ .../03-relations/02-one-to-many.md | 116 +++ .../03-relations/03-many-to-many.md | 58 ++ .../03-relations/04-self-relations.md | 69 ++ .../03-relations/05-referential-actions.md | 55 ++ .../06-database/03-relations/06-modules.md | 61 ++ .../06-database/03-relations/_category_.json | 5 + .../06-concepts/06-database/04-indexing.md | 64 ++ .../06-concepts/06-database/05-crud.md | 191 ++++ .../06-concepts/06-database/06-filter.md | 293 +++++++ .../06-database/07-relation-queries.md | 225 +++++ .../06-concepts/06-database/08-sort.md | 75 ++ .../06-database/08-transactions.md | 116 +++ .../06-concepts/06-database/09-pagination.md | 110 +++ .../06-concepts/06-database/10-raw-access.md | 81 ++ .../06-concepts/06-database/11-migrations.md | 212 +++++ .../06-concepts/06-database/_category_.json | 5 + .../06-concepts/07-configuration.md | 148 ++++ .../version-2.3.0/06-concepts/08-caching.md | 58 ++ .../version-2.3.0/06-concepts/09-logging.md | 63 ++ .../version-2.3.0/06-concepts/10-modules.md | 98 +++ .../06-concepts/11-authentication/01-setup.md | 255 ++++++ .../11-authentication/02-basics.md | 141 +++ .../03-working-with-users.md | 31 + .../04-providers/01-email.md | 193 ++++ .../04-providers/02-google.md | 244 ++++++ .../04-providers/03-apple.md | 57 ++ .../04-providers/05-firebase.md | 98 +++ .../04-providers/06-custom-providers.md | 258 ++++++ .../04-providers/_category_.json | 4 + .../11-authentication/05-custom-overrides.md | 254 ++++++ .../11-authentication/_category_.json | 5 + .../06-concepts/12-file-uploads.md | 154 ++++ .../06-concepts/13-health-checks.md | 39 + .../06-concepts/14-scheduling.md | 83 ++ .../version-2.3.0/06-concepts/15-streams.md | 214 +++++ .../06-concepts/16-backward-compatibility.md | 9 + .../version-2.3.0/06-concepts/17-webserver.md | 74 ++ .../06-concepts/18-testing/01-get-started.md | 243 +++++ .../06-concepts/18-testing/02-the-basics.md | 259 ++++++ .../18-testing/03-advanced-examples.md | 149 ++++ .../18-testing/04-best-practises.md | 125 +++ .../06-concepts/18-testing/_category_.json | 4 + .../06-concepts/19-experimental.md | 89 ++ .../version-2.3.0/06-concepts/_category_.json | 4 + .../07-deployments/01-deployment-strategy.md | 52 ++ .../02-deploying-to-gce-terraform.md | 296 +++++++ .../03-deploying-to-gcr-console.md | 102 +++ .../07-deployments/04-deploying-to-aws.md | 289 ++++++ .../07-deployments/05-general.md | 52 ++ .../07-deployments/_category_.json | 3 + .../08-upgrading/01-upgrade-from-mini.md | 11 + .../02-upgrade-to-one-point-two.md | 267 ++++++ .../08-upgrading/03-upgrade-to-two.md | 405 +++++++++ .../04-upgrade-to-two-point-two.md | 184 ++++ .../08-upgrading/_category_.json | 4 + .../version-2.3.0/09-tools/01-insights.md | 14 + .../version-2.3.0/09-tools/02-lsp.md | 13 + .../version-2.3.0/09-tools/_category_.json | 5 + versioned_docs/version-2.3.0/10-contribute.md | 145 +++ versioned_docs/version-2.3.0/index.md | 43 + .../version-2.3.0-sidebars.json | 8 + versions.json | 1 + 79 files changed, 9798 insertions(+) create mode 100644 versioned_docs/version-2.3.0/01-get-started.md create mode 100644 versioned_docs/version-2.3.0/02-get-started-with-mini.md create mode 100644 versioned_docs/version-2.3.0/03-capabilities.md create mode 100644 versioned_docs/version-2.3.0/04-support.md create mode 100644 versioned_docs/version-2.3.0/05-tutorials/01-first-app.mdx create mode 100644 versioned_docs/version-2.3.0/05-tutorials/02-real-time-communication.md create mode 100644 versioned_docs/version-2.3.0/05-tutorials/03-code-example.md create mode 100644 versioned_docs/version-2.3.0/05-tutorials/04-authentication.md create mode 100644 versioned_docs/version-2.3.0/05-tutorials/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/01-working-with-endpoints.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/02-models.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/03-serialization.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/04-exceptions.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/05-sessions.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/01-connection.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/02-models.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/01-one-to-one.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/02-one-to-many.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/03-many-to-many.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/04-self-relations.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/05-referential-actions.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/06-modules.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/04-indexing.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/05-crud.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/06-filter.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/07-relation-queries.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/08-sort.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/08-transactions.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/09-pagination.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/10-raw-access.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/11-migrations.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/06-database/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/07-configuration.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/08-caching.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/09-logging.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/10-modules.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/01-setup.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/02-basics.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/03-working-with-users.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/01-email.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/02-google.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/03-apple.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/05-firebase.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/06-custom-providers.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/05-custom-overrides.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/11-authentication/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/12-file-uploads.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/13-health-checks.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/14-scheduling.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/15-streams.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/16-backward-compatibility.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/17-webserver.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/18-testing/01-get-started.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/18-testing/02-the-basics.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/18-testing/03-advanced-examples.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/18-testing/04-best-practises.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/18-testing/_category_.json create mode 100644 versioned_docs/version-2.3.0/06-concepts/19-experimental.md create mode 100644 versioned_docs/version-2.3.0/06-concepts/_category_.json create mode 100644 versioned_docs/version-2.3.0/07-deployments/01-deployment-strategy.md create mode 100644 versioned_docs/version-2.3.0/07-deployments/02-deploying-to-gce-terraform.md create mode 100644 versioned_docs/version-2.3.0/07-deployments/03-deploying-to-gcr-console.md create mode 100644 versioned_docs/version-2.3.0/07-deployments/04-deploying-to-aws.md create mode 100644 versioned_docs/version-2.3.0/07-deployments/05-general.md create mode 100644 versioned_docs/version-2.3.0/07-deployments/_category_.json create mode 100644 versioned_docs/version-2.3.0/08-upgrading/01-upgrade-from-mini.md create mode 100644 versioned_docs/version-2.3.0/08-upgrading/02-upgrade-to-one-point-two.md create mode 100644 versioned_docs/version-2.3.0/08-upgrading/03-upgrade-to-two.md create mode 100644 versioned_docs/version-2.3.0/08-upgrading/04-upgrade-to-two-point-two.md create mode 100644 versioned_docs/version-2.3.0/08-upgrading/_category_.json create mode 100644 versioned_docs/version-2.3.0/09-tools/01-insights.md create mode 100644 versioned_docs/version-2.3.0/09-tools/02-lsp.md create mode 100644 versioned_docs/version-2.3.0/09-tools/_category_.json create mode 100644 versioned_docs/version-2.3.0/10-contribute.md create mode 100644 versioned_docs/version-2.3.0/index.md create mode 100644 versioned_sidebars/version-2.3.0-sidebars.json diff --git a/versioned_docs/version-2.3.0/01-get-started.md b/versioned_docs/version-2.3.0/01-get-started.md new file mode 100644 index 00000000..95a2d73b --- /dev/null +++ b/versioned_docs/version-2.3.0/01-get-started.md @@ -0,0 +1,302 @@ +# Get started + +This page will help you understand how a Serverpod project is structured, how to make calls to endpoints, and how to communicate with the database. + +
+ +## Serverpod or Serverpod Mini? + +Serverpod Mini is a lightweight version of Serverpod that is perfect for small projects or when you want to try out Serverpod without setting up a Postgres database. If you start with Mini, you can upgrade to the full version of Serverpod anytime. + +__[Get started with Mini](get-started-with-mini)__ + +
+__Serverpod vs Serverpod Mini comparison__ +

+ +| Feature | Serverpod | Serverpod Mini | +|-----------------------|:---------:|:--------------:| +| Remote method calls | ✅ | ✅ | +| Generated data models | ✅ | ✅ | +| Streaming data | ✅ | ✅ | +| Custom auth | ✅ | ✅ | +| Pre-built auth | ✅ | | +| Postgres database ORM | ✅ | | +| Task scheduling | ✅ | | +| Basic logging | ✅ | ✅ | +| Serverpod Insights | ✅ | | +| Caching | ✅ | ✅ | +| File uploads | ✅ | | +| Health checks | ✅ | | +| Relic web server | ✅ | | +| Easy deployment | ✅ | ✅ | + +

+
+ +## Creating a new Serverpod project +The full version of Serverpod needs access to a Postgres database. The easiest way to set that up is to use our pre-configured Docker container. Install __[Flutter](https://flutter.dev/docs/get-started/install)__, __[Serverpod](/)__ and __[Docker Desktop](https://docs.docker.com/get-docker/)__ before you begin. + +Create a new project by running `serverpod create`. + +```bash +$ serverpod create mypod +``` + +:::info + +Serverpod executes the `flutter create` command inside the flutter package during project creation. On Windows, `flutter` commands require that developer mode is enabled in the system settings. + +::: + +This command will create a new directory called `mypod`, with three dart packages inside; `mypod_server`, `mypod_client`, and `mypod_flutter`. + +- `mypod_server`: This package contains your server-side code. Modify it to add new endpoints or other features your server needs. +- `mypod_client`: This is the code needed to communicate with the server. Typically, all code in this package is generated automatically, and you should not edit the files in this package. +- `mypod_flutter`: This is the Flutter app, pre-configured to connect to your local server. + +### Starting the server + +Make sure that __[Docker Desktop](https://www.docker.com/products/docker-desktop/)__ is running, then start your Docker containers with `docker compose up --build --detach`. It will start Postgres and Redis. Then, run `dart bin/main.dart --apply-migrations` to start your server. + +```bash +$ cd mypod/mypod_server +$ docker compose up --build --detach +$ dart bin/main.dart --apply-migrations +``` + +If everything is working, you should see something like this on your terminal: + +```text +SERVERPOD version: 2.x.x, mode: development, time: 2022-09-12 17:22:02.825468Z +Insights listening on port 8081 +Server default listening on port 8080 +Webserver listening on port 8082 +``` + +:::info + +If you need to stop the Docker containers at some point, just run `docker compose stop` or use the Docker Desktop application. You can also use Docker Desktop to start, stop, and manage your containers. + +::: + +:::important + +In your development environment it can be helpful to always start Serverpod with the `--apply-migrations` flag, as this will ensure that the database is always up-to-date with your latest migration. However, in production you should typically start the server without the flag, unless you want to actually apply a new migration. + +::: + +### Running the demo app + +Start the default demo app by changing the directory into the Flutter package that was created and running `flutter run`. + +```bash +$ cd mypod/mypod_flutter +$ flutter run -d chrome +``` + +The flag `-d chrome` runs the app in Chrome, for other run options please see the Flutter documentation. + +:::info + +**iOS Simulator**: Because an iOS simulator has its own localhost, it won't find the server running on your machine. Therefore, you will need to pass the IP address of your machine when creating the client in `mypod/mypod_flutter/lib/main.dart`. Depending on your local network, it might look something like this: + +```dart +var client = Client('http://192.168.1.117:8080/') + ..connectivityMonitor = FlutterConnectivityMonitor(); +``` + +::: + +:::info +**MacOS**: +If you run the app on MacOS, you will need to add permissions for outgoing connections in your Xcode project. To do this, open the `Runner.xcworkspace` in Xcode. Then check the _Outgoing Connections (Client)_ under _Runner_ > _Signing & Capabilities_ > _App Sandbox_. Make sure to add the capability for all run configurations. + +::: + +## Server overview + +At first glance, the complexity of the server may seem daunting, but there are only a few directories and files you need to pay attention to. The rest of the files will be there when you need them in the future, e.g., when you want to deploy your server or if you want to set up continuous integration. + +These are the most important directories: + +- `config`: These are the configuration files for your Serverpod. These include a `password.yaml` file with your passwords and configurations for running your server in development, staging, and production. By default, everything is correctly configured to run your server locally. +- `lib/src/endpoints`: This is the default location for your server's endpoints. When you add methods to an endpoint, Serverpod will generate the corresponding methods in your client. +- `lib/src/models`: Default location for your model definition files. The files define the classes you can pass through your API and how they relate to your database. Serverpod generates serializable objects from the model definitions. + +Both the `endpoints` and `models` directories contain sample files that give a quick idea of how they work. So this a great place to start learning. + +### Generating code + +Whenever you change your code in either the `endpoints` or `models` directory, you will need to regenerate the classes managed by Serverpod. Do this by running `serverpod generate`. + +```bash +$ cd mypod/mypod_server +$ serverpod generate +``` + +### Working with endpoints + +Endpoints are the connection points to the server from the client. With Serverpod, you add methods to your endpoint, and your client code will be generated. For the code to be generated, you need to place your endpoint in the `lib/src/endpoints` directory of your server. Your endpoint should extend the `Endpoint` class. For methods to be generated, they need to return a typed `Future`, and its first parameter should be a `Session` object. The `Session` object holds information about the call being made and provides access to the database. + +```dart +import 'package:serverpod/serverpod.dart'; + +class ExampleEndpoint extends Endpoint { + Future hello(Session session, String name) async { + return 'Hello $name'; + } +} +``` + +The above code will create an endpoint called `example` (the Endpoint suffix will be removed) with the single `hello` method. To generate the client-side code run `serverpod generate` in the home directory of the server. + +On the client side, you can now invoke the method by calling: + +```dart +var result = await client.example.hello('World'); +``` + +:::tip + +To learn more about endpoints, see the [Working with endpoints](concepts/working-with-endpoints) section. + +::: + +### Serializing data + +Serverpod makes it easy to generate serializable classes that can be passed between server and client or used to communicate with the database. + +The structure for your serialized classes is defined in `.spy.yaml` files anywhere in the `lib` directory. Run `serverpod generate` in the home directory of the server to build the Dart code for the classes and make them accessible to both the server and client. + +Here is a simple example of a `.spy.yaml` file defining a serializable class: + +```yaml +class: Company +fields: + name: String + foundedDate: DateTime? +``` + +Supported types are `bool`, `int`, `double`, `String`, `DateTime`, `ByteData`, and other serializable classes. You can also use `List`s and `Map`s of the supported types, just make sure to specify the types. Null safety is supported. The keys of `Map` must be non-nullable `String`s. Once your classes are generated, you can use them as parameters or return types to endpoint methods. + +:::tip + +You can also create custom serialized classes with tools such as Freezed. Learn more in the [Serialization](concepts/serialization) section. + +::: + +## Working with the database + +A core feature of Serverpod is to query the database easily. Serverpod provides an ORM that supports type and null safety. + +### Connecting to the database + +When working with the database, it is common that you want to connect to it with a database viewer such as [Postico2](https://eggerapps.at/postico2/), [PgAdmin](https://www.pgadmin.org/download/), or [DBeaver](https://dbeaver.io/download/). To connect to the database, you need to specify the host and port along with the database name, user name, and password. In your project, you can find these inside the `config` directory. + +The connection details can be found in the file `config/development.yaml`. The variable `name` refers to the database name (which is your project name only). + +```yaml +database: + host: localhost + port: 8090 + name: projectname + user: postgres + +... +``` + +The password can be found in the file `config/passwords.yaml`. + +```yaml +development: + database: '' + +... +``` + +### Migrations + +With database migrations, Serverpod makes it easy to evolve your database schema. When you make changes to your project that should be reflected in your database, you need to create a migration. A migration is a set of SQL queries that are run to update the database. To create a migration, run `serverpod create-migration` in the home directory of the server. + +```bash +$ cd mypod/mypod_server +$ serverpod create-migration +``` + +Migrations are then applied to the database as part of the server startup by adding the `--apply-migrations` flag. + +```bash +$ cd mypod/mypod_server +$ dart bin/main.dart --apply-migrations +``` + +:::tip + +To learn more about database migrations, see the [Migrations](concepts/database/migrations) section. + +::: + +### Object database mapping + +Add a `table` key to your model file to add a mapping to the database. The value specified after the key sets the database table name. Here is the `Company` class from earlier with a database table mapping to a table called `company`: + +```yaml +class: Company +table: company +fields: + name: String + foundedDate: DateTime? +``` + +CRUD operations are available through the static `db` method on all classes with database bindings. + +:::tip + +To learn more about database CRUD operations, see the [CRUD](concepts/database/crud) section. + +::: + +### Writing to database + +Inserting a new row into the database is as simple as calling the static `db.insertRow` method. + +```dart +var myCompany = Company(name: 'Serverpod corp.', foundedDate: DateTime.now()); +myCompany = await Company.db.insertRow(session, myCompany); +``` + +The method returns the inserted object with its `id` field set from the database. + +### Reading from database + +Retrieving a single row from the database can done by calling the static `db.findById` method and providing the `id` of the row. + +```dart +var myCompany = await Company.db.findById(session, companyId); +``` + +You can also use an expression to do a more refined search through the `db.findFirstRow(...)`. method. The `where` parameter is a typed expression builder. The builder's parameter, `t`, contains a description of the table and gives access to the table's columns. + +```dart +var myCompany = await Company.db.findFirstRow( + session, + where: (t) => t.name.equals('My Company'), +); +``` + +The example above will return a single row from the database where the `name` column is equal to `My Company`. + +If no matching row is found, `null` is returned. + +:::tip + +Working with a database is an extensive subject. Learn more in the [Database](concepts/database/connection) section. + +::: + +## Where to go next + +You should now have a basic understanding of how Serverpod works. The different topics are described in more detail in the _Concepts_ section of the documentation. If you are unfamiliar with server-side development, a good starting place for learning is to do the [Build your first app](tutorials/first-app) tutorial. There are also many good video tutorials linked in the _Tutorials_ section. + +If you get stuck, never be afraid to ask questions in our [community on Github](https://github.com/serverpod/serverpod/discussions). The Serverpod team is very active there, and many questions are also answered by other developers in the community. diff --git a/versioned_docs/version-2.3.0/02-get-started-with-mini.md b/versioned_docs/version-2.3.0/02-get-started-with-mini.md new file mode 100644 index 00000000..b85c171f --- /dev/null +++ b/versioned_docs/version-2.3.0/02-get-started-with-mini.md @@ -0,0 +1,114 @@ +# Get started with Mini + +Serverpod Mini is a slimmer version of Serverpod that does not need to be connected to a Postgres database. Before you begin, make sure that you have __[Flutter](https://flutter.dev/docs/get-started/install)__ and __[Serverpod](/)__ installed. + +
+ +## Create a new project +Create a mini project by running: + +```bash +$ serverpod create myminipod --mini +``` + +Serverpod will create a new project for you. It contains three Dart packages, but you only need to pay attention to the `myminipod_server` and `myminipod_flutter` directories. The server directory contains your server files, and the flutter directory contains your app. The third package (`myminipod_client`) contains generated code that is used by the Flutter app to communicate with the server. + +Start your server by changing directory into your server directory, and run the `bin/main.dart` file: + +```bash +$ cd myminipod/myminipod_server +$ dart bin/main.dart +``` + +Your default project comes with a sample Flutter app, all hooked up to talk with your server. Run it with the `flutter` command: + +```bash +$ cd myminipod/myminipod_flutter +$ flutter run -d chrome +``` + +Easy as that. 🥳 + +:::tip + +If you are using VS Code, install our Serverpod extension. It will help you validate any Serverpod-related files in your project! + +::: + +## Creating models +In Serverpod, you define your models in easy-to-read YAML-files, which you place anywhere in your server’s `lib` directory with the `.spy.yaml` extension. Model files will be converted to Dart classes that can be serialized and sent to and from the server to your app. This is an example of a model file: + +```yaml +class: Company +fields: + name: String + foundedDate: DateTime? + employees: List +``` + +For types, you can use most basic Dart types, such as `String`, `double`, `int`, `bool`, `DateTime`, and `ByteData`. You can also include `List` and `Map`, just make sure to specify their types. Any other class specified among your models is also supported. + +Whenever you add or edit a model file, run `serverpod generate` in your server directory. Then, Serverpod will generate all the updated Dart classes: + +```bash +$ cd myminipod/myminipod_server +$ serverpod generate +``` + +## Adding methods to your server +With Serverpod, you add Dart methods to endpoints placed in your server’s `lib/src/endpoints` directory. By doing so, Serverpod will analyze your server code and automatically generate the corresponding methods in your Flutter app. Calling a method on the server is just like calling a local method in your app. + +For the server methods to work, there are a few things you need to keep in mind: + +- You must place the methods in a class that extends the Endpoint class. +- The methods must return a typed Future. The types you use in your methods are the same as those supported by your models. +- The first parameter of your method must be a Session object. The session contains extra information about the call being made to the server, such as the HTTP request object. + +This is an example of an endpoint that uses the Company class that we defined in the example model in the previous section. + +```dart +import 'package:serverpod/serverpod.dart'; + +class CompanyEndpoint extends Endpoint { + Future isLegit(Session session, Company company) async { + // Check if the company has the foundedDate set and that it + // has been around for more than one year. + + if (company.foundedDate == null) { + return false; + } + + var oneYearAgo = DateTime.now().subract(Duration(days: 365)); + return company.foundedDate!.isBefore(oneYearAgo); + } +} +``` + +After adding or modifying endpoints and endpoint methods, you must run `serverpod generate` to keep your Flutter app up-to-date. + +```bash +$ cd myminipod/myminipod_server +$ serverpod generate +``` + +## Calling the server methods from the app +When you run `serverpod generate` Serverpod will add your endpoints and server methods to the `client` object in your Flutter app. From the client, you can access all endpoints and methods. + +To call the endpoint method we just created from Flutter, just create a `Company` object, call the method, and await the result: + +```dart +var company = Company( + name: 'Serverpod', + foundedDate: DateTime(2021, 9, 27), + employees: [ + 'Alex', + 'Isak', + 'Viktor', + ], +); + +var result = await client.company.isLegit(company); +``` + +## Conclusion +You are now ready to start exploring the exciting world of Serverpod! And even if you start out with Serverpod mini, you can always [upgrade](upgrading/upgrade-from-mini) to the full version later. diff --git a/versioned_docs/version-2.3.0/03-capabilities.md b/versioned_docs/version-2.3.0/03-capabilities.md new file mode 100644 index 00000000..673a34b9 --- /dev/null +++ b/versioned_docs/version-2.3.0/03-capabilities.md @@ -0,0 +1,53 @@ +# Capabilities + +Serverpod is a complete, competent backend for Flutter. For the glossy sales pitch, head to our main page at [Serverpod.dev](https://serverpod.dev). + +Every design decision in Serverpod aims to minimize the amount of code you need to write and make it as readable as possible. Apart from being just a server, Serverpod incorporates many common tasks that are otherwise cumbersome to implement or require external services. + +## Code generation + +Serverpod automatically generates your model and client-side code by analyzing your server. Calling a remote endpoint is as easy as making a local method call. + +## World-class logging + +Stop struggling. You no longer need to search through endless server logs. Pinpoint exceptions and slow database queries in an easy-to-use user interface with a single click. + +## Built-in caching + +Cut down on your database costs. Don't save all your data permanently when you don't have to. Serverpod comes with a high-performance distributed cache built right in. Any serializable objects can be cached locally on your server or using Redis if you need to use the same cache across a cluster of servers. + +## Easy to use ORM + +Save time. Talking with your database can be a hassle. With Serverpod's ORM, your queries use native Dart types and null-safety. There is a straight path from your statically checked code to the database. + +## Database migrations + +Easily keep your database in sync as the requirements of your project evolve. Serverpod comes with a complete database migration system that helps you apply and version changes to the database. + +## File uploads + +Upload files straight to Google Cloud Storage, S3, or store them in your database. + +## Authentication + +Sign in through social logins or wing your own. Currently supported are Google, Apple, Firebase, and email. + +## Data streaming + +Pass serialized objects through authenticated sockets. Push messages from your server for real-time communication. Sending messages across a cluster of servers is supported. Perfect for building games or chatting applications, or anything you can imagine. + +## Task scheduling + +Serverpod's future calls replace complicated cron jobs. Call a method anytime in the future or after a specified delay. The calls persist even if the server is restarted. + +## Health checks + +Monitor the database and external services that you are using. Write custom health checks and get notified when something goes wrong. + +## Easy deployment + +Serverpod comes with Terraform scripts for Google Cloud Platform and AWS, making deploying your server very quick. We are still working on scripts for other platforms. Please get in touch with us if you want to [contribute](/contribute). + +## Built-in web server + +Serverpod comes with a built-in web server. This makes it very easy to share data for applications that need both an app and traditional web pages. You can also use the webserver to create webhooks or generate custom REST APIs to communicate with 3rd party services. _The web server is still experimental, and we are actively working on it_. diff --git a/versioned_docs/version-2.3.0/04-support.md b/versioned_docs/version-2.3.0/04-support.md new file mode 100644 index 00000000..ac465aec --- /dev/null +++ b/versioned_docs/version-2.3.0/04-support.md @@ -0,0 +1,23 @@ +# Support & community + +If you get stuck, you can get support through our Github community. The authors of Serverpod are checking in pretty much every day and helping out as much as we can. + +__[Public discussion board](https://github.com/serverpod/serverpod/discussions)__ + +## Reporting issues + +Serverpod is a work in progress, and there may be issues that we aren't aware of. If you run into something that doesn't seem to work the way you expect it to, please file an issue so that we can get it fixed as soon as possible. + +__[Serverpod issue tracker](https://github.com/serverpod/serverpod/issues)__ + +## Stay up-to-date + +We set up a mailing list to keep you up-to-date with any news and updates around Serverpod. We send an email about once a month or when something significant happens. + +__[Join the email list](https://serverpod.news)__ + +## Community updates + +We are continuously adding videos on Youtube, including announcements of major new features, tutorials, and tips. + +__[Follow our YouTube channel](https://www.youtube.com/@serverpod)__ \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/05-tutorials/01-first-app.mdx b/versioned_docs/version-2.3.0/05-tutorials/01-first-app.mdx new file mode 100644 index 00000000..178ddca4 --- /dev/null +++ b/versioned_docs/version-2.3.0/05-tutorials/01-first-app.mdx @@ -0,0 +1,828 @@ +import ReactPlayer from 'react-player' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +# Build your first app + +You will build a simple note-taking app in this tutorial. You will learn the fundamental building blocks of Serverpod that enable you to create powerful and scalable server-side applications with ease. + +
+ +We are assuming you have all the tools setup and ready to go. If not, please follow the [Installing Serverpod](/) guide to get up and running. + +We will cover the following topics: + +- Creating serializable objects +- Creating a database table +- Creating API endpoints for CRUD operations +- Using the serverpod code generator +- Using the generated client library +- Connecting a Flutter app to the server + + + +Demo of what we will build: ([Full code example](https://github.com/serverpod/notes)). + + + +## Create a new project + +Create a new project using the Serverpod CLI. Run the following command in your terminal: + +```bash +$ serverpod create notes +``` + +To start the server: + +```bash +$ cd notes/notes_server +$ docker compose up --build --detach +$ dart bin/main.dart --apply-migrations +``` + +:::info + +Make sure you have Docker running on your machine before executing these commands. + +::: + +## Serialize objects + +Serverpod comes with a convenient way to create serializable objects with the help of code generation. These objects can easily be sent back and forth between the server and the client. This is managed by defining our objects in a YAML file which the code generator then parses and generates the necessary code for. + +To define the structure of our Note object, we will create a YAML file called `note.yaml` inside the `lib/src/models` directory in your Serverpod project (`notes_server`). Add the following content in `note.yaml`: + +```yaml +### Holds a note with a text written by the user. +class: Note +fields: + ### The contents of the note. + text: String +``` + +Let's take a closer look at the content of the `note.yaml` file: +* **class:** Specifies the name of the class to be generated, which in this case is **Note**. +* **fields:** This keyword indicates the beginning of the field definitions for the **Note** class. +* **text:** Defines a text field of type String in the Note class, in this minimal example we only have one field but you can add as many fields as you need. + + +Use the code generator to generate the code for the `Note` class from the definition in `note.yaml`. Run the following command from the root of your server project (`notes_server`): + +```bash +$ serverpod generate +``` + +After the code generation process is complete, you can access the generated code for the `Note` class in `lib/src/generated/note.dart` inside your Serverpod project. + +![Serverpod Insights](/img/tutorial/note-app/02-note-class-v2.png) + +By implementing the `SerializableModel` class, the `Note` object becomes capable of automatic serialization and deserialization. This makes the `Note` object transmittable between the server and the client. + +In simple terms, we have created a class, `Note`, that can hold information and can be passed within the server. Additionally, we can send this object to the client side of our application. Serverpod takes care of handling the conversion between the object and its serialized representation, making it convenient to work with and transfer data seamlessly. + +## Create database tables + +Serverpod provides built-in support for database integrations. By defining a database table named `note` in the YAML file using the `table` keyword, we can create database bindings for our `Note` class. + +The updated content of the `note.yaml` file should look like this: + +```yaml +### Holds a note with a text written by the user. +class: Note +table: note +fields: + ### The contents of the note. + text: String +``` + +Run the code generator again to generate the necessary code used to access the database table: + +```bash +$ serverpod generate +``` + +Take a look at the updated `lib/src/generated/note.dart` file. You will notice that the code generator has added new methods for interacting with the database. + +### Create database migration +To create the new `note` table in the database we will use the Serverpod migration system. Run the following command to generate a new database migration: + +```bash +$ serverpod create-migration +``` + +This creates a new migration that contains a description of the database schema and SQL code to add the table. These files can be found in the `migrations` directory in your server project (`notes_server`). + +### Apply database migration + +To apply the database migration, start the server with the `--apply-migrations` command. We can also run the server in maintenance mode which will shut down the server as soon as its tasks are done. + +```bash +$ dart run bin/main.dart --role maintenance --apply-migrations +``` + +Once command has executed successfully, the database table for storing the note data will have been created. + +:::info + +Any time you update the table definitions you have to create a migration and apply it to the database to update the database schema. + +::: + + +## Create API endpoints + +In Serverpod, endpoints are defined in the `endpoints` folder (`lib/src`) within your server project (`notes_server`). The code generator analyzes the code within these endpoints and generates a client library based on the defined functions. This client library is then used by your Flutter app (`notes_flutter`) to interact with your backend server. + +Create a new file called `notes_endpoint.dart` inside `lib/src/endpoints` folder and add the following code: + +```dart +import 'package:serverpod/server.dart'; + +import '../generated/protocol.dart'; + +class NotesEndpoint extends Endpoint { + // Endpoint implementation goes here +} + +``` + +In the above code, we import the necessary dependencies and import the generated `Note` class from the `protocol.dart` file. We also define the `NotesEndpoint` class, which extends the `Endpoint` class provided by Serverpod. This is required for the endpoint to be recognized by Serverpod's code generator. + +### Define endpoints + +To define an endpoint that can be called from the client, we need to create a method inside the `NotesEndpoint` class. This method must return a `Future` of a **serializable object**, **primitive datatype**, or **void**. + +The first method parameter must be a `Session` object. This is a special object in Serverpod that contains information about the current session, as well as other helpful methods. + +```dart +Future example(Session session) async { + // Endpoint implementation goes here +} +``` + +The method is also allowed to have any number of extra parameters. These parameters will be passed from the client when the endpoint is called. The parameters follow the same type restrictions as the return type. + +### Store notes in the database + +To store notes in the database we define a `createNote` method in the `NotesEndpoint` class. This method takes a `Note` object and stores it in the database. To make the method accessible from the app (`notes_flutter`), make sure that the first parameter passed to this method is a `Session` object. + +```dart +Future createNote(Session session, Note note) async { + await Note.db.insertRow(session, note); +} +``` + +In the above code, we use the `Note.db.insertRow` method, created by `serverpod generate`, to insert the specified `Note` object into the database. + +### Delete notes from the database +To delete notes from the database we define a `deleteNote` method in the `NotesEndpoint` class. The method takes a `Note` object which represents the note that needs to be deleted. + +```dart +Future deleteNote(Session session, Note note) async { + await Note.db.deleteRow(session, note); +} +``` + +In the above code, we use the `Note.db.deleteRow` method to delete the specified `Note` object from the database. + + +### Fetch notes from the database + +To retrieve all notes from the database we define the `getAllNotes` method in the `NotesEndpoint` class. This method retrieves all the notes from the database and returns them as a list of `Note` objects. + +```dart +Future> getAllNotes(Session session) async { + // By ordering by the id column, we always get the notes in the same order + // and not in the order they were updated. + return await Note.db.find( + session, + orderBy: (t) => t.id, + ); +} +``` + +In the code above, we use the `Note.db.find` method to retrieve all the notes from the database. By specifying `orderBy: (t) => t.id`, we ensure that the notes are always returned in the same order based on the id column. + +Putting it all together you end up with a `notes_endpoint.dart` file that looks like this: + +```dart +import 'package:serverpod/server.dart'; + +import '../generated/protocol.dart'; + +class NotesEndpoint extends Endpoint { + Future> getAllNotes(Session session) async { + // By ordering by the id column, we always get the notes in the same order + // and not in the order they were updated. + return await Note.db.find( + session, + orderBy: (t) => t.id, + ); + } + + Future createNote(Session session, Note note) async { + await Note.db.insertRow(session, note); + } + + Future deleteNote(Session session, Note note) async { + await Note.db.deleteRow(session, note); + } +} +``` + +### Generate the client library + +Congratulations! You have now created all the endpoints needed for the notes app, complete with database integration that persistently stores the notes. + +Now run the code generator again to generate the client library for our endpoints. This needs to be run from the server directory `notes_server`. + +```bash +$ serverpod generate +``` + +You can find the newly generated code in the client directory `notes_client`. You normally don't need to touch this package, but it's good to know where it is located. + + +## Build the Flutter app + +It's time to build the `notes_flutter` app. + +Open the `main.dart` file in your Flutter project (`notes_flutter`). + +Locate the `MyHomePageState` class. + +Update the `MyHomePageState` class by removing all the unnecessary code, so that it looks like this: + +```dart +class MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + ); + } +} + +``` + +You can also remove the entire `_ResultDisplay` class from the file. + +Your `main.dart` file should now look like this: + +```dart +import 'package:notes_client/notes_client.dart'; +import 'package:flutter/material.dart'; +import 'package:serverpod_flutter/serverpod_flutter.dart'; + +// Sets up a singleton client object that can be used to talk to the server from +// anywhere in our app. The client is generated from your server code. +// The client is set up to connect to a Serverpod running on a local server on +// the default port. You will need to modify this to connect to staging or +// production servers. +var client = Client('http://localhost:8080/') + ..connectivityMonitor = FlutterConnectivityMonitor(); + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Serverpod Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Serverpod Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + MyHomePageState createState() => MyHomePageState(); +} + +class MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + ); + } +} +``` + +### Fetch the notes from the server + +To fetch all the notes from the server and handle potential connection failures, we first need to declare variables to hold the state inside the `MyHomePageState` class. + +```dart +import 'package:notes_client/notes_client.dart'; +... + +class MyHomePageState extends State { + + List? _notes; + Exception? _connectionException; + + ... +} +``` + +Let's create a method to handle the connection failures. Call it `_connectionFailed`, it updates the state to set `_notes` to `null` and stores the thrown exception in `_connectionException`. + +```dart +void _connectionFailed(dynamic exception) { + setState(() { + _notes = null; + _connectionException = exception; + }); +} +``` + +Next, let's add a method for fetching notes from the server endpoint we created earlier. The method updates the state with the notes we received. If the call fails, we catch an exception and call the `_connectionFailed` method instead. + +```dart +Future _loadNotes() async { + try { + final notes = await client.notes.getAllNotes(); + setState(() { + _notes = notes; + }); + } catch (e) { + _connectionFailed(e); + } +} +``` + +Since, we want to call the `_loadNotes` method when the app is started, we override the `initState` method and call the `_loadNotes` method from there. + +Add the `initState` method inside the `MyHomePageState` class. + +```dart +@override +void initState() { + super.initState(); + _loadNotes(); +} +``` + +### Render the notes + +To render the fetched notes in the app's main screen, we will update the `build` method inside the `MyHomePageState` class. Here is the modified code: + +```dart +@override +Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: _notes == null + ? Container() + : ListView.builder( + itemCount: _notes!.length, + itemBuilder: ((context, index) { + return ListTile( + title: Text(_notes![index].text), + ); + }), + ), + ); +} +``` + +The `_notes` variable is checked to determine if notes have been fetched from the server. If `_notes` is null, an empty `Container` widget is displayed as a placeholder. + +If `_notes` is not null, a `ListView.builder` widget is used to render the notes. The `itemCount` property is set to the length of the `_notes` list, and the itemBuilder callback is responsible for building the individual `ListTile` widgets. + +Inside the `itemBuilder` callback, each note's text is displayed in a `ListTile` using the `Text` widget. + +### Create new notes + +To create new notes we need to access the `createNote` method of our `notes` endpoint. To do this, we create a helper method called `_createNote` inside `MyHomePageState` class that takes a `Note` object as a parameter. + +```dart +Future _createNote(Note note) async { + try { + await client.notes.createNote(note); + await _loadNotes(); + } catch (e) { + _connectionFailed(e); + } +} +``` + +The `_createNote` method calls the `createNote` method (`lib/src/endpoints/notes_endpoint.dart` in `notes_server`) to store the note in the database on the server. Then `_loadNotes` is called to refresh our list of notes. + +The user needs graphical interface to create notes, so let's create a dialog for this. Create a new file called `note_dialog.dart` in the `lib` directory of your Flutter app. Add the following code: + +
+Code: note_dialog.dart +

+ +```dart +import 'package:flutter/material.dart'; + +void showNoteDialog({ + required BuildContext context, + String text = '', + required ValueChanged onSaved, +}) { + showDialog( + context: context, + builder: (context) => NoteDialog( + text: text, + onSaved: onSaved, + ), + ); +} + +class NoteDialog extends StatefulWidget { + const NoteDialog({ + required this.text, + required this.onSaved, + super.key, + }); + + final String text; + final ValueChanged onSaved; + + @override + NoteDialogState createState() => NoteDialogState(); +} + +class NoteDialogState extends State { + final TextEditingController controller = TextEditingController(); + + @override + void initState() { + super.initState(); + controller.text = widget.text; + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Expanded( + child: TextField( + controller: controller, + expands: true, + maxLines: null, + minLines: null, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Write your note here...', + ), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + widget.onSaved(controller.text); + Navigator.of(context).pop(); + }, + child: const Text('Save'), + ), + ], + ), + ), + ); + } +} +``` + +

+
+ +The dialog is needed but we will skip going into details of how it works as this is just normal Flutter code. The gist is that we have a function that triggers an input dialog and a callback for when the user saves the input. + +We need a button to trigger this dialog and a floating action button would be great for this. Add the following code to the build method inside the `MyHomePageState` class: + +```dart +@override +Widget build(BuildContext context) { + return Scaffold( + ... + floatingActionButton: _notes == null + ? null + : FloatingActionButton( + onPressed: () { + showNoteDialog( + context: context, + onSaved: (text) { + var note = Note( + text: text, + ); + _notes!.add(note); + + _createNote(note); + }, + ); + }, + child: const Icon(Icons.add), + ), + ); +} +``` + +Also import the dialog: + +```dart +import 'package:notes_flutter/note_dialog.dart'; +``` + +In the above code, we trigger the note dialog when the action button is pressed and then save the note in the `onSaved` callback. To make the UI feel more responsive, we add the changes to the notes list before calling `_createNote`. This way the note will be added to the list immediately and then updated when the server responds. + +Finally, add the loading screen to the project. Create a new file called `loading_screen.dart` in the `lib` directory of your Flutter app and add the following code. + +
+Code: loading_screen.dart +

+ +```dart +import 'package:flutter/material.dart'; + +class LoadingScreen extends StatelessWidget { + const LoadingScreen({ + this.exception, + required this.onTryAgain, + super.key, + }); + + final Exception? exception; + final VoidCallback onTryAgain; + + @override + Widget build(BuildContext context) { + if (exception != null) { + return Center( + child: ElevatedButton( + onPressed: onTryAgain, + child: const Text('Try again'), + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + } +} + +``` + +

+
+ +Replace the empty `Container` with the loading screen in the `build` method. + +
+Code: main.dart +

+ +```dart +import 'package:notes_client/notes_client.dart'; +import 'package:flutter/material.dart'; +import 'package:serverpod_flutter/serverpod_flutter.dart'; + +import 'note_dialog.dart'; + +// Sets up a singleton client object that can be used to talk to the server from +// anywhere in our app. The client is generated from your server code. +// The client is set up to connect to a Serverpod running on a local server on +// the default port. You will need to modify this to connect to staging or +// production servers. +var client = Client('http://localhost:8080/') + ..connectivityMonitor = FlutterConnectivityMonitor(); + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Notes', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Notes'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + MyHomePageState createState() => MyHomePageState(); +} + +class MyHomePageState extends State { + // This field holds the list of notes that we've received from the server or + // null if no notes have been received yet. + List? _notes; + + // If the connection to the server fails, this field will hold the exception + // that was thrown. + Exception? _connectionException; + + @override + void initState() { + super.initState(); + _loadNotes(); + } + + Future _loadNotes() async { + try { + final notes = await client.notes.getAllNotes(); + setState(() { + _notes = notes; + }); + } catch (e) { + _connectionFailed(e); + } + } + + Future _createNote(Note note) async { + try { + await client.notes.createNote(note); + await _loadNotes(); + } catch (e) { + _connectionFailed(e); + } + } + + void _connectionFailed(dynamic exception) { + // If the connection to the server fails, we clear the list of notes and + // store the exception that was thrown. This will make the loading screen + // appear and show a button to try again. + + // In a real app you would probably want to do more complete error handling. + setState(() { + _notes = null; + _connectionException = exception; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: _notes == null + ? LoadingScreen( + exception: _connectionException, + onTryAgain: _loadNotes, + ) + : ListView.builder( + itemCount: _notes!.length, + itemBuilder: ((context, index) { + return ListTile( + title: Text(_notes![index].text), + ); + }), + ), + floatingActionButton: _notes == null + ? null + : FloatingActionButton( + onPressed: () { + // When we tap the floating action button we want to show a + // dialog where we can create a new note. + showNoteDialog( + context: context, + onSaved: (text) { + var note = Note( + text: text, + ); + + // Add the note to the list of notes before we've received + // a response from the server which makes the UI feel more + // responsive. + _notes!.add(note); + + // Actually create the note on the server. + _createNote(note); + }, + ); + }, + child: const Icon(Icons.add), + ), + ); + } +} +``` + +Also import the loading screen: + +```dart +import 'package:notes_flutter/note_dialog.dart'; +``` + +

+
+ + +### Run the app + +Start the database and server: +Make sure you reboot the server if you started it earlier. + +```bash +$ cd notes_server +$ docker compose up --build --detach +$ dart bin/main.dart --apply-migrations +``` + +Start the Flutter app in Chrome (or the platform of your choice): + +```bash +$ cd notes_flutter +$ flutter run -d chrome +``` + + + +### Delete notes + +Implementing the delete functionality is very similar to the create functionality. We will add a delete button to each note and then call the delete endpoint when the button is pressed. First, let's add a helper method, `_deleteNote` inside the `MyHomePageState` class to call the endpoint: + +```dart +Future _deleteNote(Note note) async { + try { + await client.notes.deleteNote(note); + await _loadNotes(); + } catch (e) { + _connectionFailed(e); + } +} +``` +The `_deleteNote` method calls the `deleteNote` endpoint to delete the note from the database on the server. Then `_loadNotes` is called to refresh the notes list. + +Next in our `ListTile` we add a delete button and call the `_deleteNote` method when the button is pressed. Just as before we update the local state first to make the UI feel more responsive. + +```dart +ListTile( + ... + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + var note = _notes![index]; + + setState(() { + _notes!.remove(note); + }); + + _deleteNote(note); + }, + ), +), +``` + +We can now delete all the notes we have created. + + + + +### Edit notes + +We leave this part as an exercise for the reader. Try to see if you can implement the edit functionality. You can use the `showNoteDialog` method we created earlier to show the dialog for editing the note. If you followed along so far you have all the tools you need to implement this feature! + +Give it a go, in case you need some help you can look at the ([full code example](https://github.com/serverpod/notes)). + +## Summary + +In this tutorial, you learned how to build a simple note-taking app using the Serverpod backend framework. You started by setting up the necessary tools and environment to work with Serverpod. Then, you covered various aspects of building an app, including creating serializable objects, creating database tables, and creating API endpoints for performing CRUD operations. + +You also learned how to use Serverpod's code generator and how to use the generated client library to connect the Flutter app to the server. By establishing this connection, you were able to fetch data from the server and display it in the app. + +Throughout the tutorial, you gained an understanding of how Serverpod simplifies the development process by automatically handling the serialization and deserialization of objects, managing database tables, and seamlessly integrating with Flutter. You have acquired the foundational knowledge to build powerful and scalable server-side applications using Serverpod. + +Congratulations on completing this tutorial. You are now equipped with the skills to build your own server-side applications using Serverpod. Happy coding! + +Want to learn more? Check out some of our other tutorials, or the tutorials created by our community. \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/05-tutorials/02-real-time-communication.md b/versioned_docs/version-2.3.0/05-tutorials/02-real-time-communication.md new file mode 100644 index 00000000..e8107691 --- /dev/null +++ b/versioned_docs/version-2.3.0/05-tutorials/02-real-time-communication.md @@ -0,0 +1,403 @@ +# Real-time communication + +Have you ever found real-time communication in apps challenging? It doesn't have to be. Today, we're diving into how to build a collaborative drawing experience using Flutter and Serverpod. We'll call our app Pixorama - a fun and interactive project inspired by Reddit's r/place. Pixorama lets users draw together on a shared grid, with every pixel placed updating in real-time across all connected devices. + +
+ +_This tutorial is also available as a video._ + +:::info + +Before you begin, make sure that you have [installed Serverpod](/). It's also recommended that you read the [Get started with Mini](../get-started-with-mini) guide. + +::: + +You can try out the final app here: [https://pixorama.live](https://pixorama.live) + +![Serverpod Insights](/img/tutorial/pixorama/pixorama.png) + +## What is Pixorama? + +Pixorama is a collaborative drawing app where users can place pixels on a grid to create images together. Imagine two instances of the app running simultaneously - draw a pixel on one and watch it instantly appear on the other. This seamless synchronization happens because each time you draw a pixel, a message is sent to the server, which then broadcasts it to all connected clients. + +## Understanding real-time communication + +In traditional REST APIs, communication with the server involves sending a request and receiving a response. However, real-time communication requires the server to push updates to clients as they happen. This is commonly achieved using web sockets, which maintain an open connection between the server and client, allowing for continuous data exchange. While web sockets can be tricky, requiring data serialization and connection management, Serverpod simplifies this process. + +With the release of Serverpod 2.1, a new feature called [streaming methods](../concepts/streams) was introduced. This feature allows us to return a stream from a server method and call it from our app. Serverpod handles the underlying web socket connection for us. Now, let's get started with building Pixorama. + +## Setting up the project + +We begin by creating a new project with the `serverpod create` command. Since we don't need to store data in a database, we'll use the Mini version of Serverpod. Serverpod Mini is a lightweight version of Serverpod without a database, advanced logging, and other features - perfect for our needs. Create the project with the command: + +```bash +serverpod create pixorama --mini +``` + +Now, let's open the project in VS Code and explore the structure. The server code resides in the `pixorama_server` package. We'll start by creating models - classes that we can serialize and pass between the client and server. Our models will be placed in the `lib/src/models` directory. + +## Creating models + +First, we remove the `example.spy.yaml` model, as we won't need it. We'll create two new models: `ImageData` and `ImageUpdate`. Place them in the `lib/src/models` directory and call them `image_data.spy.yaml` and `image_update.spy.yaml`. + +```yaml +# lib/src/models/image_data.spy.yaml + +class: ImageData +fields: + pixels: ByteData + width: int + height: int +``` + +The `ImageData` model represents the entire image that will be sent to the app when it connects to the server. It stores the image's pixels as ByteData, where each byte represents a pixel. Additionally, it includes the image's width and height. + +```yaml +# lib/src/models/image_update.spy.yaml + +class: ImageUpdate +fields: + pixelIndex: int + colorIndex: int +``` + +The `ImageUpdate` model captures changes to individual pixels, including the pixel's index in the byte array and its new color value. + +With our models defined, we run serverpod generate to create the actual Dart files for these models. Run the command from your server's root directory (`pixorama_server`). + +```bash +cd pixorama_server +serverpod generate +``` + +## Building the server + +Next, we'll build the server. We need to create a new endpoint. An endpoint is a connection point for the client to interact with the server. In Serverpod, you create endpoints by extending the `Endpoint` class and placing it in the `lib/src/endpoints` directory. The endpoint will manage our pixel data and handle client updates. + +We will start by creating a `PixoramaEndpoint` class, which we place in a file called `pixorama_endpoint.dart` in the `lib/src/endpoints` directory. + +```dart +// lib/src/endpoints/pixorama_endpoint.dart + +import 'dart:typed_data'; + +import 'package:serverpod/serverpod.dart'; + +class PixoramaEndpoint extends Endpoint { + static const _imageWidth = 64; + static const _imageHeight = 64; + static const _numPixels = _imageWidth * _imageHeight; + + static const _numColorsInPalette = 16; + static const _defaultPixelColor = 2; + + final _pixelData = Uint8List(_numPixels) + ..fillRange( + 0, + _numPixels, + _defaultPixelColor, + ); +} +``` + +We define a number of constants that define the dimensions of our image. We represent the image itself with a `Uint8List`. Each byte in the list will be a pixel in our image. + +### Handling pixel updates + +The core functionality of Pixorama lies in how the server passes image data to clients and keeps them updated. Serverpod's built-in messaging system, Message Central, allows us to publish and subscribe to events. We create a channel named `pixel-added` to handle pixel updates in our `PixoramaEndpoint` class. + +```dart +// lib/src/endpoints/pixorama_endpoint.dart + +class PixoramaEndpoint extends Endpoint { + // ... + + static const _channelPixelAdded = 'pixel-added'; +} +``` + +When a user draws a pixel, the `setPixel` endpoint method is called. This method verifies the validity of the input (ensuring the color index is within the valid range and the pixel index is within bounds). If valid, it updates our pixel data and broadcasts the update to all listeners within the server via the `pixel-added` channel. + +```dart +// lib/src/endpoints/pixorama_endpoint.dart + +// Here we need to import the model files from our generated protocol. +import 'package:pixorama_server/src/generated/protocol.dart'; + +class PixoramaEndpoint extends Endpoint { + // ... + + static const _channelPixelAdded = 'pixel-added'; + + /// Sets a single pixel and notifies all connected clients about the change. + Future setPixel( + Session session, { + required int colorIndex, + required int pixelIndex, + }) async { + // Check that the input parameters are valid. If not, throw a + // `FormatException`, which will be logged and thrown as + // `ServerpodClientException` in the app. + if (colorIndex < 0 || colorIndex >= _numColorsInPalette) { + throw FormatException('colorIndex is out of range: $colorIndex'); + } + if (pixelIndex < 0 || pixelIndex >= _numPixels) { + throw FormatException('pixelIndex is out of range: $pixelIndex'); + } + + // Update our global image. + _pixelData[pixelIndex] = colorIndex; + + // Notify all connected clients that we set a pixel, by posting a message + // to the _channelPixelAdded channel. + session.messages.postMessage( + _channelPixelAdded, + ImageUpdate( + pixelIndex: pixelIndex, + colorIndex: colorIndex, + ), + ); + } +} +``` + +Finally, we create an `imageUpdates` method, which returns a stream of updates to clients. This method first sends the full image data to the client, followed by any subsequent pixel updates. This method will listen to updates from our `pixel-added` channel and relay them to the client. By creating the stream from the message channel before sending the first update to our client, we ensure no message risks being lost between the first full update and the first individual pixel being sent. + +```dart +// lib/src/endpoints/pixorama_endpoint.dart +class PixoramaEndpoint extends Endpoint { + // ... + + /// Returns a stream of image updates. The first message will always be a + /// `ImageData` object, which contains the full image. Sequential updates + /// will be `ImageUpdate` objects, which contains a single updated pixel. + Stream imageUpdates(Session session) async* { + // Request a stream of updates from the pixel-added channel in + // MessageCentral. + var updateStream = + session.messages.createStream(_channelPixelAdded); + + // Yield a first full image to the client. + yield ImageData( + pixels: _pixelData.buffer.asByteData(), + width: _imageWidth, + height: _imageHeight, + ); + + // Relay all individual pixel updates from the pixel-added channel to + // the client. + await for (var imageUpdate in updateStream) { + yield imageUpdate; + } + } +} +``` + +That's all the code we need to write for the server side. To make the new endpoint available to our Flutter app, we run serverpod generate in the root directory of our server. + +```bash +cd pixorama_server +serverpod generate +``` + +## Building the Flutter app + +With the server side complete, it's time to build the Flutter app. When we created the project, Serverpod set up a basic Flutter app for us in the `pixorama_flutter` package. + +First, we will use the pixels package to draw our pixel editor. Import it by running the following command in your `pixorama_flutter` directory: + +```bash +cd pixorama_flutter +flutter pub add pixels +``` + +Next, let's open the `main.dart` file and rename the `MyHomePage` class to `PixoramaApp`. We also remove the demo code and replace it with a `Scaffold` containing a `Pixorama` widget. This is our new main file: + +```dart +// lib/main.dart + +import 'package:pixorama_client/pixorama_client.dart'; +import 'package:flutter/material.dart'; +import 'package:serverpod_flutter/serverpod_flutter.dart'; + +import 'src/pixorama.dart'; + +var client = Client('http://$localhost:8080/') + ..connectivityMonitor = FlutterConnectivityMonitor(); + +void main() { + // Start the app. + runApp(const PixoramaApp()); +} + +class PixoramaApp extends StatelessWidget { + const PixoramaApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Pixorama', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Scaffold( + body: const Pixorama(), + ), + ); + } +} +``` + +Now, we will create the `Pixorama` widget. This is where all the drawing magic will happen. Create a new file called `pixorama.dart` and place it in `lib/src`. Start by creating a new stateful widget called `Pixorama`. We are also importing a few of the packages and files we are going to use: + +```dart +// lib/src/pixorama.dart + +import 'package:flutter/material.dart'; +import 'package:pixels/pixels.dart'; +import 'package:pixorama_client/pixorama_client.dart'; + +import '../../main.dart'; + +class Pixorama extends StatefulWidget { + const Pixorama({super.key}); + + @override + State createState() => _PixoramaState(); +} + +class _PixoramaState extends State { +} +``` + +The `Pixorama` widget draws the image using the `PixelEditor` from the `pixels` package. A `PixelImageController` manages the pixel data, and in the `initState` method, we call a `_listenToUpdates` method to connect to the server and listen for updates. Let's add the `PixelImageController` and `initState` method to our `_PixoramaState` class: + +```dart +// lib/src/pixorama.dart + +class _PixoramaState extends State { + // The pixel image controller contains our image data and handles updates. + // If it is null, the image has not yet been loaded from the server. + PixelImageController? _imageController; + + @override + void initState() { + super.initState(); + + // Connect to the server and start listening to updates. + _listenToUpdates(); + } +} +``` + +Next, let's implement the `_listenToUpdates` method. The `_listenToUpdates` method runs indefinitely, maintaining a connection to the server and processing updates as they arrive. It handles both `ImageData` (the full image) and `ImageUpdate` (individual pixel changes), updating the `PixelImageController` accordingly. If the connection is lost, it will wait 5 seconds before it tries to reconnect to the server. + +```dart +// lib/src/pixorama.dart + +class _PixoramaState extends State { + // ... + + Future _listenToUpdates() async { + // Indefinitely try to connect and listen to updates from the server. + while (true) { + try { + // Get the stream of updates from the server. + final imageUpdates = client.pixorama.imageUpdates(); + + // Listen for updates from the stream. The await for construct will + // wait for a message to arrive from the server, then run through the + // body of the loop. + await for (final update in imageUpdates) { + // Check which type of update we have received. + if (update is ImageData) { + // This is a complete image update, containing all pixels in the + // image. Create a new PixelImageController with the pixel data. + setState(() { + _imageController = PixelImageController( + pixels: update.pixels, + palette: PixelPalette.rPlace(), + width: update.width, + height: update.height, + ); + }); + } else if (update is ImageUpdate) { + // Got an incremental update of the image. Just set the single + // pixel. + _imageController?.setPixelIndex( + pixelIndex: update.pixelIndex, + colorIndex: update.colorIndex, + ); + } + } + } on MethodStreamException catch (_) { + // We lost the connection to the server, or failed to connect. + setState(() { + _imageController = null; + }); + } + + // Wait 5 seconds until we try to connect again. + await Future.delayed(Duration(seconds: 5)); + } + } +} +``` + +Worth noting is that the `MethodStreamException` is a superclass of a set of more detailed exceptions. It's often sufficient to catch all types of failures (like what we do here) that can happen when streaming data, but it's possible to detect if the stream failed because we failed to connect, if the server went down, or if the connection was lost. + +### Building the Interface + +Finally, we need to implement the widget's `build` method, where we create the user interface for drawing pixels. We display a progress indicator if the `_imageController` is `null` (indicating no image has been received yet). Once the image is received, we use the `PixelEditor` widget to render it, and any pixel changes made by the user are sent to the server via the `setPixel` method. + +```dart +// lib/src/pixorama.dart + +class _PixoramaState extends State { + // ... + + @override + Widget build(BuildContext context) { + return Center( + child: _imageController == null + ? const CircularProgressIndicator() + : PixelEditor( + controller: _imageController!, + onSetPixel: (details) { + // When a user clicks a pixel we will get a callback from the + // PixelImageController, with information about the changed + // pixel. When that happens we call the setPixels method on + // the server. + client.pixorama.setPixel( + pixelIndex: details.tapDetails.index, + colorIndex: details.colorIndex, + ); + }, + ), + ); + } +} +``` + +## Running Pixorama + +To test Pixorama, start the server by navigating to the `pixorama_server` directory and running: + +```bash +dart bin/main.dart +``` + +Then, launch the Flutter app by changing to the `pixorama_flutter` directory and running: + +```bash +flutter run -d chrome +``` + +You can also start a second instance of the app to see real-time updates reflected across both instances. + +## Conclusion + +This project was a brief introduction to building real-time apps with Flutter and Serverpod. With less than a page of code on the server side, we created a collaborative drawing app that's both fun and functional. You can find the full Pixorama code on GitHub here: +[https://github.com/serverpod/pixorama](https://github.com/serverpod/pixorama) + +Happy coding! diff --git a/versioned_docs/version-2.3.0/05-tutorials/03-code-example.md b/versioned_docs/version-2.3.0/05-tutorials/03-code-example.md new file mode 100644 index 00000000..1ce564b7 --- /dev/null +++ b/versioned_docs/version-2.3.0/05-tutorials/03-code-example.md @@ -0,0 +1,8 @@ +# Code examples + +Looking at examples can be a great way to learn. Here we collect samples created by the Serverpod team and the community. + +- __[Notes app](https://github.com/serverpod/notes)__: A simple note-taking app showcasing how to create and interact with endpoints and the database. +- __[Pixorama](https://pixorama.live)__: A multi-user drawing experience. Showcases Serverpod's real-time capabilities and is less than one page of code. +- __[Auth module](https://github.com/serverpod/serverpod/tree/main/examples/auth_example)__: Shows how to use the auth module and authenticate with different providers. +- __[Chat module](https://github.com/serverpod/serverpod/tree/main/examples/chat)__: Shows how to use the chat module to do a real-time chat. diff --git a/versioned_docs/version-2.3.0/05-tutorials/04-authentication.md b/versioned_docs/version-2.3.0/05-tutorials/04-authentication.md new file mode 100644 index 00000000..4e00da58 --- /dev/null +++ b/versioned_docs/version-2.3.0/05-tutorials/04-authentication.md @@ -0,0 +1,8 @@ +# Authentication + +Our comprehensive Authentication series is designed to guide you seamlessly through the process of setting up and utilizing the auth module within Serverpod. Each part of this series is tailored to introduce and explain different aspects of authentication, aiming to make it a straightforward process, regardless of your experience level. We recommend starting with the first one to complete the setup of the auth module before moving on to specific providers. + +- __[Setup & Email and Password](https://medium.com/serverpod/getting-started-with-serverpod-authentication-part-1-72c25280e6e9)__: This part covers the integration of the auth module into your project, and lays out the steps to establish Email and Password authentication. +- __[Google Sign in](https://medium.com/serverpod/integrating-google-sign-in-with-serverpod-authentication-part-2-6fade3099baf)__: We walk you through setting up Google Sign in as part of your authentication scheme using the auth module. + - __[Google API](https://medium.com/serverpod/working-with-the-google-api-in-serverpod-authentication-part-2-5-bbb077ec74d4)__: We dive deeper into the Google API, teaching you how to leverage it for enhanced functionalities and guiding users to grant access to their resources. +- __[Sign in with Apple](https://medium.com/serverpod/integrating-apple-sign-in-with-serverpod-authentication-part-3-f5a49d006800)__: Add Sign in with Apple to your app. diff --git a/versioned_docs/version-2.3.0/05-tutorials/_category_.json b/versioned_docs/version-2.3.0/05-tutorials/_category_.json new file mode 100644 index 00000000..db4d68bd --- /dev/null +++ b/versioned_docs/version-2.3.0/05-tutorials/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorials", + "collapsed": false +} diff --git a/versioned_docs/version-2.3.0/06-concepts/01-working-with-endpoints.md b/versioned_docs/version-2.3.0/06-concepts/01-working-with-endpoints.md new file mode 100644 index 00000000..72f155da --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/01-working-with-endpoints.md @@ -0,0 +1,92 @@ +# Working with endpoints + +Endpoints are the connection points to the server from the client. With Serverpod, you add methods to your endpoint, and your client code will be generated to make the method call. For the code to be generated, you need to place the endpoint file anywhere under the `lib` directory of your server. Your endpoint should extend the `Endpoint` class. For methods to be generated, they need to return a typed `Future`, and its first argument should be a `Session` object. The `Session` object holds information about the call being made and provides access to the database. + +```dart +import 'package:serverpod/serverpod.dart'; + +class ExampleEndpoint extends Endpoint { + Future hello(Session session, String name) async { + return 'Hello $name'; + } +} +``` + +The above code will create an endpoint called `example` (the Endpoint suffix will be removed) with the single `hello` method. To generate the client-side code run `serverpod generate` in the home directory of the server. + +On the client side, you can now call the method by calling: + +```dart +var result = await client.example.hello('World'); +``` + +The client is initialized like this: + +```dart +// Sets up a singleton client object that can be used to talk to the server from +// anywhere in our app. The client is generated from your server code. +// The client is set up to connect to a Serverpod running on a local server on +// the default port. You will need to modify this to connect to staging or +// production servers. +var client = Client('http://$localhost:8080/') + ..connectivityMonitor = FlutterConnectivityMonitor(); +``` + +If you run the app in an Android emulator, the `localhost` parameter points to `10.0.2.2`, rather than `127.0.0.1` as this is the IP address of the host machine. To access the server from a different device on the same network (such as a physical phone) replace `localhost` with the local ip address. You can find the local ip by running `ifconfig` (Linux/MacOS) or `ipconfig` (Windows). + +Make sure to also update the `publicHost` in the development config to make sure the server always serves the client with the correct path to assets etc. + +```yaml +# your_project_server/config/development.yaml + +apiServer: + port: 8080 + publicHost: localhost # Change this line + publicPort: 8080 + publicScheme: http +... +``` + +:::info + +You can pass the `--watch` flag to `serverpod generate` to watch for changed files and generate code whenever your source files are updated. This is useful during the development of your server. + +::: + +## Passing parameters + +There are some limitations to how endpoint methods can be implemented. Parameters and return types can be of type `bool`, `int`, `double`, `String`, `UuidValue`, `Duration`, `DateTime`, `ByteData`, or generated serializable objects (see next section). A typed `Future` should always be returned. Null safety is supported. When passing a `DateTime` it is always converted to UTC. + +You can also pass `List` and `Map` as parameters, but they need to be strictly typed with one of the types mentioned above. For `Map`, the keys must be non-nullable strings. E.g., `Map` is valid, but `Map` is not. + +:::warning + +While it's possible to pass binary data through a method call and `ByteData`, it is not the most efficient way to transfer large files. See our [file upload](file-uploads) interface. The size of a call is by default limited to 512 kB. It's possible to change by adding the `maxRequestSize` to your config files. E.g., this will double the request size to 1 MB: + +```yaml +maxRequestSize: 1048576 +``` + +::: + +## Return types + +The return type must be a typed Future. Supported return types are the same as for parameters. + +## Ignore endpoint definition + +If you want the code generator to ignore an endpoint definition, you can annotate the class with `@ignoreEndpoint`, imported from `serverpod_shared/annotations.dart`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it. + +```dart +import 'package:serverpod/serverpod.dart'; +import 'package:serverpod_shared/annotations.dart'; + +@ignoreEndpoint +class ExampleEndpoint extends Endpoint { + Future hello(Session session, String name) async { + return 'Hello $name'; + } +} +``` + +The above code will not generate any server or client bindings for the example endpoint. diff --git a/versioned_docs/version-2.3.0/06-concepts/02-models.md b/versioned_docs/version-2.3.0/06-concepts/02-models.md new file mode 100644 index 00000000..e807f617 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/02-models.md @@ -0,0 +1,346 @@ +# Working with models + +Models are Yaml files used to define serializable classes in Serverpod. They are used to generate Dart code for the server and client, and, if a database table is defined, to generate database code for the server. + +Using regular `.yaml` files within `lib/src/models` is supported, but it is recommended to use `.spy.yaml` (.spy stands for "Serverpod YAML"). Using this file type allows placing the model files anywhere in your servers `lib` directory and enables syntax highlighting provided by the [Serverpod Extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod) for VS Code. + +The files are analyzed by the Serverpod CLI when generating the project and creating migrations. + +Run `serverpod generate` to generate dart classes from the model files. + +## Class + +```yaml +class: Company +fields: + name: String + foundedDate: DateTime? + employees: List +``` + +Supported types are [bool](https://api.dart.dev/dart-core/bool-class.html), [int](https://api.dart.dev/dart-core/int-class.html), [double](https://api.dart.dev/dart-core/double-class.html), [String](https://api.dart.dev/dart-core/String-class.html), [Duration](https://api.dart.dev/dart-core/Duration-class.html), [DateTime](https://api.dart.dev/dart-core/DateTime-class.html), [ByteData](https://api.dart.dev/dart-typed_data/ByteData-class.html), [UuidValue](https://pub.dev/documentation/uuid/latest/uuid_value/UuidValue-class.html), and other serializable [classes](#class), [exceptions](#exception) and [enums](#enum). You can also use [List](https://api.dart.dev/dart-core/List-class.html)s and [Map](https://api.dart.dev/dart-core/Map-class.html)s of the supported types, just make sure to specify the types. Null safety is supported. Once your classes are generated, you can use them as parameters or return types to endpoint methods. + +### Limiting visibility of a generated class + +By default, generated code for your serializable objects is available both on the server and the client. You may want to have the code on the server side only. E.g., if the serializable object is connected to a database table containing private information. + +To make a serializable class generated only on the server side, set the serverOnly property to true. + +```yaml +class: MyPrivateClass +serverOnly: true +fields: + hiddenSecretKey: String +``` + +It is also possible to set a `scope` on a per-field basis. By default all fields are visible to both the server and the client. The available scopes are `all`, `serverOnly`, `none`. + +:::info +**none** is not typically used in serverpod apps. It is intended for the serverpod framework, itself. +::: + +```yaml +class: SelectivelyHiddenClass +fields: + hiddenSecretKey: String, scope=serverOnly + publicKey: String +``` + +:::info +Serverpod's models can easily be saved to or read from the database. You can read more about this in the [Database](database/models) section. +::: + +## Exception + +The Serverpod models supports creating exceptions that can be thrown in endpoints by using the `exception` keyword. For more in-depth description on how to work with exceptions see [Error handling and exceptions](exceptions). + +```yaml +exception: MyException +fields: + message: String + errorType: MyEnum +``` + +## Enum + +It is easy to add custom enums with serialization support by using the `enum` keyword. + +```yaml +enum: Animal +values: + - dog + - cat + - bird +``` + +By default the serialization will convert the enum to an int representing the index of the value. Changing the order may therefore have unforeseen consequences when reusing old data (such as from a database). Changing the serialization to be based on the name instead of index is easy. + +```yaml +enum: Animal +serialized: byName +values: + - dog + - cat + - bird +``` + +`serialized` has two valid values `byName` and `byIndex`. When using `byName` the string literal of the enum is used, when using `byIndex` the index value (0, 1, 2, etc) is used. + +:::info + +It's recommended to always set `serialized` to `byName` in any new Enum models, as this is less fragile and will be changed to the default setting in version 3 of Serverpod. + +::: + +## Adding documentation + +Serverpod allows you to add documentation to your serializable objects in a similar way that you would add documentation to your Dart code. Use three hashes (###) to indicate that a comment should be considered documentation. + +```yaml +### Information about a company. +class: Company +fields: + ### The name of the company. + name: String + + ### The date the company was founded, if known. + foundedDate: DateTime? + + ### A list of people currently employed at the company. + employees: List +``` + +## Generated code + +Serverpod generates some convenience methods on the Dart classes. + +### copyWith + +The `copyWith` method allows for efficient object copying with selective field updates and is available on all generated classes. Here's how it operates: + +```dart +var john = User(name: 'John Doe', age: 25); +var jane = john.copyWith(name: 'Jane Doe'); +``` + +The `copyWith` method generates a deep copy of an object, preserving all original fields unless explicitly modified. It can distinguish between a field set to `null` and a field left unspecified (undefined). When using `copyWith`, any field you don't update remains unchanged in the new object. + +### toJson / fromJson + +The `toJson` and `fromJson` methods are generated on all models to help with serialization. Serverpod manages all serialization for you out of the box and you will rarely have to use these methods by your self. See the [Serialization](serialization) section for more info. + +### Custom methods + +Sometimes you will want to add custom methods to the generated classes. The easiest way to do this is with [Dart's extension feature](https://dart.dev/language/extension-methods). + +```dart +extension MyExtension on MyClass { + bool isCustomMethod() { + return true; + } +} +``` + +## Default Values + +Serverpod supports defining default values for fields in your models. These default values can be specified using three different keywords that determine how and where the defaults are applied: + +### Keywords + +- **default**: This keyword sets a default value for both the model (code) and the database (persisted data). It acts as a general fallback if more specific defaults aren't provided. +- **defaultModel**: This keyword sets a default value specifically for the model (the code side). If `defaultModel` is not provided, the model will use the value specified by `default` if it's available. +- **defaultPersist**: This keyword sets a default value specifically for the database. If `defaultPersist` is not provided, the database will use the value specified by `default` if it's available. + +### How priorities work + +- **For the model (code side):** If both `defaultModel` and `default` are provided, the model will use the `defaultModel` value. If `defaultModel` is not provided, it will fall back to using the `default` value. +- **For the database (persisted data):** If both `defaultPersist` and `default` are provided, the database will use the `defaultPersist` value. If `defaultPersist` is not provided, it will fall back to using the `default` value. + +You can use these default values individually or in combination as needed. It is not required to use all default types for a field. + +:::info + +When using `default` or `defaultModel` in combination with `defaultPersist`, it's important to understand how the interaction between these keywords affects the final value in the database. + +If you set a `default` or `defaultModel` value, the model's field or variable will have a value when it's passed to the database—it will not be `null`. Because of this, the SQL query will not use the `defaultPersist` value since the field already has a value assigned by the model. In essence, assigning a `default` or `defaultModel` is like directly providing a value to the field, and the database will use this provided value instead of its own default. + +This means that `defaultPersist` only comes into play when the model does not provide a value, allowing the database to apply its own default setting. + +::: + +### Supported default values + +#### Boolean + +| Type | Keyword | Description | +|-----------------|--------------------|-------------------------------------------------------| +| **Boolean** | `true` or `false` | Sets the field to a boolean value, either `true` or `false`. | + +**Example:** + +```yaml +boolDefault: bool, default=true +``` + +#### DateTime + +| Type | Keyword | Description | +|-------------------------|---------------|--------------------------------------------------------------| +| **Current Date and Time** | `now` | Sets the field to the current date and time. | +| **Specific UTC DateTime** | UTC DateTime string in the format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` | Sets the field to a specific date and time. | + +**Example:** + +```yaml +dateTimeDefaultNow: DateTime, default=now +dateTimeDefaultUtc: DateTime, default=2024-05-01T22:00:00.000Z +``` + +#### Double + +| Type | Keyword | Description | +|-----------------|--------------------|-------------------------------------------------------| +| **Double** | Any double value | Sets the field to a specific double value. | + +**Example:** + +```yaml +doubleDefault: double, default=10.5 +``` + +#### Duration + +| Type | Keyword | Description | +|---------------------|---------------------|-----------------------------------------------------------------------------| +| **Specific Duration** | A valid duration in the format `Xd Xh Xmin Xs Xms` | Sets the field to a specific duration value. For example, `1d 2h 10min 30s 100ms` represents 1 day, 2 hours, 10 minutes, 30 seconds, and 100 milliseconds. | + +**Example:** + +```yaml +durationDefault: Duration, default=1d 2h 10min 30s 100ms +``` + +#### Enum + +| Type | Keyword | Description | +|-----------------|--------------------|-------------------------------------------------------| +| **Enum** | Any valid enum value | Sets the field to a specific enum value. | + +**Example:** + +```yaml +enum: ByNameEnum +serialized: byName +values: + - byName1 + - byName2 +``` + +```yaml +enum: ByIndexEnum +serialized: byIndex +values: + - byIndex1 + - byIndex2 +``` + +```yaml +class: EnumDefault +table: enum_default +fields: + byNameEnumDefault: ByNameEnum, default=byName1 + byIndexEnumDefault: ByIndexEnum, default=byIndex1 +``` + +In this example: + +- The `byNameEnumDefault` field will default to `'byName1'` in the database. +- The `byIndexEnumDefault` field will default to `0` (the index of `byIndex1`). + +#### Integer + +| Type | Keyword | Description | +|-----------------|--------------------|-------------------------------------------------------| +| **Integer** | Any integer value | Sets the field to a specific integer value. | + +**Example:** + +```yaml +intDefault: int, default=10 +``` + +#### String + +| Type | Keyword | Description | +|-----------------|--------------------|-------------------------------------------------------| +| **String** | Any string value | Sets the field to a specific string value. | + +**Example:** + +```yaml +stringDefault: String, default='This is a string' +``` + +#### UuidValue + +| Type | Keyword | Description | +|--------------------|--------------------|-------------------------------------------------------| +| **Random UUID** | `random` | Generates a random UUID. On the Dart side, `Uuid().v4obj()` is used. On the database side, `gen_random_uuid()` is used. | +| **UUID String** | A valid UUID version 4 string | Assigns a specific UUID to the field. | + +**Example:** + +```yaml +uuidDefaultRandom: UuidValue, default=random +uuidDefaultUuid: UuidValue, default='550e8400-e29b-41d4-a716-446655440000' +``` + +### Example + +```yaml +class: DefaultValue +table: default_value +fields: + ### Sets the current date and time as the default value. + dateTimeDefault: DateTime, default=now + + ### Sets the default value for a boolean field. + boolDefault: bool, defaultModel=false, defaultPersist=true + + ### Sets the default value for an integer field. + intDefault: int, defaultPersist=20 + + ### Sets the default value for a double field. + doubleDefault: double, default=10.5, defaultPersist=20.5 + + ### Sets the default value for a string field. + stringDefault: String, default="This is a string", defaultModel="This is a string" +``` + +## Keywords + +|**Keyword**|Note|[class](#class)|[exception](#exception)|[enum](#enum)| +|---|---|:---:|:---:|:---:| +|[**values**](#enum)|A special key for enums with a list of all enum values. |||✅| +|[**serialized**](#enum)|Sets the mode enums are serialized in |||✅| +|[**serverOnly**](#limiting-visibility-of-a-generated-class)|Boolean flag if code generator only should create the code for the server. |✅|✅|✅| +|[**table**](database/models)|A name for the database table, enables generation of database code. |✅||| +|[**managedMigration**](database/migrations#opt-out-of-migrations)|A boolean flag to opt out of the database migration system. |✅||| +|[**fields**](#class)|All fields in the generated class should be listed here. |✅|✅|| +|[**type (fields)**](#class)|Denotes the data type for a field. |✅|✅|| +|[**scope**](#limiting-visibility-of-a-generated-class)|Denotes the scope for a field. |✅||| +|[**persist**](database/models)|A boolean flag if the data should be stored in the database or not can be negated with `!persist` |✅||| +|[**relation**](database/relations/one-to-one)|Sets a relation between model files, requires a table name to be set. |✅||| +|[**name**](database/relations/one-to-one#bidirectional-relations)|Give a name to a relation to pair them. |✅||| +|[**parent**](database/relations/one-to-one#with-an-id-field)|Sets the parent table on a relation. |✅||| +|[**field**](database/relations/one-to-one#custom-foreign-key-field)|A manual specified foreign key field. |✅||| +|[**onUpdate**](database/relations/referential-actions)|Set the referential actions when updating data in the database. |✅||| +|[**onDelete**](database/relations/referential-actions)|Set the referential actions when deleting data in the database. |✅||| +|[**optional**](database/relations/one-to-one#optional-relation)|A boolean flag to make a relation optional. |✅||| +|[**indexes**](database/indexing)|Create indexes on your fields / columns. |✅||| +|[**fields (index)**](database/indexing)|List the fields to create the indexes on. |✅||| +|[**type (index)**](database/indexing)|The type of index to create. |✅||| +|[**unique**](database/indexing)|Boolean flag to make the entries unique in the database. |✅||| +|[**default**](#default-values)|Sets the default value for both the model and the database. This keyword cannot be used with **relation**. |✅||| +|[**defaultModel**](#default-values)|Sets the default value for the model side. This keyword cannot be used with **relation**. |✅||| +|[**defaultPersist**](#default-values)|Sets the default value for the database side. This keyword cannot be used with **relation** and **!persist**. |✅||| diff --git a/versioned_docs/version-2.3.0/06-concepts/03-serialization.md b/versioned_docs/version-2.3.0/06-concepts/03-serialization.md new file mode 100644 index 00000000..438af210 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/03-serialization.md @@ -0,0 +1,179 @@ +# Custom serialization + +For most purposes, you will want to use Serverpod's native serialization. However, there may be cases where you want to serialize more advanced objects. With Serverpod, you can pass any serializable objects as long as they conform to three simple rules: + +1. Your objects must have a method called `toJson()` which returns a JSON serialization of the object. + + ```dart + Map toJson() { + return { + name: 'John Doe', + }; + } + ``` + +2. There must be a constructor or factory called `fromJson()`, which takes a JSON serialization as parameters. + + ```dart + factory ClassName.fromJson( + Map json, + ) { + return ClassName( + name: json['name'] as String, + ); + } + ``` + +3. There must be a method called `copyWith()`, which returns a new instance of the object with the specified fields replaced. + :::tip + In the framework, `copyWith()` is implemented as a deep copy to ensure immutability. We recommend following this approach when implementing it for custom classes to avoid unintentional side effects caused by shared mutable references. + ::: + + ```dart + ClassName copyWith({ + String? name, + }) { + return ClassName( + name: name ?? this.name, + ); + } + ``` + +4. You must declare your custom serializable objects in the `config/generator.yaml` file in the server project, the path needs to be accessible from both the server package and the client package. + + ```yaml + ... + extraClasses: + - package:my_project_shared/my_project_shared.dart:ClassName + ``` + +## Setup example + +We recommend creating a new dart package specifically for sharing these types of classes and importing it into the server and client `pubspec.yaml`. This can easily be done by running `$ dart create -t package _shared` in the root folder of your project. + +Your folder structure should then look like this: + +```text +├── my_project_client +├── my_project_flutter +├── my_project_server +├── my_project_shared +``` + +Then you need to update both your `my_project_server/pubspec.yaml` and `my_project_client/pubspec.yaml` and add the new package as a dependency. + +```yaml +dependencies: + ... + my_project_shared: + path: ../my_project_shared + ... +``` + +Now you can create your custom class in your new shared package: + +```dart +class ClassName { + String name; + ClassName(this.name); + + toJson() { + return { + 'name': name, + }; + } + + factory ClassName.fromJson( + Map jsonSerialization, + ) { + return ClassName( + jsonSerialization['name'], + ); + } +} +``` + +After adding a new serializable class, you must run `serverpod generate`. You are now able to use this class in your endpoints and leverage the full serialization/deserialization management that comes with Serverpod. + +In your server project, you can create an endpoint returning your custom object. + +```dart +import 'package:relation_test_shared/relation_test_shared.dart'; +import 'package:serverpod/serverpod.dart'; + +class ExampleEndpoint extends Endpoint { + Future getMyCustomClass(Session session) async { + return ClassName( + 'John Doe', + ); + } +} +``` + +## Custom class with Freezed + +Serverpod also has support for using custom classes created with the [Freezed](https://pub.dev/packages/freezed) package. + +```dart +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'freezed_custom_class.freezed.dart'; +part 'freezed_custom_class.g.dart'; + +@freezed +class FreezedCustomClass with _$FreezedCustomClass { + const factory FreezedCustomClass({ + required String firstName, + required String lastName, + required int age, + }) = _FreezedCustomClass; + + factory FreezedCustomClass.fromJson( + Map json, + ) => + _$FreezedCustomClassFromJson(json); +} +``` + +In the config/generator.yaml, you declare the package and the class: + +```yaml +extraClasses: + - package:my_shared_package/my_shared_package.dart:FreezedCustomClass +``` + +## Custom class with ProtocolSerialization + +If you need certain fields to be omitted when transmitting to the client-side, your server-side custom class should implement the `ProtocolSerialization` interface. This requires adding a method named `toJsonForProtocol()`. Serverpod will then use this method to serialize your object for protocol communication. If the class does not implement `ProtocolSerialization`, Serverpod defaults to using the `toJson()` method. + +### Implementation Example + +Here’s how you can implement it: + +```dart +class CustomClass implements ProtocolSerialization { + final String? value; + final String? serverSideValue; + + ....... + + // Serializes fields specifically for protocol communication + Map toJsonForProtocol() { + return { + "value":value, + }; + } + + // Serializes all fields, including those intended only for server-side use + Map toJson() { + return { + "value": value, + "serverSideValue": serverSideValue, + }; + } +} +``` + +This structure ensures that sensitive or server-only data is not exposed to the client, enhancing security and data integrity. + +Importantly, this implementation is not required for client-side custom models. diff --git a/versioned_docs/version-2.3.0/06-concepts/04-exceptions.md b/versioned_docs/version-2.3.0/06-concepts/04-exceptions.md new file mode 100644 index 00000000..e4baa660 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/04-exceptions.md @@ -0,0 +1,74 @@ +# Error handling and exceptions + +Handling errors well is essential when you are building your server. To simplify things, Serverpod allows you to throw an exception on the server, serialize it, and catch it in your client app. + +If you throw a normal exception that isn't caught by your code, it will be treated as an internal server error. The exception will be logged together with its stack trace, and a 500 HTTP status (internal server error) will be sent to the client. On the client side, this will throw a non-specific ServerpodException, which provides no more data than a session id number which can help identifiy the call in your logs. + +:::tip + +Use the Serverpod Insights app to view your logs. It will show any failed or slow calls and will make it easy to pinpoint any errors in your server. + +::: + +## Serializable exceptions + +Serverpod allows adding data to an exception you throw on the server and extracting that data in the client. This is useful for passing error messages back to the client when a call fails. You use the same YAML-files to define the serializable exceptions as you would with any serializable model (see [serialization](serialization) for details). The only difference is that you use the keyword `exception` instead of `class`. + +```yaml +exception: MyException +fields: + message: String + errorType: MyEnum +``` + +After you run `serverpod generate`, you can throw that exception when processing a call to the server. + +```dart +class ExampleEndpoint extends Endpoint { + Future doThingy(Session session) { + // ... do stuff ... + if (failure) { + throw MyException( + message: 'Failed to do thingy', + errorType: MyEnum.thingyError, + ); + } + } +} +``` + +In your app, catch the exception as you would catch any exception. + +```dart +try { + client.example.doThingy(); +} +on MyException catch(e) { + print(e.message); +} +catch(e) { + print('Something else went wrong.'); +} +``` + +### Default values in exceptions + +Serverpod allows you to specify default values for fields in exceptions, similar to how it's done in models using the `default` and `defaultModel` keywords. If you're unfamiliar with how these keywords work, you can refer to the [Default Values](models#default-values) section in the [Working with Models](models) documentation. + +:::info +Since exceptions are not persisted in the database, the `defaultPersist` keyword is not supported. If both `default` and `defaultModel` are specified, `defaultModel` will always take precedence, making it unnecessary to use both. +::: + +**Example:** + +```yaml +exception: MyException +fields: + message: String, default="An error occurred" + errorCode: int, default=1001 +``` + +In this example: + +- The `message` field will default to `"An error occurred"` if not provided. +- The `errorCode` field will default to `1001`. diff --git a/versioned_docs/version-2.3.0/06-concepts/05-sessions.md b/versioned_docs/version-2.3.0/06-concepts/05-sessions.md new file mode 100644 index 00000000..1d073271 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/05-sessions.md @@ -0,0 +1,26 @@ +# Sessions + +The `Session` object provides information about the current context in a method call in Serverpod. It provides access to the database, caching, authentication, data storage, and messaging within the server. It will also contain information about the HTTP request object. + +If you need additional information about a call, you may need to cast the Session to one of its subclasses, e.g., `MethodCallSession` or `StreamingSession`. The `MethodCallSession` object provides additional properties, such as the name of the endpoint and method and the underlying `HttpRequest` object. + +:::tip + +You can use the Session object to access the IP address of the client calling a method. Serverpod includes an extension on `HttpRequest` that allows you to access the IP address even if your server is running behind a load balancer. + +```dart +session as MethodCallSession; +var ipAddress = session.httpRequest.remoteIpAddress; +``` + +::: + +## Creating sessions + +In most cases, Serverpod manages the life cycle of the Session objects for you. A session is created for a call or a streaming connection and is disposed of when the call has been completed. In rare cases, you may want to create a session manually. For instance, if you are making a database call outside the scope of a method or a future call. In these cases, you can create a new session with the `createSession` method of the `Serverpod` singleton. You can access the singleton by the static `Serverpod.instance` field. If you create a new session, you are also responsible for closing it using the `session.close()` method. + +:::note + +It's not recommended to keep a session open indefinitely as it can lead to memory leaks, as the session stores logs until it is closed. It's inexpensive to create a new session, so keep them short. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/01-connection.md b/versioned_docs/version-2.3.0/06-concepts/06-database/01-connection.md new file mode 100644 index 00000000..089d29e7 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/01-connection.md @@ -0,0 +1,107 @@ +# Connection + +In Serverpod the connection details and password for the database are stored inside the `config` directory in your server package. Serverpod automatically establishes a connection to the Postgres instance by using these configuration details when you start the server. + +The easiest way to get started is to use a Docker container to run your local Postgres server, and this is how Serverpod is set up out of the box. This page contains more detailed information if you want to connect to another database instance or run Postgres locally yourself. + + +### Connection details + +Each environment configuration contains a `database` keyword that specifies the connection details. +For your development build you can find the connection details in the `config/development.yaml` file. + +This is an example: + +```yaml +... +database: + host: localhost + port: 8090 + name: + user: postgres +... +``` + +The `name` refers to the database name, `host` is the domain name or IP address pointing to your Postgres instance, `port` is the port that Postgres is listening to, and `user` is the username that is used to connect to the database. + +:::caution + +By default, Postgres is listening for connections on port 5432. However, the Docker container shipped with Serverpod uses port 8090 to avoid conflicts. If you host your own instance, double-check that the correct port is specified in your configuration files. + +::: + +### Database password + +The database password is stored in a separate file called `passwords.yaml` in the same `config` directory. The password for each environment is stored under the `database` keyword in the file. + +An example of this could look like this: + +```yaml +... +development: + database: '' +... +``` + +## Development database + +A newly created Serverpod project has a preconfigured Docker instance with a Postgres database set up. Run the following command from the root of the `server` package to start the database: + +```bash +$ docker compose up --build --detach +``` + +To stop the database run: + +```bash +$ docker compose stop +``` + +To remove the database and __delete__ all associated data, run: + +```bash +$ docker compose down -v +``` + +## Connecting to a custom Postgres instance + +Just like you can connect to the Postgres database inside the Docker container, you can connect to any other Postgres instance. There are a few things you need to take into consideration: + +- Make sure that your Postgres instance is up and running and is reachable from your Serverpod server. +- You will need to create a user with a password, and a database. + +### Connecting to a local Postgres server + +If you want to connect to a local Postgres Server (with the default setup) then the `development.yaml` will work fine if you set the correct port, user, database, and update the password in the `passwords.yaml` file. + +### Connecting to a remote Postgres server + +To connect to a remote Postgres server (that you have installed on a VPS or VDS), you need to follow a couple of steps: + +- Make sure that the Postgres server has a reachable network address and that it accepts incoming traffic. +- You may need to open the database port on the machine. This may include configuring its firewall. +- Update your Serverpod `database` config to use the public network address, database name, port, user, and password. + + +### Connecting to Google Cloud SQL + +You can connect to a Google Cloud SQL Postgres instance in two ways: + +1. Setting up the _Public IP Authorized networks_ (with your Serverpod server IP) and changing the database host string to the _Cloud SQL public IP_. +2. Using the _Connection String_ if you are hosting your Serverpod server on Google Cloud Run and changing the database host string to the Cloud SQL: `/cloudsql/my-project:server-location:database-name/.s.PGSQL.5432`. + +The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. + +:::info + +If you are using the `isUnixSocket` don't forget to add **"/.s.PGSQL.5432"** to the end of the `host` IP address. Otherwise, your Google Cloud Run instance will not be able to connect to the database. + +::: + +### Connecting to AWS RDS + +You can connect to an AWS RDS Instance in two ways: +1. Enable public access to the database and configure VPC/Subnets to accept your Serverpod's IP address. +2. Use the Endpoint `database-name.some-unique-id.server-location.rds.amazonaws.com` to connect to it from AWS ECS. + +The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/02-models.md b/versioned_docs/version-2.3.0/06-concepts/06-database/02-models.md new file mode 100644 index 00000000..b5a48b78 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/02-models.md @@ -0,0 +1,55 @@ +# Models + +It's possible to map serializable models to tables in your database. To do this, add the `table` key to your yaml file: + +```yaml +class: Company +table: company +fields: + name: String +``` + +When the `table` keyword is added to the model, the `serverpod generate` command will generate new methods for [interacting](crud) with the database. The addition of the keyword will also be detected by the `serverpod create-migration` command that will generate the necessary [migrations](migrations) needed to update the database. + +:::info + +When you add a `table` to a serializable class, Serverpod will automatically add an `id` field of type `int?` to the class. You should not define this field yourself. The `id` is set when you interact with an object stored in the database. + +::: + +### Non persistent fields + +You can opt out of creating a column in the database for a specific field by using the `!persist` keyword. + +```yaml +class: Company +table: company +fields: + name: String, !persist +``` + +All fields are persisted by default and have an implicit `persist` set on each field. + +### Data representation + +Storing a field with a primitive / core dart type will be handled as its respective type. However, if you use a complex type, such as another model, a `List`, or a `Map`, these will be stored as a `json` object in the database. + +```yaml +class: Company +table: company +fields: + address: Address # Stored as a json column +``` + +This means that each row has its own copy of the nested object that needs to be updated individually. If you instead want to reference the same object from multiple different tables, you can use the `relation` keyword. + +This creates a database relation between two tables and always keeps the data in sync. + +```yaml +class: Company +table: company +fields: + address: Address?, relation +``` + +For a complete guide on how to work with relations see the [relation section](relations/one-to-one). diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/01-one-to-one.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/01-one-to-one.md new file mode 100644 index 00000000..8f95e73a --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/01-one-to-one.md @@ -0,0 +1,193 @@ +# One-to-one + +One-to-one (1:1) relationships represent a unique association between two entities, there is at most one model that can be connected on either side of the relation. This means we have to set a **unique index** on the foreign key in the database. Without the unique index the relation would be considered a one-to-many (1:n) relation. + +## Defining the Relationship + +In the following examples we show how to configure a 1:1 relationship between `User` and `Address`. + +### With an id field + +In the most simple case, all we have to do is add an `id` field on one of the models. + +```yaml +# address.yaml +class: Address +table: address +fields: + street: String + +# user.yaml +class: User +table: user +fields: + addressId: int, relation(parent=address) // Foreign key field +indexes: + user_address_unique_idx: + fields: addressId + unique: true +``` + +In the example, the `relation` keyword annotates the `addressId` field to hold the foreign key. The field needs to be of type `int` and the relation keyword needs to specify the `parent` parameter. The `parent` parameter defines which table the relation is towards, in this case the `Address` table. + +The addressId is **required** in this example because the field is not nullable. That means that each `User` must have a related `Address`. If you want to make the relation optional, change the datatype from `int` to `int?`. + +When fetching a `User` from the database the `addressId` field will automatically be populated with the related `Address` object `id`. + +### With an object + +While the previous example highlights manual handling of data, there's an alternative approach that simplifies data access using automated handling. By directly specifying the Address type in the User class, Serverpod can automatically handle the relation for you. + +```yaml +# address.yaml +class: Address +table: address +fields: + street: String + +# user.yaml +class: User +table: user +fields: + address: Address?, relation // Object relation field +indexes: + user_address_unique_idx: + fields: addressId + unique: true +``` + +In this example, we define an object relation field by annotating the `address` field with the `relation` keyword where the type is another model, `Address?`. + +Serverpod then automatically generates a foreign key field (as seen in the last example) named `addressId` in the `User` class. This auto-generated field is non-nullable by default and is by default always named from the object relation field with the suffix `Id`. + +The object field, in this case `address`, must always be nullable (as indicated by `Address?`). + +An object relation field gives a big advantage when fetching data. Utilizing [relational queries](../relation-queries) enables filtering based on relation attributes or optionally including the related data in the result. + +No `parent` keyword is needed here because the relational table is inferred from the type on the field. + +### Optional relation + +```yaml +# user.yaml +class: User +table: user +fields: + address: Address?, relation(optional) +indexes: + user_address_unique_idx: + fields: addressId + unique: true +``` + +With the introduction of the `optional` keyword in the relation, the automatically generated `addressId` field becomes nullable. This means that the `addressId` can either hold a foreign key to the related `address` table or be set to null, indicating no associated address. + +### Custom foreign key field + +Serverpod also provides a way to customize the name of the foreign key field used in an object relation. + +```yaml +# user.yaml +class: User +table: user +fields: + customIdField: int + address: Address?, relation(field=customIdField) +indexes: + user_address_unique_idx: + fields: customIdField + unique: true +``` + +In this example, we define a custom foreign key field with the `field` parameter. The argument defines what field that is used as the foreign key field. In this case, `customIdField` is used instead of the default auto-generated name. + +If you want the custom foreign key to be nullable, simply define its type as `int?`. Note that the `field` keyword cannot be used in conjunction with the `optional` keyword. Instead, directly mark the field as nullable. + +### Generated SQL + +The following code block shows how to set up the same relation with raw SQL. Serverpod will generate this code behind the scenes. + +```sql +CREATE TABLE "address" ( + "id" serial PRIMARY KEY, + "street" text NOT NULL +); + +CREATE TABLE "user" ( + "id" serial PRIMARY KEY, + "addressId" integer NOT NULL +); + + +CREATE UNIQUE INDEX "user_address_unique_idx" ON "user" USING btree ("addressId"); + +ALTER TABLE ONLY "user" + ADD CONSTRAINT "user_fk_0" + FOREIGN KEY("addressId") + REFERENCES "address"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +``` + +## Independent relations defined on both sides + +You are able to define as many independent relations as you wish on each side of the relation. This is useful when you want to have multiple relations between two entities. + +```yaml +# user.yaml +class: User +table: user +fields: + friendsAddress: Address?, relation +indexes: + user_address_unique_idx: + fields: friendsAddressId + unique: true + +# address.yaml +class: Address +table: address +fields: + street: String + resident: User?, relation +indexes: + address_user_unique_idx: + fields: residentId + unique: true +``` + +Both relations operate independently of each other, resulting in two distinct relationships with their respective unique indexes. + +## Bidirectional relations + +If access to the same relation is desired from both sided, a bidirectional relation can be defined. + +```yaml +# user.yaml +class: User +table: user +fields: + addressId: int + address: Address?, relation(name=user_address, field=addressId) +indexes: + user_address_unique_idx: + fields: addressId + unique: true + +# address.yaml +class: Address +table: address +fields: + street: String + user: User?, relation(name=user_address) +``` + +The example illustrates a 1:1 relationship between User and Address where both sides of the relationship are explicitly specified. + +Using the `name` parameter, we define a shared name for the relationship. It serves as the bridge connecting the `address` field in the User class to the `user` field in the Address class. Meaning that the same `User` referencing an `Address` is accessible from the `Address` as well. + +Without specifying the `name` parameter, you'd end up with two unrelated relationships. + +When the relationship is defined on both sides, it's **required** to specify the `field` keyword. This is because Serverpod cannot automatically determine which side should hold the foreign key field. You decide which side is most logical for your data. + +In a relationship where there is an object on both sides a unique index is always **required** on the foreign key field. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/02-one-to-many.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/02-one-to-many.md new file mode 100644 index 00000000..25b1a840 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/02-one-to-many.md @@ -0,0 +1,116 @@ +# One-to-many + +One-to-many (1:n) relationships describes a scenario where multiple records from one table can relate to a single record in another table. An example of this would the relationship between a company and its employees, where multiple employees can be employed at a single company. + +The Serverpod framework provides versatility in establishing these relations. Depending on the specific use case and clarity desired, you can define the model relationship either from the 'many' side (like `Employee`) or the 'one' side (like `Company`). + +## Defining the relationship + +In the following examples we show how to configure a 1:n relationship between `Company` and `Employee`. + +### Implicit definition + +With an implicit setup, Serverpod determines and establishes the relationship based on the table and class structures. + +```yaml +# company.yaml +class: Company +table: company +fields: + name: String + employees: List?, relation + +# employee.yaml +class: Employee +table: employee +fields: + name: String +``` + +In the example, we define a 1:n relation between `Company` and `Employee` by using the `List` type on the `employees` field together with the `relation` keyword. + +The corresponding foreign key field is automatically integrated into the 'many' side (e.g., `Employee`) as a concealed column. + +When fetching companies it now becomes possible to include any or all employees in the query. 1:n relations also enables additional [filtering](../filter#one-to-many) and [sorting](../sort#sort-on-relations) operations for [relational queries](../relation-queries). + +### Explicit definition + +In an explicit definition, you directly specify the relationship in a one-to-many relation. + +This can be done by through an [object relation](one-to-one#with-an-object): + +```yaml +# company.yaml +class: Company +table: company +fields: + name: String + +# employee.yaml +class: Employee +table: employee +fields: + name: String + company: Company?, relation +``` + +Or through a [foreign key field](one-to-one#with-an-id-field): + +```yaml +# company.yaml +class: Company +table: company +fields: + name: String + +# employee.yaml +class: Employee +table: employee +fields: + name: String + companyId: int, relation +``` + +The examples are 1:n relations because there is **no** unique index constraint on the foreign key field. This means that multiple employees can reference the same company. + +## Bidirectional relations + +For a more comprehensive representation, you can define the relationship from both sides. + +Either through an [object relation](one-to-one#with-an-object) on the many side: + +```yaml +# company.yaml +class: Company +table: company +fields: + name: String + employees: List?, relation(name=company_employees) + +# employee.yaml +class: Employee +table: employee +fields: + name: String + company: Company?, relation(name=company_employees) +``` + +Or through a [foreign key field](one-to-one#with-an-id-field) on the many side: + +```yaml +# company.yaml +class: Company +table: company +fields: + name: String + employees: List?, relation(name=company_employees) + +# employee.yaml +class: Employee +table: employee +fields: + name: String + companyId: int, relation(name=company_employees, parent=company) +``` + +Just as in the 1:1 examples, the `name` parameter with a unique string that links both sides together. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/03-many-to-many.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/03-many-to-many.md new file mode 100644 index 00000000..006fa5a1 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/03-many-to-many.md @@ -0,0 +1,58 @@ +# Many-to-many + +Many-to-many (n:m) relationships describes a scenario where multiple records from a table can relate to multiple records in another table. An example of this would be the relationship between students and courses, where a single student can enroll in multiple courses, and a single course can have multiple students. + +The Serverpod framework supports these complex relationships by explicitly creating a separate model, often called a junction or bridge table, that records the relation. + +## Overview + +In the context of many-to-many relationships, neither table contains a direct reference to the other. Instead, a separate table holds the foreign keys of both tables. This setup allows for a flexible and normalized approach to represent n:m relationships. + +Modeling the relationship between `Student` and `Course`, we would create an `Enrollment` model as a junction table to store the relationship explicitly. + +## Defining the relationship + +In the following examples we show how to configure a n:m relationship between `Student` and `Course`. + +### Many tables + +Both the `Course` and `Student` tables have a direct relationship with the `Enrollment` table but no direct relationship with each other. + +```yaml +# course.yaml +class: Course +table: course +fields: + name: String + enrollments: List?, relation(name=course_enrollments) +``` + +```yaml +# student.yaml +class: Student +table: student +fields: + name: String + enrollments: List?, relation(name=student_enrollments) +``` + +Note that the `name` argument is different, `course_enrollments` and `student_enrollments`, for the many tables. This is because each row in the junction table holds a relation to both many tables, `Course` and `Student`. + +### Junction table + +The `Enrollment` table acts as the bridge between `Course` and `Student`. It contains foreign keys from both tables, representing the many-to-many relationship. + +```yaml +# enrollment.yaml +class: Enrollment +table: enrollment +fields: + student: Student?, relation(name=student_enrollments) + course: Course?, relation(name=course_enrollments) +indexes: + enrollment_index_idx: + fields: studentId, courseId + unique: true +``` + +The unique index on the combination of `studentId` and `courseId` ensures that a student can only be enrolled in a particular course once. If omitted a student would be allowed to be enrolled in the same course multiple times. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/04-self-relations.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/04-self-relations.md new file mode 100644 index 00000000..e59c570a --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/04-self-relations.md @@ -0,0 +1,69 @@ +# Self-relations + +A self-referential or self-relation occurs when a table has a foreign key that references its own primary key within the same table. This creates a relationship between different rows within the same table. + +## One-to-one + +Imagine we have a blog and want to create links between our posts, where you can traverse forward and backward in the post history. Then we can create a self-referencing relation pointing to the next post in the chain. + +```yaml +class: Post +table: post +fields: + content: String + previous: Post?, relation(name=next_previous_post) + nextId: int? + next: Post?, relation(name=next_previous_post, field=nextId, onDelete=SetNull) +indexes: + next_unique_idx: + fields: nextId + unique: true +``` + +In this example, there is a named relation holding the data on both sides of the relation. The field `nextId` is a nullable field that stores the id of the next post. It is nullable as it would be impossible to create the first entry if we already needed to have a post created. The next post represents the object on "this" side while the previous post is the corresponding object on the "other" side. Meaning that the previous post is connected to the `nextId` of the post that came before it. + +## One-to-many + +In a one-to-many self-referenced relation there is one object field connected to a list field. In this example we have modeled the relationship between a cat and her potential kittens. Each cat has at most `one` mother but can have `n` kittens, for brevity, we have only modeled the mother. + +```yaml +class: Cat +table: cat +fields: + name: String + mother: Cat?, relation(name=cat_kittens, optional, onDelete=SetNull) + kittens: List?, relation(name=cat_kittens) +``` + +The field `motherId: int?` is injected into the dart class, the field is nullable since we marked the field `mother` as an `optional` relation. We can now find all the kittens by looking at the `motherId` of other cats which should match the `id` field of the current cat. The other cat can instead be found by looking at the `motherId` of the current cat and matching it against one other cat `id` field. + +## Many-to-many + +Let's imagine we have a system where we have members that can block other members. We would like to be able to query who I'm blocking and who is blocking me. This can be achieved by modeling the data as a many-to-many relation ship. + +Each member has a list of all other members they are blocking and another list of all members that are blocking them. But since the list side needs to point to a foreign key and cannot point to another list directly, we have to define a junction table that holds the connection between the rows. + +```yaml +class: Member +table: member +fields: + name: String + blocking: List?, relation(name=member_blocked_by_me) + blockedBy: List?, relation(name=member_blocking_me) +``` + +```yaml +class: Blocking +table: blocking +fields: + blocked: Member?, relation(name=member_blocking_me, onDelete=Cascade) + blockedBy: Member?, relation(name=member_blocked_by_me, onDelete=Cascade) +indexes: + blocking_blocked_unique_idx: + fields: blockedId, blockedById + unique: true +``` + +The junction table has an entry for who is blocking and another for who is getting blocked. Notice that the `blockedBy` field in the junction table is linked to the `blocking` field in the member table. We have also added a combined unique constraint on both the `blockedId` and `blockedById`, this makes sure we only ever have one entry per relation, meaning I can only block one other member one time. + +The cascade delete means that if a member is deleted all the blocking entries are also removed for that member. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/05-referential-actions.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/05-referential-actions.md new file mode 100644 index 00000000..a5c58de4 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/05-referential-actions.md @@ -0,0 +1,55 @@ +# Referential actions + +In Serverpod, the behavior of update and delete for relations can be precisely defined using the onUpdate and onDelete properties. These properties map directly to the corresponding referential actions in PostgreSQL. + +## Available referential actions + +| Action | Description | +| --- | --- | +| **NoAction** | If any constraint violation occurs, no action will be taken, and an error will be raised. | +| **Restrict** | If any referencing rows still exist when the constraint is checked, an error is raised. | +| **SetDefault** | The field will revert to its default value. Note: This action necessitates that a default value is configured for the field. | +| **Cascade** | Any action taken on the parent (update/delete) will be mirrored in the child. | +| **SetNull** | The field value is set to null. This action is permissible only if the field has been marked as optional. | + +## Syntax + +Use the following syntax to apply referential actions + +```yaml +relation(onUpdate=, onDelete=) +``` + +## Default values +If no referential actions are specified, the default behavior will be applied. + +If the relation is defined as an [object relation](one-to-one#with-an-object), the default behavior is `NoAction` for both onUpdate and onDelete. + +```yaml +parent: Model?, relation(onUpdate=NoAction, onDelete=NoAction) +``` + + +If the relation is defined as an [id relation](one-to-one#with-an-id-field), the default behavior is `NoAction` for onUpdate and `Cascade` for onDelete. + + +```yaml +parentId: int?, relation(parent=model_table, onUpdate=NoAction, onDelete=Cascade) +``` + +:::info + +The sequence of onUpdate and onDelete is interchangeable. + +::: + +### Full example + +```yaml +class: Example +table: example +fields: + parentId: int?, relation(parent=example, onUpdate=SetNull, onDelete=NoAction) +``` + +In the given example, if the `example` parent is updated, the `parentId` will be set to null. If the parent is deleted, no action will be taken for parentId. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/06-modules.md b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/06-modules.md new file mode 100644 index 00000000..387ce754 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/06-modules.md @@ -0,0 +1,61 @@ +# Relations with modules + +Serverpod [modules](../../modules) usually come with predefined tables and data structures. Sometimes it can be useful to extend them with your data structures by creating a relation to the module tables. Relations to modules come with some restrictions since you do not own the definition of the table, you cannot change the table structure of a module table. + +Since you do not directly control the models inside the modules it is recommended to create a so-called "bridge" table/model linking the module's model to your own. This can be done in the same way we normally would setup a one-to-one relation. + +```yaml +class: User +table: user +fields: + userInfo: module:auth:UserInfo?, relation + age: int +indexes: + user_info_id_unique_idx: + fields: userInfoId + unique: true +``` + +Or by referencing the table name if you only want to access the id. + +```yaml +class: User +table: user +fields: + userInfoId: int, relation(parent=serverpod_user_info) + age: int +indexes: + user_info_id_unique_idx: + fields: userInfoId + unique: true +``` + +It is now possible to make any other relation to our model as described in [one-to-one](./one-to-one), [one-to-many](./one-to-many), [many-to-many](./many-to-many) and [self-relations](./self-relations). + +## Advanced example + +A one-to-many relation with the "bridge" table could look like this. + +```yaml +class: User +table: user +fields: + userInfo: module:auth:UserInfo?, relation + age: int + company: Company?, relation(name=company_employee) +indexes: + user_info_id_unique_idx: + fields: userInfoId + unique: true + company_unique_idx: + fields: companyId + unique: true +``` + +```yaml +class: Company +table: company +fields: + name: String + employees: List?, relation(name=company_employee) +``` diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/_category_.json b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/_category_.json new file mode 100644 index 00000000..9b7fe3a6 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/03-relations/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Relations", + "collapsed": true + } + \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/04-indexing.md b/versioned_docs/version-2.3.0/06-concepts/06-database/04-indexing.md new file mode 100644 index 00000000..f28ae772 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/04-indexing.md @@ -0,0 +1,64 @@ +# Indexing + +For performance reasons, you may want to add indexes to your database tables. These are added in the YAML-files defining the serializable objects. + +### Add an index + +To add an index, add an `indexes` section to the YAML-file. The `indexes` section is a map where the key is the name of the index and the value is a map with the index details. + +```yaml +class: Company +table: company +fields: + name: String +indexes: + company_name_idx: + fields: name +``` + +The `fields` keyword holds a comma-separated list of column names. These are the fields upon which the index is created. Note that the index can contain several fields. + +```yaml +class: Company +table: company +fields: + name: String + foundedAt: DateTime +indexes: + company_idx: + fields: name, foundedAt +``` + +### Making fields unique + +Adding a unique index ensures that the value or combination of values stored in the fields are unique for the table. This can be useful for example if you want to make sure that no two companies have the same name. + +```yaml +class: Company +table: company +fields: + name: String +indexes: + company_name_idx: + fields: name + unique: true +``` + +The `unique` keyword is a bool that can toggle the index to be unique, the default is set to false. If the `unique` keyword is applied to a multi-column index, the index will be unique for the combination of the fields. + +### Specifying index type + +It is possible to add a type key to specify the index type. + +```yaml +class: Company +table: company +fields: + name: String +indexes: + company_name_idx: + fields: name + type: brin +``` + +If no type is specified the default is `btree`. All [PostgreSQL index types](https://www.postgresql.org/docs/current/indexes-types.html) are supported, `btree`, `hash`, `gist`, `spgist`, `gin`, `brin`. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/05-crud.md b/versioned_docs/version-2.3.0/06-concepts/06-database/05-crud.md new file mode 100644 index 00000000..fdaf852f --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/05-crud.md @@ -0,0 +1,191 @@ +# CRUD + +To interact with the database you need a [`Session`](../sessions) object as this object holds the connection to the database. All CRUD operations are accessible via the session object and the generated models. The methods can be found under the static `db` field in your generated models. + +For the following examples we will use this model: + +```yaml +class: Company +table: company +fields: + name: String +``` + +:::note + +You can also access the database methods through the session object under the field `db`. However, this is typically only recommended if you want to do custom queries where you explicitly type out your SQL queries. + +::: + +## Create + +There are two ways to create a new row in the database. + +### Inserting a single row + +Inserting a single row to the database is done by calling the `insertRow` method on your generated model. The method will return the entire company object with the `id` field set. + +```dart +var row = Company(name: 'Serverpod'); +var company = await Company.db.insertRow(session, row); +``` + +### Inserting several rows + +Inserting several rows in a batch operation is done by calling the `insert` method. This is an atomic operation, meaning no entries will be created if any entry fails to be created. + +```dart +var rows = [Company(name: 'Serverpod'), Company(name: 'Google')]; +var companies = await Company.db.insert(session, rows); +``` + +:::info +In previous versions of Serverpod the `insert` method mutated the input object by setting the `id` field. In the example above the input variable remains unmodified after the `insert`/`insertRow` call. +::: + +## Read + +There are three different read operations available. + +### Finding by id + +You can retrieve a single row by its `id`. + +```dart +var company = await Company.db.findById(session, companyId); +``` + +This operation either returns the model or `null`. + +### Finding a single row + +You can find a single row using an expression. + +```dart +var company = await Company.db.findFirstRow( + session, + where: (t) => t.name.equals('Serverpod'), +); +``` + +This operation returns the first model matching the filtering criteria or `null`. See [filter](filter) and [sort](sort) for all filter operations. + +:::info +If you include an `orderBy`, it will be evaluated before the list is reduced. In this case, `findFirstRow()` will return the first entry from the sorted list. +::: + +### Finding multiple rows + +To find multiple rows, use the same principle as for finding a single row. + +```dart +var companies = await Company.db.find( + session, + where: (t) => t.id < 100, + limit: 50, +); +``` + +This operation returns a `List` of your models matching the filtering criteria. + +See [filter](filter) and [sort](sort) for all filter and sorting operations and [pagination](pagination) for how to paginate the result. + +## Update + +There are two update operations available. + +### Update a single row + +To update a single row, use the `updateRow` method. + +```dart +var company = await Company.db.findById(session, companyId); // Fetched company has its id set +company.name = 'New name'; +var updatedCompany = await Company.db.updateRow(session, company); +``` + +The object that you update must have its `id` set to a non-`null` value and the id needs to exist on a row in the database. The `updateRow` method returns the updated object. + +### Update several rows + +To batch update several rows use the `update` method. + +```dart +var companies = await Company.db.find(session); +companies = companies.map((c) => c.copyWith(name: 'New name')).toList(); +var updatedCompanies = await Company.db.update(session, companies); +``` + +This is an atomic operation, meaning no entries will be updated if any entry fails to be updated. The `update` method returns a `List` of the updated objects. + +### Update a specific column + +It is possible to target one or several columns that you want to mutate, meaning any other column will be left unmodified even if the dart object has introduced a change. + +Update a single row, the following code will update the company name, but will not change the address column. + +```dart +var company = await Company.db.findById(session, companyId); +company.name = 'New name'; +company.address = 'Baker street'; +var updatedCompany = await Company.db.updateRow(session, company, columns: (t) => [t.name]); +``` + +The same syntax is available for multiple rows. + +```dart +var companies = await Company.db.find(session); +companies = companies.map((c) => c.copyWith(name: 'New name', address: 'Baker Street')).toList(); +var updatedCompanies = await Company.db.update(session, companies, columns: (t) => [t.name]); +``` + +## Delete + +Deleting rows from the database is done in a similar way to updating rows. However, there are three delete operations available. + +### Delete a single row + +To delete a single row, use the `deleteRow` method. + +```dart +var company = await Company.db.findById(session, companyId); // Fetched company has its id set +var companyDeleted = await Company.db.deleteRow(session, company); +``` + +The input object needs to have the `id` field set. The `deleteRow` method returns the deleted model. + +### Delete several rows + +To batch delete several rows, use the `delete` method. + +```dart +var companiesDeleted = await Company.db.delete(session, companies); +``` + +This is an atomic operation, meaning no entries will be deleted if any entry fails to be deleted. The `delete` method returns a `List` of the models deleted. + +### Delete by filter + +You can also do a [filtered](filter) delete and delete all entries matching a `where` query, by using the `deleteWhere` method. + +```dart +var companiesDeleted = await Company.db.deleteWhere( + session, + where: (t) => t.name.like('%Ltd'), +); +``` + +The above example will delete any row that ends in *Ltd*. The `deleteWhere` method returns a `List` of the models deleted. + +## Count + +Count is a special type of query that helps counting the number of rows in the database that matches a specific [filter](filter). + +```dart +var count = await Company.db.count( + session, + where: (t) => t.name.like('s%'), +); +``` + +The return value is an `int` for the number of rows matching the filter. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/06-filter.md b/versioned_docs/version-2.3.0/06-concepts/06-database/06-filter.md new file mode 100644 index 00000000..bdfd5899 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/06-filter.md @@ -0,0 +1,293 @@ +# Filter + +Serverpod makes it easy to build expressions that are statically type-checked. Columns and relational fields are referenced using table descriptor objects. The table descriptors, `t`, are accessible from each model and are passed as an argument to a model specific expression builder function. A callback is then used as argument to the `where` parameter when fetching data from the database. + +## Column operations + +The following column operations are supported in Serverpod, each column datatype supports a different set of operations that make sense for that type. + +:::info +When using the operators, it's a good practice to place them within a set of parentheses as the precedence rules are not always what would be expected. +::: + +### Equals + +Compare a column to an exact value, meaning only rows that match exactly will remain in the result. + +```dart +await User.db.find( + where: (t) => t.name.equals('Alice') +); +``` + +In the example we fetch all users with the name Alice. + +Not equals is the negated version of equals. + +```dart +await User.db.find( + where: (t) => t.name.notEquals('Bob') +); +``` + +In the example we fetch all users with a name that is not Bob. If a non-`null` value is used as an argument for the notEquals comparison, rows with a `null` value in the column will be included in the result. + +### Comparison operators + +Compare a column to a value, these operators are support for `int`, `double`, `Duration`, and `DateTime`. + +```dart +await User.db.find( + where: (t) => t.age > 25 +); +``` + +In the example we fetch all users that are older than 25 years old. + +```dart +await User.db.find( + where: (t) => t.age >= 25 +); +``` + +In the example we fetch users that are 25 years old or older. + +```dart +await User.db.find( + where: (t) => t.age < 25 +); +``` + +In the example we fetch all users that are younger than 25 years old. + +```dart +await User.db.find( + where: (t) => t.age <= 25 +); +``` + +In the example we fetch all users that are 25 years old or younger. + +### Between + +The between method takes two values and checks if the columns value is between the two input variables *inclusively*. + +```dart +await User.db.find( + where: (t) => t.age.between(18, 65) +); +``` + +In the example we fetch all users between 18 and 65 years old. This can also be expressed as `(t.age >= 18) & (t.age <= 65)`. + +The 'not between' operation functions similarly to 'between' but it negates the condition. It also works inclusively with the boundaries. + +```dart +await User.db.find( + where: (t) => t.age.notBetween(18, 65) +); +``` + +In the example we fetch all users that are not between 18 and 65 years old. This can also be expressed as `(t.age < 18) | (t.age > 65)`. + +### In set + +In set can be used to match with several values at once. This method functions the same as equals but for multiple values, `inSet` will make an exact comparison. + +```dart +await User.db.find( + where: (t) => t.name.inSet({'Alice', 'Bob'}) +); +``` + +In the example we fetch all users with a name matching either Alice or Bob. If an empty set is used as an argument for the inSet comparison, no rows will be included in the result. + +The 'not in set' operation functions similarly to `inSet`, but it negates the condition. + +```dart +await User.db.find( + where: (t) => t.name.notInSet({'Alice', 'Bob'}) +); +``` + +In the example we fetch all users with a name not matching Alice or Bob. Rows with a `null` value in the column will be included in the result. If an empty set is used as an argument for the notInSet comparison, all rows will be included in the result. + +### Like + +Like can be used to perform match searches against `String` entries in the database, this matcher is case-sensitive. This is useful when matching against partial entries. + +Two special characters enables matching against partial entries. + +- **`%`** Matching any sequence of character. +- **`_`** Matching any single character. + +| String | Matcher | Is matching | +|--|--|--| +| abc | a% | true | +| abc | _b% | true | +| abc | a_c | true | +| abc | b_ | false | + +We use like to match against a partial string. + +```dart +await User.db.find( + where: (t) => t.name.like('A%') +); +``` + +In the example we fetch all users with a name that starts with A. + +There is a negated version of like that can be used to exclude rows from the result. + +```dart +await User.db.find( + where: (t) => t.name.notLike('B%') +); +``` + +In the example we fetch all users with a name that does not start with B. + +### ilike + +`ilike` works the same as `like` but is case-insensitive. + +```dart +await User.db.find( + where: (t) => t.name.ilike('a%') +); +``` + +In the example we fetch all users with a name that starts with a or A. + +There is a negated version of `ilike` that can be used to exclude rows from the result. + +```dart +await User.db.find( + where: (t) => t.name.notIlike('b%') +); +``` + +In the example we fetch all users with a name that does not start with b or B. + +### Logical operators + +Logical operators are also supported when filtering, allowing you to chain multiple statements together to create more complex queries. + +The `&` operator is used to chain two statements together with an `and` operation. + +```dart +await User.db.find( + where: (t) => (t.name.equals('Alice') & (t.age > 25)) +); +``` + +In the example we fetch all users with the name "Alice" *and* are older than 25. + +The `|` operator is used to chain two statements together with an `or` operation. + +```dart +await User.db.find( + where: (t) => (t.name.like('A%') | t.name.like('B%')) +); +``` + +In the example we fetch all users that has a name that starts with A *or* B. + +## Relation operations + +If a relation between two models is defined a [one-to-one](relations/one-to-one) or [one-to-many](relations/one-to-many) object relation, then relation operations are supported in Serverpod. + +### One-to-one + +For 1:1 relations the columns of the relation can be accessed directly on the relation field. This enables filtering on related objects properties. + +```dart +await User.db.find( + where: (t) => t.address.street.like('%road%') +); +``` + +In the example each user has a relation to an address that has a street field. Using relation operations we then fetch all users where the related address has a street that contains the word "road". + +### One-to-many + +For 1:n relations, there are special filter methods where you can create sub-filters on all the related data. With them, you can answer questions on the aggregated result on many relations. + +#### Count + +Count can be used to count the number of related entries in a 1:n relation. The `count` always needs to be compared with a static value. + +```dart +await User.db.find( + where: (t) => t.orders.count() > 3 +); +``` + +In the example we fetch all users with more than three orders. + +We can apply a sub-filter to the `count` operator filter the related entries before they are counted. + +```dart +await User.db.find( + where: (t) => t.orders.count((o) => o.itemType.equals('book')) > 3 +); +``` + +In the example we fetch all users with more than three "book" orders. + +#### None + +None can be used to retrieve rows that have no related entries in a 1:n relation. Meaning if there exists a related entry then the row is omitted from the result. The operation is useful if you want to ensure that a many relation does not contain any related rows. + +```dart +await User.db.find( + where: (t) => t.orders.none() +); +``` + +In the example we fetch all users that have no orders. + +We can apply a sub-filter to the `none` operator to filter the related entries. Meaning if there is a match in the sub-filter the row will be omitted from the result. + +```dart +await User.db.find( + where:((t) => t.orders.none((o) => o.itemType.equals('book'))) +); +``` + +In the example we fetch all users that have no "book" orders. + +#### Any + +Any works similarly to the `any` method on lists in Dart. If there exists any related entry then include the row in the result. + +```dart +await User.db.find( + where: (t) => t.orders.any() +); +``` + +In the example we fetch all users that have any order. + +We can apply a sub-filter to the `any` operator to filter the related entries. Meaning if there is a match in the sub-filter the row will be included in the result. + +```dart +await User.db.find( + where:((t) => t.orders.any((o) => o.itemType.equals('book'))) +); +``` + +In the example we fetch all users that have any "book" order. + +#### Every + +Every works similarly to the `every` method on lists in Dart. If every related entry matches the sub-filter then include the row in the result. For the `every` operator the sub-filter is mandatory. + +```dart +await User.db.find( + where: (t) => t.orders.every((o) => o.itemType.equals('book')) +); +``` + +In the example we fetch all users that have only "book" orders. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/07-relation-queries.md b/versioned_docs/version-2.3.0/06-concepts/06-database/07-relation-queries.md new file mode 100644 index 00000000..6a669845 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/07-relation-queries.md @@ -0,0 +1,225 @@ +# Relation queries + +The Serverpod query framework supports filtering on, sorting on, and including relational data structures. In SQL this is often achieved using a join operation. The functionality is available if there exists any [one-to-one](relations/one-to-one) or [one-to-many](relations/one-to-many) object relations between two models. + +## Include relational data + +To include relational data in a query, use the `include` method. The `include` method has a typed interface and contains all the declared relations in your yaml file. + +```dart +var employee = await Employee.db.findById( + session, + employeeId, + include: Employee.include( + address: Address.include(), + ), +); +``` + +The example above return a employee including the related address object. + +### Nested includes + +It is also possible to include deeply nested objects. + +```dart +var employee = await Employee.db.findById( + session, + employeeId, + include: Employee.include( + company: Company.include( + address: Address.include(), + ), + ), +); +``` + +The example above returns an employee including the related company object that has the related address object included. + +Any relational object can be included or not when making a query but only the includes that are explicitly defined will be included in the result. + +```dart +var user = await Employee.db.findById( + session, + employeeId, + include: Employee.include( + address: Address.include(), + company: Company.include( + address: Address.include(), + ), + ), +); +``` + +The example above includes several different objects configured by specifying the named parameters. + +## Include relational lists + +Including a list of objects (1:n relation) can be done with the special `includeList` method. In the simplest case, the entire list is included. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList(), + ), +); +``` + +The example above returns a company with all related employees included. + +### Nested includes + +The `includeList` method works slightly differently from a normal `include` and to include nested objects the `includes` field must be used. When including something on a list it means that every entry in the list will each have access to the nested object. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList( + includes: Employee.include( + address: Address.include(), + ), + ), + ), +); +``` + +The example above returns a company with all related employees included. Each employee will have the related address object included. + +It is even possible to include lists within lists. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList( + includes: Employee.include( + tools: Tool.includeList(), + ), + ), + ), +); +``` + +The example above returns a company with all related employees included. Each employee will have the related tools list included. + +:::note +For each call to includeList (nested or not) the Serverpod Framework will perform one additional query to the database. +::: + +### Filter and sort + +When working with large datasets, it's often necessary to [filter](filter) and [sort](sort) the records to retrieve the most relevant data. Serverpod offers methods to refine the included list of related objects: + +#### Filter + +Use the `where` clause to filter the results based on certain conditions. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList( + where: (t) => t.name.ilike('a%') + ), + ), +); +``` + +The example above retrieves only employees whose names start with the letter 'a'. + +#### Sort + +The orderBy clause lets you sort the results based on a specific field. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList( + orderBy: (t) => t.name, + ), + ), +); +``` + +The example above sorts the employees by their names in ascending order. + +### Pagination + +[Paginate](pagination) results by specifying a limit on the number of records and an offset. + +```dart +var user = await Company.db.findById( + session, + employeeId, + include: Company.include( + employees: Employee.includeList( + limit: 10, + offset: 10, + ), + ), +); +``` + +The example above retrieves the next 10 employees starting from the 11th record: + +Using these methods in conjunction provides a powerful way to query, filter, and sort relational data efficiently. + +## Update + +Managing relationships between tables is a common task. Serverpod provides methods to link (attach) and unlink (detach) related records: + +### Attach single row + +Link an individual employee to a company. This operation associates an employee with a specific company: + +```dart +var company = await Company.db.findById(session, companyId); +var employee = await Employee.db.findById(session, employeeId); + +await Company.db.attachRow.employees(session, company!, employee!); +``` + +### Bulk attach rows + +For scenarios where you need to associate multiple employees with a company at once, use the bulk attach method. This operation is atomic, ensuring all or none of the records are linked: + +```dart +var company = await Company.db.findById(session, companyId); +var employee = await Employee.db.findById(session, employeeId); + +await Company.db.attach.employees(session, company!, [employee!]); +``` + +### Detach single row + +To remove the association between an employee and a company, use the detach row method: + +```dart +var employee = await Employee.db.findById(session, employeeId); + +await Company.db.detachRow.employees(session, employee!); +``` + +### Bulk detach rows + +In cases where you need to remove associations for multiple employees simultaneously, use the bulk detach method. This operation is atomic: + +```dart +var employee = await Employee.db.findById(session, employeeId); + +await Company.db.detach.employees(session, [employee!]); +``` + +:::note +When using the attach and detach methods the objects passed to them have to have the `id` field set. + +The detach method is also required to have the related nested object set if you make the call from the side that does not hold the foreign key. +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/08-sort.md b/versioned_docs/version-2.3.0/06-concepts/06-database/08-sort.md new file mode 100644 index 00000000..888f45e8 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/08-sort.md @@ -0,0 +1,75 @@ +# Sort + +It is often desirable to order the results of a database query. The 'find' method has an `orderBy` parameter where you can specify a column for sorting. The parameter takes a callback as an argument that passes a model-specific table descriptor, also accessible through the `t` field on the model. The table descriptor represents the database table associated with the model and includes fields for each corresponding column. The callback is then used to specify the column to sort by. + +```dart +var companies = await Company.db.find( + session, + orderBy: (t) => t.name, +); +``` + +In the example we fetch all companies and sort them by their name. + +By default the order is set to ascending, this can be changed to descending by setting the param `orderDecending: true`. + +```dart +var companies = await Company.db.find( + session, + orderBy: (t) => t.name, + orderDescending: true, +); +``` + +In the example we fetch all companies and sort them by their name in descending order. + +To order by several different columns use `orderByList`, note that this cannot be used in conjunction with `orderBy` and `orderDescending`. + +```dart +var companies = await Company.db.find( + session, + orderByList: (t) => [ + Order(column: t.name, orderDescending: true), + Order(column: t.id), + ], +); +``` + +In the example we fetch all companies and sort them by their name in descending order, and then by their id in ascending order. + +## Sort on relations + +To sort based on a field from a related model, use the chained field reference. + +```dart +var companies = await Company.db.find( + session, + orderBy: (t) => t.ceo.name, +); +``` + +In the example we fetch all companies and sort them by their CEO's name. + +You can order results based on the count of a list relation (1:n). + +```dart +var companies = await Company.db.find( + session, + orderBy: (t) => t.employees.count(), +); +``` + +In the example we fetch all companies and sort them by the number of employees. + +The count used for sorting can also be filtered using a sub-filter. + +```dart +var companies = await Company.db.find( + session, + orderBy: (t) => t.employees.count( + (employee) => employee.role.equals('developer'), + ), +); +``` + +In the example we fetch all companies and sort them by the number of employees with the role of "developer". diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/08-transactions.md b/versioned_docs/version-2.3.0/06-concepts/06-database/08-transactions.md new file mode 100644 index 00000000..9bee9a60 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/08-transactions.md @@ -0,0 +1,116 @@ +# Transactions + +The essential point of a database transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all. + +Serverpod handles database transactions through the `session.db.transaction` method. The method takes a callback function that receives a transaction object. + +The transaction is committed when the callback function returns, and rolled back if an exception is thrown. Any return value of the callback function is returned by the `transaction` method. + +Simply pass the transaction object to each database operation method to include them in the same atomic operation: + +```dart +var result = await session.db.transaction((transaction) async { + // Do some database queries here. + await Company.db.insertRow(session, company, transaction: transaction); + await Employee.db.insertRow(session, employee, transaction: transaction); + + // Optionally return a value. + return true; +}); +``` + +In the example we insert a company and an employee in the same transaction. If any of the operations fail, the entire transaction will be rolled back and no changes will be made to the database. If the transaction is successful, the return value will be `true`. + +## Transaction isolation + +The transaction isolation level can be configured when initiating a transaction. The isolation level determines how the transaction interacts with concurrent database operations. If no isolation level is supplied, the level is determined by the database engine. + +:::info + +At the time of writing, the default isolation level for the PostgreSQL database engine is `IsolationLevel.readCommitted`. + +::: + +To set the isolation level, configure the `isolationLevel` property of the `TransactionSettings` object: + +```dart +await session.db.transaction( + (transaction) async { + await Company.db.insertRow(session, company, transaction: transaction); + await Employee.db.insertRow(session, employee, transaction: transaction); + }, + settings: TransactionSettings(isolationLevel: IsolationLevel.serializable), +); +``` + +In the example the isolation level is set to `IsolationLevel.serializable`. + +The available isolation levels are: + +| Isolation Level | Constant | Description | +|-----------------|-----------------------|-------------| +| Read uncommitted | `IsolationLevel.readUncommitted` | Exhibits the same behavior as `IsolationLevel.readCommitted` in PostgresSQL | +| Read committed | `IsolationLevel.readCommitted` | Each statement in the transaction sees a snapshot of the database as of the beginning of that statement. | +| Repeatable read | `IsolationLevel.repeatableRead` | The transaction only observes rows committed before the first statement in the transaction was executed giving a consistent view of the database. If any conflicting writes among concurrent transactions occur, an exception is thrown. | +| Serializable | `IsolationLevel.serializable` | Gives the same guarantees as `IsolationLevel.repeatableRead` but also throws if read rows are updated by other transactions. | + +For a detailed explanation of the different isolation levels, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/transaction-iso.html). + +## Savepoints + +A savepoint is a special mark inside a transaction that allows all commands that are executed after it was established to be rolled back, restoring the transaction state to what it was at the time of the savepoint. + +Read more about savepoints in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-savepoint.html). + +### Creating savepoints +To create a savepoint, call the `createSavepoint` method on the transaction object: + +```dart +await session.db.transaction((transaction) async { + await Company.db.insertRow(session, company, transaction: transaction); + // Create savepoint + var savepoint = await transaction.createSavepoint(); + await Employee.db.insertRow(session, employee, transaction: transaction); +}); +``` + +In the example, we create a savepoint after inserting a company but before inserting the employee. This gives us the option to roll back to the savepoint and preserve the company insertion. + +#### Rolling back to savepoints + +Once a savepoint is created, you can roll back to it by calling the `rollback` method on the savepoint object: + +```dart +await session.db.transaction((transaction) async { + // Changes preserved in the database + await Company.db.insertRow(session, company, transaction: transaction); + + // Create savepoint + var savepoint = await transaction.createSavepoint(); + + await Employee.db.insertRow(session, employee, transaction: transaction); + // Changes rolled back + await savepoint.rollback(); +}); +``` + +In the example, we create a savepoint after inserting a company. We then insert an employee but invoke a rollback to our savepoint. This results in the database preserving the company but not the employee insertion. + +#### Releasing savepoints + +Savepoints can also be released, which means that the changes made after the savepoint are preserved in the transaction. Releasing a savepoint will also render any subsequent savepoints invalid. + +To release a savepoint, call the `release` method on the savepoint object: + +```dart +await session.db.transaction((transaction) async { + // Create two savepoints + var savepoint = await transaction.createSavepoint(); + var secondSavepoint = await transaction.createSavepoint(); + + await Company.db.insertRow(session, company, transaction: transaction); + await savepoint.release(); +}); +``` + +In the example, two savepoints are created. After the company is inserted the first savepoint is released, which renders the second savepoint invalid. If the second savepoint is used to rollback, an exception will be thrown. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/09-pagination.md b/versioned_docs/version-2.3.0/06-concepts/06-database/09-pagination.md new file mode 100644 index 00000000..fecd3965 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/09-pagination.md @@ -0,0 +1,110 @@ +# Pagination + +Serverpod provides built-in support for pagination to help manage large datasets, allowing you to retrieve data in smaller chunks. Pagination is achieved using the `limit` and `offset` parameters. + +## Limit + +The `limit` parameter specifies the maximum number of records to return from the query. This is equivalent to the number of rows on a page. + +```dart +var companies = await Company.db.find( + session, + limit: 10, +); +``` + +In the example we fetch the first 10 companies. + +## Offset + +The `offset` parameter determines the starting point from which to retrieve records. It essentially skips the first `n` records. + +```dart +var companies = await Company.db.find( + session, + limit: 10, + offset: 30, +); +``` + +In the example we skip the first 30 rows and fetch the 31st to 40th company. + +## Using limit and offset for pagination + +Together, `limit` and `offset` can be used to implement pagination. + +```dart +int page = 3; +int companiesPerPage = 10; + +var companies = await Company.db.find( + session, + orderBy: (t) => t.id, + limit: companiesPerPage, + offset: (page - 1) * companiesPerPage, +); +``` + +In the example we fetch the third page of companies, with 10 companies per page. + +### Tips + +1. **Performance**: Be aware that while `offset` can help in pagination, it may not be the most efficient way for very large datasets. Using an indexed column to filter results can sometimes be more performant. +2. **Consistency**: Due to possible data changes between paginated requests (like additions or deletions), the order of results might vary. It's recommended to use an `orderBy` parameter to ensure consistency across paginated results. +3. **Page numbering**: Page numbers usually start from 1. Adjust the offset calculation accordingly. + +## Cursor-based pagination + +A limit-offset pagination may not be the best solution if the table is changed frequently and rows are added or removed between requests. + +Cursor-based pagination is an alternative method to the traditional limit-offset pagination. Instead of using an arbitrary offset to skip records, cursor-based pagination uses a unique record identifier (a _cursor_) to mark the starting or ending point of a dataset. This approach is particularly beneficial for large datasets as it offers consistent and efficient paginated results, even if the data is being updated frequently. + +### How it works + +In cursor-based pagination, the client provides a cursor as a reference point, and the server returns data relative to that cursor. This cursor is usually an `id`. + +### Implementing cursor-based pagination + +1. **Initial request**: + For the initial request, where no cursor is provided, retrieve the first `n` records: + + ```dart + int recordsPerPage = 10; + + var companies = await Company.db.find( + session, + orderBy: (t) => t.id, + limit: recordsPerPage, + ); + ``` + +2. **Subsequent requests**: + For the subsequent requests, use the cursor (for example, the last `id` from the previous result) to fetch the next set of records: + + ```dart + int cursor = lastCompanyIdFromPreviousPage; // This is typically sent by the client + + var companies = await Company.db.find( + session, + where: Company.t.id > cursor, + orderBy: (t) => t.id, + limit: recordsPerPage, + ); + ``` + +3. **Returning the cursor**: + When returning data to the client, also return the cursor, so it can be used to compute the starting point for the next page. + + ```dart + return { + 'data': companies, + 'lastCursor': companies.last.id, + }; + ``` + +### Tips + +1. **Choosing a cursor**: While IDs are commonly used as cursors, timestamps or other unique, sequentially ordered fields can also serve as effective cursors. +2. **Backward pagination**: To implement backward pagination, use the first item from the current page as the cursor and adjust the query accordingly. +3. **Combining with sorting**: Ensure the field used as a cursor aligns with the sorting order. For instance, if you're sorting data by a timestamp in descending order, the cursor should also be based on the timestamp. +4. **End of data**: If the returned data contains fewer items than the requested limit, it indicates that you've reached the end of the dataset. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/10-raw-access.md b/versioned_docs/version-2.3.0/06-concepts/06-database/10-raw-access.md new file mode 100644 index 00000000..1bf16d39 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/10-raw-access.md @@ -0,0 +1,81 @@ +# Raw access + +The library provides methods to execute raw SQL queries directly on the database for advanced scenarios. + +## `unsafeQuery` + +Executes a single SQL query and returns a `DatabaseResult` containing the results. This method uses the extended query protocol, allowing for parameter binding to prevent SQL injection. + +```dart +DatabaseResult result = await session.db.unsafeQuery( + r'SELECT * FROM mytable WHERE id = @id', + parameters: QueryParameters.named({'id': 1}), +); +``` + +## `unsafeExecute` + +Executes a single SQL query without returning any results. Use this for statements that modify data, such as `INSERT`, `UPDATE`, or `DELETE`. Returns the number of rows affected. + +```dart +int result = await session.db.unsafeExecute( + r'DELETE FROM mytable WHERE id = @id', + parameters: QueryParameters.named({'id': 1}), +); +``` + +## `unsafeSimpleQuery` + +Similar to `unsafeQuery`, but uses the simple query protocol. This protocol does not support parameter binding, making it more susceptible to SQL injection. **Use with extreme caution and only when absolutely necessary.** + +Simple query mode is suitable for: + +* Queries containing multiple statements. +* Situations where the extended query protocol is not available (e.g., replication mode or with proxies like PGBouncer). + + +```dart + DatabaseResult result = await session.db.unsafeSimpleQuery( + r'SELECT * FROM mytable WHERE id = 1; SELECT * FROM othertable;' + ); +``` + +## `unsafeSimpleExecute` + +Similar to `unsafeExecute`, but uses the simple query protocol. It does not return any results. **Use with extreme caution and only when absolutely necessary.** + +Simple query mode is suitable for the same scenarios as `unsafeSimpleQuery`. + +```dart + int result = await session.db.unsafeSimpleExecute( + r'DELETE FROM mytable WHERE id = 1; DELETE FROM othertable;' + ); +``` + +## Query parameters + +To protect against SQL injection attacks, always use query parameters when passing values into raw SQL queries. The library provides two types of query parameters: + +* **Named parameters:** Use `@` to denote named parameters in your query and pass a `Map` of parameter names and values. +* **Positional parameters:** Use `$1`, `$2`, etc., to denote positional parameters and pass a `List` of parameter values in the correct order. + +```dart +// Named parameters +var result = await db.unsafeQuery( + r'SELECT id FROM apparel WHERE color = @color AND size = @size', + QueryParameters.named({ + 'color': 'green', + 'size': 'XL', + })); + +// Positional parameters +var result = await db.unsafeQuery( + r'SELECT id FROM apparel WHERE color = $1 AND size = $2', + QueryParameters.positional(['green', 'XL']), +); +``` + +:::danger +Always sanitize your input when using raw query methods. For the `unsafeQuery` and `unsafeExecute` methods, use query parameters to prevent SQL injection. Avoid using `unsafeSimpleQuery` and `unsafeSimpleExecute` unless the simple query protocol is strictly required. +::: + diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/11-migrations.md b/versioned_docs/version-2.3.0/06-concepts/06-database/11-migrations.md new file mode 100644 index 00000000..9d7dbef0 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/11-migrations.md @@ -0,0 +1,212 @@ +# Migrations + +Serverpod comes bundled with a simple-to-use but powerful migration system that helps you keep your database schema up to date as your project evolves. Database migrations provide a structured way of upgrading your database while maintaining existing data. + +A migration is a set of database operations (e.g. creating a table, adding a column, etc.) required to update the database schema to match the requirements of the project. Each migration handles both initializing a new database and rolling an existing one forward from a previous state. + +If you ever get out of sync with the migration system, repair migrations can be used to bring the database schema up to date with the migration system. Repair migrations identify the differences between the two and create a unique migration that brings the live database schema in sync with a migration database schema. + +## Opt out of migrations + +It is possible to selectively opt out of the migration system per table basis, by setting the `managedMigration` key to false in your model. When this flag is set to false the generated migrations will not define any SQL code for this table. You will instead have to manually define and manage the life cycle of this table. + +```yaml +class: Example +table: example +managedMigration: false +fields: + name: String +``` + +If you want to transition a manually managed table to then be managed by Serverpod you first need to set this flag to `true`. Then you have two options: + +- Delete the old table you had created by yourself, and this will let Serverpod manage the schema from a clean state. However, this means that you would lose any data that was stored in the table. +- Make sure that the table schema matches the expected schema you have configured. This can be done by either manually making sure the schema aligns, or by creating a [repair migration](#creating-a-repair-migration) to get back into the correct state. + +## Creating a migration + +To create a migration navigate to your project's `server` package directory and run the `create-migration` command. + +```bash +$ serverpod create-migration +``` + +The command reads the database schema from the last migration, then compares it to the database schema necessary to accommodate the projects, and any module dependencies, current database requirements. If differences are identified, a new migration is created in the `migrations` directory to roll the database forward. + +If no previous migration exists it will create a migration assuming there is no initial state. + +See the [Pre-migration project upgrade path](../../upgrading/upgrade-to-one-point-two) section for more information on how to get started with migrations for any project created before migrations were introduced in Serverpod. + +### Force create migration + +The migration command aborts and displays an error under two conditions: + +1. When no changes are identified between the database schema in the latest migration and the schema required by the project. +2. When there is a risk of data loss. + +To override these safeguards and force the creation of a migration, use the `--force` flag. + +```bash +$ serverpod create-migration --force +``` + +### Tag migration + +Tags can be useful to identify migrations that introduced specific changes to the project. Tags are appended to the migration name and can be added with the `--tag` option. + +```bash +$ serverpod create-migration --tag "v1-0-0" +``` + +This would create a migration named `-v1-0-0`: + +```text +├── migrations +│ └── 20231205080937028-v1-0-0 +``` + +### Add data in a migration + +Since the migrations are written in SQL, it is possible to add data to the database in a migration. This can be useful if you want to add initial data to the database. + +The developer is responsible for ensuring that any added SQL statements are compatible with the database schema and rolling forward from the previous migrations. + +### Migrations directory structure + +The `migrations` directory contains a folder for each migration that is created, looking like this for a project with two migrations: + +```text +├── migrations +│ ├── 20231205080937028 +│ ├── 20231205081959122 +│ └── migration_registry.txt +``` + +Every migration is denoted by a directory labeled with a timestamp indicating when the migration was generated. Within the directory, there is a `migration_registry.txt` file. This file is automatically created whenever migrations are generated and serves the purpose of cataloging the migrations. Its primary function is to identify migration conflicts. + +For each migration, five files are created: + +- **definition.json** - Contains the complete definition of the database schema, including any database schema changes from Serverpod module dependencies. This file is parsed by the Serverpod CLI to determine the target database schema for the migration. +- **definition.sql** - Contains SQL statements to create the complete database schema. This file is applied when initializing a new database. +- **definition_project.json** - Contains the definition of the database schema for only your project. This file is parsed by the Serverpod CLI to determine what tables are different by Serverpod modules. +- **migration.json** - Contains the actions that are part of the migration. This file is parsed by the Serverpod CLI. +- **migration.sql** - Contains SQL statements to update the database schema from the last migration to the current version. This file is applied when rolling the database forward. + +## Apply migrations + +Migrations are applied using the server runtime. To apply migrations, navigate to your project's `server` package directory, then start the server with the `--apply-migrations` flag. Migrations are applied as part of the startup sequence and the framework asserts that each migration is only applied once to the database. + +```bash +$ dart run bin/main.dart --apply-migrations +``` + +Migrations can also be applied using the maintenance role. In maintenance, after migrations are applied, the server exits with an exit code indicating if migrations were successfully applied, zero for success or non-zero for failure. + +```bash +$ dart run bin/main.dart --role maintenance --apply-migrations +``` + +This is useful if migrations are applied as part of an automated process. + +If migrations are applied at the same time as repair migration, the repair migration is applied first. + +## Creating a repair migration + +If the database has been manually modified the database schema may be out of sync with the migration system. In this case, a repair migration can be created to bring the database schema up to date with the migration system. + +By default, the command connects to and pulls a live database schema from a running development server. + +To create a repair migration, navigate to your project's `server` package directory and run the `create-repair-migration` command. + +```bash +$ serverpod create-repair-migration +``` + +This creates a repair migration in the `repair-migration` directory targeting the project's latest migration. + +A repair migration is represented by a single SQL file that contains the SQL statements necessary to bring the database schema up to date with the migration system. + +:::warning +To restore the integrity of the database schema, repair migrations will attempt to remove any tables that are not part of the migration system. To preserve manually created or managed tables the [repair migration](#repair-migrations-directory-structure) needs to be modified accordingly before application. +::: + +Since each repair migration is created for a specific live database schema, Serverpod will overwrite the latest repair migration each time a new repair migration is created. + +### Migration database source + +By default, the repair migration system connects to your `development` database using the information specified in your Serverpod config. To use a different database source, the `--mode` option is used. + +```bash +$ serverpod create-repair-migration --mode production +``` + +The command connects and pulls the live database schema from a running server. + +### Targeting a specific migration + +Repair migrations can also target a specific migration version by specifying the migration name with the `--version` option. + +```bash +$ serverpod create-repair-migration --version 20230821135718-v1-0-0 +``` + +This makes it possible to revert your database schema back to any older migration version. + +### Force create repair migration + +The repair migration command aborts and displays an error under two conditions: + +1. When no changes are identified between the database schema in the latest migration and the schema required by the project. +2. When there is a risk of data loss. + +To override these safeguards and force the creation of a repair migration, use the `--force` flag. + +```bash +$ serverpod create-repair-migration --force +``` + +### Tag repair migration + +Repair migrations can be tagged just like regular migrations. Tags are appended to the migration name and can be added with the `--tag` option. + +```bash +$ serverpod create-repair-migration --tag "reset-migrations" +``` + +This would create a repair migration named `-reset-migrations` in the `repair` directory: + +```text +├── repair +│ └── 20230821135718-v1-0-0.sql +``` + +### Repair migrations directory structure + +The `repair` directory only exists if a repair migration has been created and contains a single SQL file containing statements to repair the database schema. + +```text +├── repair +│ └── 20230821135718-v1-0-0.sql +``` + +## Applying a repair migration + +The repair migration is applied using the server runtime. To apply a repair migration, start the server with the `--apply-repair-migration` flag. The repair migration is applied as part of the startup sequence and the framework asserts that each repair migration is only applied once to the database. + +```bash +$ dart run bin/main.dart --apply-repair-migration +``` + +The repair migration can also be applied using the maintenance role. In maintenance, after migrations are applied, the server exits with an exit code indicating if migrations were successfully applied, zero for success or non-zero for failure. + +```bash +$ dart run bin/main.dart --role maintenance --apply-repair-migration +``` + +If a repair migration is applied at the same time as migrations, the repair migration is applied first. + +## Rolling back migrations + +Utilizing repair migrations it is easy to roll back the project to any migration. This is useful if you want to revert the database schema to a previous state. To roll back to a previous migration, create a repair migration targeting the migration you want to roll back to, then apply the repair migration. + +Note that data is not rolled back, only the database schema. diff --git a/versioned_docs/version-2.3.0/06-concepts/06-database/_category_.json b/versioned_docs/version-2.3.0/06-concepts/06-database/_category_.json new file mode 100644 index 00000000..aad78b51 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/06-database/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Database", + "collapsed": true + } + \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/07-configuration.md b/versioned_docs/version-2.3.0/06-concepts/07-configuration.md new file mode 100644 index 00000000..9009e85c --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/07-configuration.md @@ -0,0 +1,148 @@ +# Configurations + +Serverpod can be configured in a few different ways. The minimum required settings to provide is the configuration for the API server. If no settings are provided at all, the default settings for the API server are used. + +## Configuration options + +There are three different ways to configure Serverpod: with environment variables, via yaml config files, or by supplying the dart configuration object to the Serverpod constructor. The environment variables take precedence over the yaml configurations but both can be used simultaneously. The dart configuration object will override any environment variable or config file. The tables show all available configuration options provided in the Serverpod core library. + +| Environment variable | Config file | Default | Description | +|----------------------------------------------|--------------------------------------|-----------|--------------------------------------------------| +| SERVERPOD_API_SERVER_PORT | apiServer.port | 8080 | The port number for the API server | +| SERVERPOD_API_SERVER_PUBLIC_HOST | apiServer.publicHost | localhost | The public host address of the API server | +| SERVERPOD_API_SERVER_PUBLIC_PORT | apiServer.publicPort | 8080 | The public port number for the API server | +| SERVERPOD_API_SERVER_PUBLIC_SCHEME | apiServer.publicScheme | http | The public scheme (http/https) for the API server| +| SERVERPOD_INSIGHTS_SERVER_PORT | insightsServer.port | - | The port number for the Insights server | +| SERVERPOD_INSIGHTS_SERVER_PUBLIC_HOST | insightsServer.publicHost | - | The public host address of the Insights server | +| SERVERPOD_INSIGHTS_SERVER_PUBLIC_PORT | insightsServer.publicPort | - | The public port number for the Insights server | +| SERVERPOD_INSIGHTS_SERVER_PUBLIC_SCHEME | insightsServer.publicScheme | - | The public scheme (http/https) for the Insights server | +| SERVERPOD_WEB_SERVER_PORT | webServer.port | - | The port number for the Web server | +| SERVERPOD_WEB_SERVER_PUBLIC_HOST | webServer.publicHost | - | The public host address of the Web server | +| SERVERPOD_WEB_SERVER_PUBLIC_PORT | webServer.publicPort | - | The public port number for the Web server | +| SERVERPOD_WEB_SERVER_PUBLIC_SCHEME | webServer.publicScheme | - | The public scheme (http/https) for the Web server| +| SERVERPOD_DATABASE_HOST | database.host | - | The host address of the database | +| SERVERPOD_DATABASE_PORT | database.port | - | The port number for the database connection | +| SERVERPOD_DATABASE_NAME | database.name | - | The name of the database | +| SERVERPOD_DATABASE_USER | database.user | - | The user name for database authentication | +| SERVERPOD_DATABASE_REQUIRE_SSL | database.requireSsl | false | Indicates if SSL is required for the database | +| SERVERPOD_DATABASE_IS_UNIX_SOCKET | database.isUnixSocket | false | Specifies if the database connection is a Unix socket | +| SERVERPOD_REDIS_HOST | redis.host | - | The host address of the Redis server | +| SERVERPOD_REDIS_PORT | redis.port | - | The port number for the Redis server | +| SERVERPOD_REDIS_USER | redis.user | - | The user name for Redis authentication | +| SERVERPOD_REDIS_ENABLED | redis.enabled | false | Indicates if Redis is enabled | +| SERVERPOD_MAX_REQUEST_SIZE | maxRequestSize | 524288 | The maximum size of requests allowed in bytes | +| SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED | sessionLogs.persistentEnabled | - | Enables or disables logging session data to the database. Defaults to `true` if a database is configured, otherwise `false`. | +| SERVERPOD_SESSION_CONSOLE_LOG_ENABLED | sessionLogs.consoleEnabled | - | Enables or disables logging session data to the console. Defaults to `true` if no database is configured, otherwise `false`. | + +| Environment variable | Passwords file | Default | Description | +|------------------------------------|-----------------|---------|-------------------------------------------------------------------| +| SERVERPOD_DATABASE_PASSWORD | database | - | The password for the database | +| SERVERPOD_SERVICE_SECRET | serviceSecret | - | The token used to connect with insights must be at least 20 chars | +| SERVERPOD_REDIS_PASSWORD | redis | - | The password for the Redis server | + +### Config file example + +The config file should be named after the run mode you start the server in and it needs to be placed inside the `config` directory in the root of the server project. As an example, you have the `config/development.yaml` that will be used when running in the `development` run mode. + +```yaml +apiServer: + port: 8080 + publicHost: localhost + publicPort: 8080 + publicScheme: http + +insightsServer: + port: 8081 + publicHost: localhost + publicPort: 8081 + publicScheme: http + +webServer: + port: 8082 + publicHost: localhost + publicPort: 8082 + publicScheme: http + +database: + host: localhost + port: 8090 + name: database_name + user: postgres + +redis: + enabled: false + host: localhost + port: 8091 + +maxRequestSize: 524288 + +sessionLogs: + persistentEnabled: true + consoleEnabled: true +``` + +### Passwords file example + +The password file contains the secrets used by the server to connect to different services but you can also supply your secrets if you want. This file is structured with a common `shared` section, any secret put here will be used in all run modes. The other sections are the names of the run modes followed by respective key/value pairs. + +```yaml +shared: + myCustomSharedSecret: 'secret_key' + +development: + database: 'development_password' + redis: 'development_password' + serviceSecret: 'development_service_secret' + +production: + database: 'production_password' + redis: 'production_password' + serviceSecret: 'production_service_secret' +``` + +### Dart config object example + +To configure Serverpod in Dart you simply pass an instance of the `ServerpodConfig` class to the `Serverpod` constructor. This config will override any environment variables or config files present. The `Serverpod` constructor is normally used inside the `run` function in your `server.dart` file. At a minimum, the `apiServer` has to be provided. + +```dart +Serverpod( + args, + Protocol(), + Endpoints(), + config: ServerpodConfig( + apiServer: ServerConfig( + port: 8080, + publicHost: 'localhost', + publicPort: 8080, + publicScheme: 'http', + ), + insightsServer: ServerConfig( + port: 8081, + publicHost: 'localhost', + publicPort: 8081, + publicScheme: 'http', + ), + webServer: ServerConfig( + port: 8082, + publicHost: 'localhost', + publicPort: 8082, + publicScheme: 'http', + ), + ), +); +``` + +### Default + +If no yaml config files exist, no environment variables are configured and no dart config file is supplied this default configuration will be used. + +```dart +ServerpodConfig( + apiServer: ServerConfig( + port: 8080, + publicHost: 'localhost', + publicPort: 8080, + publicScheme: 'http', + ), +); +``` diff --git a/versioned_docs/version-2.3.0/06-concepts/08-caching.md b/versioned_docs/version-2.3.0/06-concepts/08-caching.md new file mode 100644 index 00000000..2fe38e6d --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/08-caching.md @@ -0,0 +1,58 @@ +# Caching + +Accessing the database can be expensive for complex queries or if you need to run many different queries for a specific task. Serverpod makes it easy to cache frequently requested objects in the memory of your server. Any serializable object can be cached. Objects can be stored in the Redis cache if your Serverpod is hosted across multiple servers in a cluster. + +Caches can be accessed through the `Session` object. This is an example of an endpoint method for requesting data about a user: + +```dart +Future getUserData(Session session, int userId) async { + // Define a unique key for the UserData object + var cacheKey = 'UserData-$userId'; + + // Try to retrieve the object from the cache + var userData = await session.caches.local.get(cacheKey); + + // If the object wasn't found in the cache, load it from the database and + // save it in the cache. Make it valid for 5 minutes. + if (userData == null) { + userData = UserData.db.findById(session, userId); + await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5)); + } + + // Return the user data to the client + return userData; +} +``` + +In total, there are three caches where you can store your objects. Two caches are local to the server handling the current session, and one is distributed across the server cluster through Redis. There are two variants for the local cache, one regular cache, and a priority cache. Place objects that are frequently accessed in the priority cache. + +Depending on the type and number of objects that are cached in the global cache, you may want to specify custom caching rules in Redis. This is currently not handled automatically by Serverpod. + +### Cache miss handler + +If you want to handle cache misses in a specific way, you can pass in a `CacheMissHandler` to the `get` method. The `CacheMissHandler` makes it possible to store an object in the cache when a cache miss occurs. + +The above example rewritten using the `CacheMissHandler`: + +```dart +Future getUserData(Session session, int userId) async { + // Define a unique key for the UserData object + var cacheKey = 'UserData-$userId'; + + // Try to retrieve the object from the cache + var userData = await session.caches.local.get( + cacheKey, + // If the object wasn't found in the cache, load it from the database and + // save it in the cache. Make it valid for 5 minutes. + CacheMissHandler( + () async => UserData.db.findById(session, userId), + lifetime: Duration(minutes: 5), + ), + ); + + // Return the user data to the client + return userData; +} +``` + +If the `CacheMissHandler` returns `null`, no object will be stored in the cache. diff --git a/versioned_docs/version-2.3.0/06-concepts/09-logging.md b/versioned_docs/version-2.3.0/06-concepts/09-logging.md new file mode 100644 index 00000000..b86f7561 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/09-logging.md @@ -0,0 +1,63 @@ +# Logging + +Serverpod uses the database for storing logs; this makes it easy to search for errors, slow queries, or debug messages. To log custom messages during the execution of a session, use the `log` method of the `session` object. When the session is closed, either from successful execution or by failing from throwing an exception, the messages are written to the log. By default, session log entries are written for every completed session. + +```dart +session.log('This is working well'); +``` + +You can also pass exceptions and stack traces to the `log` method or set the logging level. + +```dart +session.log( + 'Oops, something went wrong', + level: LogLevel.warning, + exception: e, + stackTrace: stackTrace, +); +``` + +Log entries are stored in the following tables of the database: `serverpod_log` for text messages, `serverpod_query_log` for queries, and `serverpod_session_log` for completed sessions. Optionally, it's possible to pass a log level with the message to filter out messages depending on the server's runtime settings. + +### Controlling Session Logs with Environment Variables or Configuration Files + +You can control whether session logs are written to the database, the console, both, or neither, using environment variables or configuration files. **Environment variables take priority** over configuration file settings if both are provided. + +#### Environment Variables + +- `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED`: Controls whether session logs are written to the database. +- `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED`: Controls whether session logs are output to the console. + +#### Configuration File Example + +You can also configure logging behavior directly in the configuration file: + +```yaml +sessionLogs: + persistentEnabled: true # Logs are stored in the database + consoleEnabled: true # Logs are output to the console +``` + +### Default Behavior for Session Logs + +By default, session logging behavior depends on whether the project has database support: + +- **When a database is present** + + - `persistentEnabled` is set to `true`, meaning logs are stored in the database. + - `consoleEnabled` is set to `false` by default, meaning logs are not printed to the console unless explicitly enabled. + +- **When no database is present** + + - `persistentEnabled` is set to `false` since persistent logging requires a database. + - `consoleEnabled` is set to `true`, meaning logs are printed to the console by default. + +### Important: Persistent Logging Requires a Database + +If `persistentEnabled` is set to `true` but **no database is configured**, a `StateError` will be thrown. Persistent logging requires database support, and Serverpod ensures that misconfigurations are caught early by raising this error. + +:::info + +The Serverpod GUI is coming soon, making it easy to read, search, and configure the logs. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/10-modules.md b/versioned_docs/version-2.3.0/06-concepts/10-modules.md new file mode 100644 index 00000000..3e4db07a --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/10-modules.md @@ -0,0 +1,98 @@ +# Modules + +Serverpod is built around the concept of modules. A Serverpod module is similar to a Dart package but contains both server, client, and Flutter code. A module contains its own namespace for endpoints and methods to minimize the risk of conflicts. + +Examples of modules are the `serverpod_auth` module and the `serverpod_chat` module, which both are maintained by the Serverpod team. + +## Adding a module to your project + +### Server setup + +To add a module to your project, you must include the server and client/Flutter packages in your project's `pubspec.yaml` files. + +For example, to add the `serverpod_auth` module to your project, you need to add `serverpod_auth_server` to your server's `pubspec.yaml`: + +```yaml +dependencies: + serverpod_auth_server: ^1.x.x +``` + +:::info + +Make sure to replace `1.x.x` with the Serverpod version you are using. Serverpod uses the same version number for all official packages. If you use the same version, you will be sure that everything works together. + +::: + +In your `config/generator.yaml` you can optionally add the `serverpod_auth` module and give it a `nickname`. The nickname will determine how you reference the module from the client. If the module isn't added in the `generator.yaml`, the default nickname for the module will be used. + +```yaml +modules: + serverpod_auth: + nickname: auth +``` + +Then run `pub get` and `serverpod generate` from your server's directory (e.g., `mypod_server`) to add the module to your project's deserializer. + +```bash +$ dart pub get +$ serverpod generate +``` + +Finally, since modules might include modifications to the database schema, you should create a new database migration and apply it by running `serverpod create-migration` then `dart bin/main.dart --apply-migrations` from your server's directory. + +```bash +$ serverpod create-migration +$ dart bin/main.dart --apply-migrations +``` + +### Client setup + +In your client's `pubspec.yaml`, you will need to add the generated client code from the module. + +```yaml +dependencies: + serverpod_auth_client: ^1.x.x +``` + +### Flutter app setup + +In your Flutter app, add the corresponding dart or Flutter package(s) to your `pubspec.yaml`. + +```yaml +dependencies: + serverpod_auth_shared_flutter: ^1.x.x + serverpod_auth_google_flutter: ^1.x.x + serverpod_auth_apple_flutter: ^1.x.x +``` + +## Referencing a module + +It can be useful to reference serializable objects in other modules from the YAML-files in your models. You do this by adding the module prefix, followed by the nickname of the package. For instance, this is how you reference a serializable class in the auth package. + +```yaml +class: MyClass +fields: + userInfo: module:auth:UserInfo +``` + +## Creating custom modules + +With the `serverpod create` command, it is possible to create new modules for code that is shared between projects or that you want to publish to pub.dev. To create a module instead of a server project, pass `module` to the `--template` flag. + +```bash +$ serverpod create --template module my_module +``` + +The create command will create a server and a client Dart package. If you also want to add custom Flutter code, use `flutter create` to create a package. + +```bash +$ flutter create --template package my_module_flutter +``` + +In your Flutter package, you most likely want to import the client libraries created by `serverpod create`. + +:::info + +Most modules will need a set of database tables to function. When naming the tables, you should use the module name as a prefix to the table name to avoid any conflicts. For instance, the Serverpod tables are prefixed with `serverpod_`. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/01-setup.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/01-setup.md new file mode 100644 index 00000000..51336a8b --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/01-setup.md @@ -0,0 +1,255 @@ +# Setup + +Serverpod comes with built-in user management and authentication. It is possible to build a [custom authentication implementation](custom-overrides), but the recommended way to authenticate users is to use the `serverpod_auth` module. The module makes it easy to authenticate with email or social sign-ins and currently supports signing in with email, Google, Apple, and Firebase. + +Future versions of the authentication module will include more options. If you write another authentication module, please consider [contributing](/contribute) your code. + +![Sign-in with Serverpod](https://github.com/serverpod/serverpod/raw/main/misc/images/sign-in.png) + +## Installing the auth module + +Serverpod's auth module makes it easy to authenticate users through email or 3rd parties. The authentication module also handles basic user information, such as user names and profile pictures. Make sure to use the same version numbers as for Serverpod itself for all dependencies. + +## Server setup + +Add the module as a dependency to the server project's `pubspec.yaml`. + +```sh +$ dart pub add serverpod_auth_server +``` + +Add the authentication handler to the Serverpod instance. + +```dart +import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; + +void run(List args) async { + var pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: auth.authenticationHandler, // Add this line + ); + + ... +} +``` +Optionally, add a nickname for the module in the `config/generator.yaml` file. This nickname will be used as the name of the module in the code. + +```yaml +modules: + serverpod_auth: + nickname: auth +``` + +While still in the server project, generate the client code and endpoint methods for the auth module by running the `serverpod generate` command line tool. + +```bash +$ serverpod generate +``` + +### Initialize the auth database + +After adding the module to the server project, you need to initialize the database. First you have to create a new migration that includes the auth module tables. This is done by running the `serverpod create-migration` command line tool in the server project. + +```bash +$ serverpod create-migration +``` + +Start your database container from the server project. + +```bash +$ docker-compose up --build --detach +``` + +Then apply the migration by starting the server with the `apply-migrations` flag. + +```bash +$ dart run bin/main.dart --role maintenance --apply-migrations +``` + +The full migration instructions can be found in the [migration guide](../database/migrations). + +### Configure Authentication +Serverpod's auth module comes with a default Authentication Configuration. To customize it, go to your main `server.dart` file, import the `serverpod_auth_server` module and set up the authentication configuration: + + +```dart +import 'package:serverpod_auth_server/module.dart' as auth; + +void run(List args) async { + + auth.AuthConfig.set(auth.AuthConfig( + minPasswordLength: 12, + )); + + // Start the Serverpod server. + await pod.start(); +} + +``` + +| **Property** | **Description** | **Default** | +|:-------------|:----------------|:-----------:| +| **allowUnsecureRandom** | True if unsecure random number generation is allowed. If set to false, an error will be thrown if the platform does not support secure random number generation. | false | +| **emailSignInFailureResetTime** | The reset period for email sign in attempts. Defaults to 5 minutes. | 5min | +| **enableUserImages** | True if user images are enabled. | true | +| **extraSaltyHash** | True if the server should use the accounts email address as part of the salt when storing password hashes (strongly recommended). | true | +| **firebaseServiceAccountKeyJson** | Firebase service account key JSON file. Generate and download from the Firebase console. | - | +| **importUserImagesFromGoogleSignIn** | True if user images should be imported when signing in with Google. | true | +| **legacyUserSignOutBehavior** | Defines the default behavior for the deprecated `signOut` method used in the status endpoint. This setting controls whether users are signed out from all active devices (`SignOutOption.allDevices`) or just the current device (`SignOutOption.currentDevice`). | `SignOutOption.allDevices` | +| **maxAllowedEmailSignInAttempts** | Max allowed failed email sign in attempts within the reset period. | 5 | +| **maxPasswordLength** | The maximum length of passwords when signing up with email. | 128 | +| **minPasswordLength** | The minimum length of passwords when signing up with email. | 8 | +| **onUserCreated** | Called after a user has been created. Listen to this callback if you need to do additional setup. | - | +| **onUserUpdated** | Called whenever a user has been updated. This can be when the user name is changed or if the user uploads a new profile picture. | - | +| **onUserWillBeCreated** | Called when a user is about to be created, gives a chance to abort the creation by returning false. | - | +| **passwordResetExpirationTime** | The time for password resets to be valid. | 24h | +| **sendPasswordResetEmail** | Called when a user should be sent a reset code by email. | - | +| **sendValidationEmail** | Called when a user should be sent a validation code on account setup. | - | +| **userCanEditFullName** | True if users can edit their full name. | false | +| **userCanEditUserImage** | True if users can update their profile images. | true | +| **userCanEditUserName** | True if users can edit their user names. | true | +| **userCanSeeFullName** | True if users can view their full name. | true | +| **userCanSeeUserName** | True if users can view their user name. | true | +| **userImageFormat** | The format used to store user images. | jpg | +| **userImageGenerator** | Generator used to produce default user images. | - | +| **userImageQuality** | The quality setting for images if JPG format is used. | 70 | +| **userImageSize** | The size of user images. | 256 | +| **userInfoCacheLifetime** | The duration which user infos are cached locally in the server. | 1min | +| **validationCodeLength** | The length of the validation code used in the authentication process. This value determines the number of digits in the validation code. Setting this value to less than 3 reduces security. | 8 | + +## Client setup + +Add the auth client in your client project's `pubspec.yaml`. + +```yaml +dependencies: + ... + serverpod_auth_client: ^1.x.x +``` + +## App setup + +First, add dependencies to your app's `pubspec.yaml` file for the methods of signing in that you want to support. + +```yaml +dependencies: + flutter: + sdk: flutter + serverpod_flutter: ^1.x.x + auth_example_client: + path: ../auth_example_client + + serverpod_auth_shared_flutter: ^1.x.x +``` + +Next, you need to set up a `SessionManager`, which keeps track of the user's state. It will also handle the authentication keys passed to the client from the server, upload user profile images, etc. + +```dart +late SessionManager sessionManager; +late Client client; + +void main() async { + // Need to call this as we are using Flutter bindings before runApp is called. + WidgetsFlutterBinding.ensureInitialized(); + + // The android emulator does not have access to the localhost of the machine. + // const ipAddress = '10.0.2.2'; // Android emulator ip for the host + + // On a real device replace the ipAddress with the IP address of your computer. + const ipAddress = 'localhost'; + + // Sets up a singleton client object that can be used to talk to the server from + // anywhere in our app. The client is generated from your server code. + // The client is set up to connect to a Serverpod running on a local server on + // the default port. You will need to modify this to connect to staging or + // production servers. + client = Client( + 'http://$ipAddress:8080/', + authenticationKeyManager: FlutterAuthenticationKeyManager(), + )..connectivityMonitor = FlutterConnectivityMonitor(); + + // The session manager keeps track of the signed-in state of the user. You + // can query it to see if the user is currently signed in and get information + // about the user. + sessionManager = SessionManager( + caller: client.modules.auth, + ); + await sessionManager.initialize(); + + runApp(MyApp()); +} +``` + +The `SessionManager` has useful methods for viewing and monitoring the user's current state. + +#### Check authentication state +To check if the user is signed in: + +```dart +sessionManager.isSignedIn; +``` +Returns `true` if the user is signed in, or `false` otherwise. + +#### Access current user +To retrieve information about the current user: + +```dart +sessionManager.signedInUser; +``` +Returns a `UserInfo` object if the user is currently signed in, or `null` if the user is not. + +#### Register authentication +To register a signed in user in the session manager: + +```dart +await sessionManager.registerSignedInUser( + userInfo, + keyId, + authKey, +); +``` +This will persist the user information and refresh any open streaming connection, see [Custom Providers - Client Setup](providers/custom-providers#client-setup) for more details. + +#### Monitor authentication changes +To add a listener that tracks changes in the user's authentication state, useful for updating the UI: + +```dart +@override +void initState() { + super.initState(); + + // Rebuild the page if authentication state changes. + sessionManager.addListener(() { + setState(() {}); + }); +} +``` +The listener is triggered whenever the user's sign-in state changes. + +#### Sign out current device +To sign the user out on from the current device: + +```dart +await sessionManager.signOutDevice(); +``` +Returns `true` if the sign-out is successful, or `false` if it fails. + +#### Sign out all devices +To sign the user out across all devices: + +```dart +await sessionManager.signOutAllDevices(); +``` +Returns `true` if the user is successfully signed out from all devices, or `false` if it fails. + + +:::info + +The `signOut` method is deprecated. This method calls the deprecated `signOut` status endpoint. For additional details, see the [deprecated signout endpoint](basics#deprecated-signout-endpoint) section. Use `signOutDevice` or `signOutAllDevices` instead. + +```dart +await sessionManager.signOut(); // Deprecated +``` +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/02-basics.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/02-basics.md new file mode 100644 index 00000000..1ba5d56e --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/02-basics.md @@ -0,0 +1,141 @@ +# The basics + +Serverpod automatically checks if the user is logged in and if the user has the right privileges to access the endpoint. When using the `serverpod_auth` module you will not have to worry about keeping track of tokens, refreshing them or, even including them in requests as this all happens automatically under the hood. + +The `Session` object provides information about the current user. A unique `userId` identifies a user. You should use this id whenever you a referring to a user. Access the id of a signed-in user through the `authenticated` field of the `Session` object. + +```dart +Future myMethod(Session session) async { + var userId = (await session.authenticated)?.userId; + ... +} +``` + +You can also use the Session object to check if a user is authenticated: + +```dart +Future myMethod(Session session) async { + var isSignedIn = await session.isUserSignedIn; + ... +} +``` + +## Requiring authentication on endpoints + +It is common to want to restrict access to an endpoint to users that have signed in. You can do this by overriding the `requireLogin` property of the `Endpoint` class. + +```dart +class MyEndpoint extends Endpoint { + @override + bool get requireLogin => true; + + Future myMethod(Session session) async { + ... + } + ... +} +``` + +## Authorization on endpoints + +Serverpod also supports scopes for restricting access. One or more scopes can be associated with a user. For instance, this can be used to give admin access to a specific user. To restrict access for an endpoint, override the `requiredScopes` property. Note that setting `requiredScopes` implicitly sets `requireLogin` to true. + +```dart +class MyEndpoint extends Endpoint { + @override + bool get requireLogin => true; + + @override + Set get requiredScopes => {Scope.admin}; + + Future myMethod(Session session) async { + ... + } + ... +} +``` + +### Managing scopes + +New users are created without any scopes. To update a user's scopes, use the `Users` class's `updateUserScopes` method (requires the `serverpod_auth_server` package). This method replaces all previously stored scopes. + +```dart +await Users.updateUserScopes(session, userId, {Scope.admin}); +``` + +### Custom scopes + +You may need more granular access control for specific endpoints. To create custom scopes, extend the Scope class, as shown below: + +```dart +class CustomScope extends Scope { + const CustomScope(String name) : super(name); + + static const userRead = CustomScope('userRead'); + static const userWrite = CustomScope('userWrite'); +} +``` + +Then use the custom scopes like this: + +```dart +class MyEndpoint extends Endpoint { + @override + bool get requireLogin => true; + + @override + Set get requiredScopes => {CustomScope.userRead, CustomScope.userWrite}; + + Future myMethod(Session session) async { + ... + } + ... +} +``` + +:::caution +Keep in mind that a scope is merely an arbitrary string and can be written in any format you prefer. However, it's crucial to use unique strings for each scope, as duplicated scope strings may lead to unintentional data exposure. +::: + +## User authentication + +The `StatusEndpoint` class includes methods for handling user sign-outs, whether from a single device or all devices. + +:::info + +In addition to the `StatusEndpoint` methods, Serverpod provides more comprehensive tools for managing user authentication and sign-out processes across multiple devices. + +For more detailed information on managing and revoking authentication keys, please refer to the [Revoking authentication keys](providers/custom-providers#revoking-authentication-keys) section. + +::: + +#### Sign out device + +To sign out a single device: + +```dart +await client.modules.auth.status.signOutDevice(); +``` + +This status endpoint method obtains the authentication key from session's authentication information, then revokes that key. + +#### Sign out all devices + +To sign the user out across all devices: + +```dart +await client.modules.auth.status.signOutAllDevices(); +``` + +This status endpoint retrieves the user ID from session's authentication information, then revokes all authentication keys related to that user. + +:::info + +The `signOut` status endpoint is deprecated. Use `signOutDevice` or `signOutAllDevices` instead. + +```dart +await client.modules.auth.status.signOut(); // Deprecated +``` + +The behavior of `signOut` is controlled by `legacyUserSignOutBehavior`, which you can adjust in the [configure authentication](setup#configure-authentication) section. This allows you to control the signout behaviour of already shipped clients. +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/03-working-with-users.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/03-working-with-users.md new file mode 100644 index 00000000..beb07da4 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/03-working-with-users.md @@ -0,0 +1,31 @@ +# Working with users + +It's a common task to read or update user information on your server. You can always retrieve the id of a signed-in user through the session object. + +```dart +var userId = (await session.authenticated)?.userId; +``` + +If you sign in users through the auth module, you will be able to retrieve more information through the static methods of the `Users` class. + +```dart +var userInfo = await Users.findUserByUserId(session, userId!); +``` + +The `UserInfo` is automatically populated when the user signs in. Different data may be available depending on which method was used for authentication. + +:::tip + +The `Users` class contains many other convenient methods for working with users. You can find the full documentation [here](https://pub.dev/documentation/serverpod_auth_server/latest/protocol/Users-class.html). + +::: + +## Displaying or editing user images + +The module has built-in methods for handling a user's basic settings, including uploading new profile pictures. + +![UserImageButton](https://github.com/serverpod/serverpod/raw/main/misc/images/user-image-button.png) + +To display a user's profile picture, use the `CircularUserImage` widget and pass a `UserInfo` retrieved from the `SessionManager`. + +To edit a user profile image, use the `UserImageButton` widget. It will automatically fetch the signed-in user's profile picture and communicate with the server. diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/01-email.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/01-email.md new file mode 100644 index 00000000..29551d9e --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/01-email.md @@ -0,0 +1,193 @@ +# Email + +To properly configure Sign in with Email, you must connect your Serverpod to an external service that can send the emails. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Sendgrid or Mandrill, support SMTP. + +A comprehensive tutorial covering email/password sign-in complete with sending the validation code via email is available [here](https://medium.com/serverpod/getting-started-with-serverpod-authentication-part-1-72c25280e6e9). + +:::caution +You need to install the auth module before you continue, see [Setup](../setup). +::: + +## Server-side configuration + +In your main `server.dart` file, import the `serverpod_auth_server` module, and set up the authentication configuration: + +```dart +import 'package:serverpod_auth_server/module.dart' as auth; + +auth.AuthConfig.set(auth.AuthConfig( + sendValidationEmail: (session, email, validationCode) async { + // Send the validation email to the user. + // Return `true` if the email was successfully sent, otherwise `false`. + return true; + }, + sendPasswordResetEmail: (session, userInfo, validationCode) async { + // Send the password reset email to the user. + // Return `true` if the email was successfully sent, otherwise `false`. + return true; + }, +)); + +// Start the Serverpod server. +await pod.start(); +``` + +:::info + +For debugging purposes, you can print the validation code to the console. The chat module example does just this. You can view that code [here](https://github.com/serverpod/serverpod/blob/main/examples/chat/chat_server/lib/server.dart). + +::: + +## Client-side configuration + +Add the dependencies to your `pubspec.yaml` in your **client** project. + +```yaml +dependencies: + ... + serverpod_auth_client: ^1.x.x +``` + +Add the dependencies to your `pubspec.yaml` in your **Flutter** project. + +```yaml +dependencies: + ... + serverpod_auth_email_flutter: ^1.x.x + serverpod_auth_shared_flutter: ^1.x.x +``` + +### Prebuilt sign in button + +The package includes both methods for creating a custom email sign-in form and a pre-made `SignInWithEmailButton` widget. The widget is easy to use, all you have to do is supply the auth client. It handles everything from user signups, login, and password resets for you. + +```dart + SignInWithEmailButton( + caller: client.modules.auth, + onSignedIn: () { + // Optional callback when user successfully signs in + }, +), +``` + +![SignInWithEmailButton](/img/authentication/providers/email/1-sign-in-with-email-button.png) + +### Modal example + +The triggered modal will look like this: + +![SignInWithEmailDialog](/img/authentication/providers/email/2-auth-email-dialog.png) + +## Custom UI with EmailAuthController + +The `serverpod_auth_email_flutter` module provides the `EmailAuthController` class, which encapsulates the functionality for email/password authentication. You can use this class and create a custom UI for user registration, login, and password management. + +```dart +import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart'; + +final authController = EmailAuthController(client.modules.auth); +``` + +To let a user signup first call the `createAccountRequest` method which will trigger the backend to send an email to the user with the validation code. + +```dart +await authController.createAccountRequest(userName, email, password); +``` + +Then let the user type in the code and send it to the backend with the `validateAccount` method. This method will create the user and sign them in if the code is valid. + +```dart +await authController.validateAccount(email, verificationCode); +``` + +To let users log in if they already have an account you can use the `signIn` method. + +```dart +await authController.signIn(email, password); +``` + +Finally to let a user reset their password you first initiate a password reset with the `initiatePasswordReset` this will trigger the backend to send a verification email to the user. + +```dart +await authController.initiatePasswordReset(email); +``` + +Let the user type in the verification code along with the new password and send it to the backend with the `resetPassword` method. + +```dart +await authController.resetPassword(email, verificationCode, password); +``` + +After the password has been reset you have to call the `signIn` method to log in. This can be achieved by either letting the user type in the details again or simply chaining the `resetPassword` method and the `singIn` method for a seamless UX. + +## Password storage security + +Serverpod provides some additional configurable options to provide extra layers of security for stored password hashes. + +:::info +By default, the minimum password length is set to 8 characters. If you wish to modify this requirement, you can utilize the properties within AuthConfig. +::: + +### Peppering + +For an additional layer of security, it is possible to configure a password hash pepper. A pepper is a server-side secret that is added, along with a unique salt, to a password before it is hashed and stored. The pepper makes it harder for an attacker to crack password hashes if they have only gained access to the database. + +The [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes. + +To configure a pepper, set the `emailPasswordPepper` property in the `config/passwords.yaml` file. + +```yaml +development: + emailPasswordPepper: 'your-pepper' +``` + + It is essential to keep the pepper secret and never expose it to the client. + +:::warning + +If the pepper is changed, all passwords in the database will need to be re-hashed with the new pepper. + +::: + +### Secure random + +Serverpod uses the `dart:math` library to generate random salts for password hashing. By default, if no secure random number generator is available, a cryptographically unsecure random number is used. + +It is possible to prevent this fallback by setting the `allowUnsecureRandom` property in the `AuthConfig` to `false`. If the `allowUnsecureRandom` property is false, the server will throw an exception if a secure random number generator is unavailable. + +```dart +auth.AuthConfig.set(auth.AuthConfig( + allowUnsecureRandom: false, +)); +``` + +## Custom password hash generator + +It is possible to override the default password hash generator. The `AuthConfig` class allows you to provide a custom hash generator using the field `passwordHashGenerator` and a custom hash validator through the field `passwordHashValidator`. + +```dart +AuthConfig( + passwordHashValidator: ( + password, + email, + hash, { + onError, + onValidationFailure, + }, + ) { + // Custom hash validator. + }, + passwordHashGenerator: (password) { + // Custom hash generator. + }, +) + +``` + +It could be useful if you already have stored passwords that should be preserved or migrated. + +:::warning + +Using a custom hash generator will permanently disrupt compatibility with the default hash generator. + +::: \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/02-google.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/02-google.md new file mode 100644 index 00000000..caa8b601 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/02-google.md @@ -0,0 +1,244 @@ +# Google + +To set up Sign in with Google, you will need a Google account for your organization and set up a new project. For the project, you need to set up _Credentials_ and _Oauth consent screen_. You will also need to add the `serverpod_auth_google_flutter` package to your app and do some additional setup depending on each platform. + +A comprehensive tutorial covering everything about google sign in is available [here](https://medium.com/serverpod/integrating-google-sign-in-with-serverpod-authentication-part-2-6fade3099baf). + +:::note +Right now, we have official support for iOS, Android, and Web for Google Sign In. +::: + +:::caution +You need to install the auth module before you continue, see [Setup](../setup). +::: + +## Create your credentials + +To implement Google Sign In, you need a google cloud project. You can create one in the [Google Cloud Console](https://console.cloud.google.com/). + +### Enable Peoples API + +To be allowed to access user data and use the authentication method in Serverpod we have to enable the Peoples API in our project. + +[Enable it here](https://console.cloud.google.com/apis/library/people.googleapis.com) or find it yourself by, navigating to the _Library_ section under _APIs & Services_. Search for _Google People API_, select it, and click on _Enable_. + +### Setup OAuth consent screen + +The setup for the OAuth consent screen can be found [here](https://console.cloud.google.com/apis/credentials/consent) or under _APIs & Services_ > _OAuth consent screen_. + +1. Fill in all the required information, for production use you need a domain that adds under `Authorized` domains. + +2. Add the scopes `.../auth/userinfo.email` and `.../auth/userinfo.profile`. + +3. Add your email to the test users so that you can test your integration in development mode. + +![Scopes](/img/authentication/providers/google/1-scopes.png) + +## Server-side configuration + +Create the server credentials in the google cloud console. Navigate to _Credentials_ under _APIs & Services_. Click _Create Credentials_ and select _OAuth client ID_. Configure the OAuth client as a _**Web application**_. If you have a domain add it to the `Authorized JavaScript origins` and `Authorized redirect URIs`. For development purposes we can add `http://localhost:8082` to both fields, this is the address to the web server. + +Download the JSON file for your web application OAuth client. This file contains both the client id and the client secret. Rename the file to `google_client_secret.json` and place it in your server's `config` directory. + +:::warning + +The `google_client_secret.json` contains a private key and should not be version controlled. + +::: + +![Google credentials](/img/6-google-credentials.jpg) + +## Client-side configuration + +For our client-side configurations, we have to first create client-side credentials and include the credentials files in our projects. The Android and iOS integrations use the [google_sign_in](https://pub.dev/packages/google_sign_in) package under the hood, any documentation there should also apply to this setup. + +:::info +Rather than using the credentails file for iOS and Android, you can pass the `clientId` and the `serverClientId` to the `signInWithGoogle` method or the `SignInWithGoogleButton` widget. The `serverClientId` is the client ID from the server credentials. +::: + +### iOS + +Create the client credentials. Navigate to _Credentials_ under _APIs & Services_. Click _Create Credentials_ and select _OAuth client ID_. Configure the OAuth client as Application type _**iOS**_. + +Fill in all the required information, and create the credentials. Then download the `plist` file rename it to `GoogleService-Info.plist` and put it inside your ios project folder. Then drag and drop it into your XCode project to include the file in your build. + +Open the `GoogleService-Info.plist` in your editor and add the SERVER_CLIENT_ID if it does not exist: + +```xml + + ... + SERVER_CLIENT_ID + your_server_client_id + +``` + +Replace `your_server_client_id` with the client id from the JSON file you put inside the config folder in the server. + +#### Add the URL scheme + +To allow us to navigate back to the app after the user has signed in we have to add the URL Scheme, the scheme is the reversed client ID of your iOS app. You can find it inside the `GoogleService-Info.plist` file. + +Open the `info.plist` file in your editor and add the following to register the URL Scheme. + +```xml + + ... + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + your_reversed_client_id + + + + +``` + +Replace `your_reversed_client_id` with your reversed client ID. + +:::info + +If you have any social logins in your app you also need to integrate "Sign in with Apple" to publish your app to the app store. ([Read more](https://developer.apple.com/sign-in-with-apple/get-started/)). + +::: + +### Android + +Create the client credentials. Navigate to _Credentials_ under _APIs & Services_. Click _Create Credentials_ and select _OAuth client ID_. Configure the OAuth client as Application type _**Android**_. + +Fill in all required information, you can get the debug SHA-1 hash by running `./gradlew signingReport` in your Android project directory. Create the credentials and download the JSON file. + +Put the file inside the `android/app/` directory and rename it to `google-services.json`. + +:::info +For a production app you need to get the SHA-1 key from your production keystore! This can be done by running this command: ([Read more](https://support.google.com/cloud/answer/6158849#installedapplications&android&zippy=%2Cnative-applications%2Candroid)). + +```bash +$ keytool -list -v -keystore /path/to/keystore +``` + +::: + +### Web + +There is no need to create any client credentials for the web we will simply pass the `serverClientId` to the sign-in button. +However, we have to modify the server credentials inside the google cloud console. + +Navigate to _Credentials_ under _APIs & Services_ and select the server credentials. Under `Authorized JavaScript origins` and `Authorized redirect URIs` add the domain for your Flutter app, for development, this is `http://localhost:port` where the port is the port you are using. + +:::info + +Force flutter to run on a specific port by running. + +```bash +$ flutter run -d chrome --web-port=49660 +``` + +::: + +Set up the actual redirect URI where the user will navigate after the sign-in. You can choose any path you want but it has to be the same in the credentials, your server, and Flutter configurations. + +For example, using the path `/googlesignin`. + +For development inside `Authorized redirect URIs` add `http://localhost:8082/googlesignin`, in production use `https://example.com/googlesignin`. + +![Google credentials](/img/authentication/providers/google/2-credentials.png) + +#### Serve the redirect page + +Register the Google Sign In route inside `server.dart`. + +```dart +import 'package:serverpod_auth_server/module.dart' as auth + + +void run(List args) async { + ... + pod.webServer.addRoute(auth.RouteGoogleSignIn(), '/googlesignin'); + ... +} +``` + +This page is needed for the web app to receive the authentication code given by Google. + +### Flutter implementation + +![Scopes](/img/authentication/providers/google/3-button.png) + +Add the `SignInWithGoogleButton` to your widget. + +```dart +import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; + + +SignInWithGoogleButton( + caller: client.modules.auth, + serverClientId: _googleServerClientId, // needs to be supplied for the web integration + redirectUri: Uri.parse('http://localhost:8082/googlesignin'), +) +``` + +As an alternative to adding the JSON files in your client projects, you can supply the client and server ID on iOS and Android. + +```dart +import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; + + +SignInWithGoogleButton( + caller: client.modules.auth, + clientId: _googleClientId, // Client ID of the client (null on web) + serverClientId: _googleServerClientId, // Client ID from the server (required on web) + redirectUri: Uri.parse('http://localhost:8082/googlesignin'), +) +``` + +## Calling Google APIs + +The default setup allows access to basic user information, such as email, profile image, and name. You may require additional access scopes, such as accessing a user's calendar, contacts, or files. To do this, you will need to: + +- Add the required scopes to the OAuth consent screen. +- Request access to the scopes when signing in. Do this by setting the `additionalScopes` parameter of the `signInWithGoogle` method or the `SignInWithGoogleButton` widget. + +A full list of available scopes can be found [here](https://developers.google.com/identity/protocols/oauth2/scopes). + +:::info + +Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. + +::: + +On the server side, you can now access these Google APIs. If a user has signed in with Google, use the `GoogleAuth.authClientForUser` method from the `serverpod_auth_server` package to request an `AutoRefreshingAuthClient`. The `AutoRefreshingAuthClient` can be used to access Google's APIs on the user's behalf. + +For instance, to access the Youtube APIs, add the scope to your `SignInWithGoogleButton` in your app: + +```dart +SignInWithGoogleButton( + ... + additionalScopes: const ['https://www.googleapis.com/auth/youtube'], +) +``` + +On the server, you can utilize the [googleapis](https://pub.dev/packages/googleapis) package to access the Youtube API by first creating a client, then calling the API. + +```dart +import 'package:serverpod_auth_server/module.dart'; +import 'package:googleapis/youtube/v3.dart'; + + +final googleClient = await GoogleAuth.authClientForUser(session, userId); + +if (googleClient != null) { + var youTubeApi = YouTubeApi(googleClient); + + var favorites = await youTubeApi.playlistItems.list( + ['snippet'], + playlistId: 'LL', // Liked List + ); + +} else { + // The user hasn't signed in with Google. +} +``` diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/03-apple.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/03-apple.md new file mode 100644 index 00000000..156cfc1a --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/03-apple.md @@ -0,0 +1,57 @@ +# Apple + +Sign-in with Apple, requires that you have a subscription to the [Apple developer program](https://developer.apple.com/programs/), even if you only want to test the feature in development mode. + +A comprehensive tutorial covering Sign in with Apple is available [here](https://medium.com/serverpod/integrating-apple-sign-in-with-serverpod-authentication-part-3-f5a49d006800). + +:::note +Right now, we have official support for iOS and MacOS for Sign in with Apple. +::: + +:::caution +You need to install the auth module before you continue, see [Setup](../setup). +::: + +## Server-side configuration + +No extra steps outside installing the auth module are required. + +## Client-side configuration + +Add the dependency to your `pubspec.yaml` in your flutter project. + +```yaml +dependencies: + ... + serverpod_auth_apple_flutter: ^1.x.x +``` + +### Config + +Enable the sign-in with Apple capability in your Xcode project, this is the same type of configuration for your iOS and MacOS projects respectively. + +![Add capabilities](/img/authentication/providers/apple/1-xcode-add.png) + +![Sign in with Apple](/img/authentication/providers/apple/2-xcode-sign-in-with-apple.png) + +### Sign in button + +`serverpod_auth_apple_flutter` package comes with the widget `SignInWithAppleButton` that renders a nice Sign in with Apple button and triggers the native sign-in UI. + +```dart +import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart'; + +SignInWithAppleButton( + caller: client.modules.auth, +); +``` + +The SignInWithAppleButton widget takes a caller parameter that you pass in the authentication module from your Serverpod client, in this case, client.modules.auth. + +![Sign-in button](/img/authentication/providers/apple/3-button.png) + +## Extra + +The `serverpod_auth_apple_flutter` implements the sign-in logic using [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple). The documentation for this package should in most cases also apply to the Serverpod integration. + +_Note that Sign in with Apple may not work on some versions of the Simulator (iOS 13.5 works). This issue doesn't affect real devices._ diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/05-firebase.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/05-firebase.md new file mode 100644 index 00000000..1816b20b --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/05-firebase.md @@ -0,0 +1,98 @@ +# Firebase + +Serverpod uses [Firebase UI auth](https://pub.dev/packages/firebase_ui_auth) to handle authentication through Firebase. It allows you to add social sign-in types that Serverpod doesn't directly support. + +:::warning + +Serverpod automatically merges accounts that are using the same email addresses, so make sure only to allow sign-ins where the email has been verified. + +::: + +## Server-side configuration + +The server needs the service account credentials for access to your Firebase project. To create a new key go to the [Firebase console](https://console.firebase.google.com/) then navigate to `project settings > service accounts` click on `Generate new private key` and then `Generate key`. + +![Service account](/img/authentication/providers/firebase/1-server-key.png) + +This will download the JSON file, rename it to `firebase_service_account_key.json` and place it in the `config` folder in your server project. Note that if this file is corrupt or if the name does not follow the convention here the authentication with firebase will fail. + +:::danger +The firebase_service_account_key.json file gives admin access to your Firebase project, never store it in version control. +::: + +## Client-side configuration + +To add authentication with Firebase, you must first install and initialize the Firebase CLI tools and Flutter fire. Follow the instructions [here](https://firebase.google.com/docs/flutter/setup?platform=web) for your Flutter project. + +## Firebase config + +The short version: + +```bash +$ flutter pub add firebase_core firebase_auth firebase_ui_auth +$ flutterfire configure +``` + +In the Firebase console, configure the different social sign-ins you plan to use, under `Authentication > Sign-in method`. + +![Auth provider](/img/authentication/providers/firebase/2-auth-provider.png) + +In your `main.dart` in your flutter project add: + +```dart +import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; + +... +void main() async { + ... + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + firebase.FirebaseUIAuth.configureProviders([ + firebase.PhoneAuthProvider(), + ]); + + ... + runApp(const MyApp()); +} +``` + +## Trigger the auth UI with Serverpod + +Add the [serverpod_auth_firebase_flutter](https://pub.dev/packages/serverpod_auth_firebase_flutter) package. + +```bash +$ flutter pub add serverpod_auth_firebase_flutter +``` + +The `SignInWithFirebaseButton` is a convenient button that triggers the sign-in flow and can be used like this: + +```dart +SignInWithFirebaseButton( + caller: client.modules.auth, + authProviders: [ + firebase.PhoneAuthProvider(), + ], + onFailure: () => print('Failed to sign in with Firebase.'), + onSignedIn: () => print('Signed in with Firebase.'), +) +``` + +Where `caller` is the Serverpod client you use to talk with the server and `authProviders` a list with the firebase auth providers you want to enable in the UI. + +You can also trigger the Firebase auth UI by calling the method `signInWithFirebase` like so: + +```dart +await signInWithFirebase( + context: context, + caller: client.modules.auth, + authProviders: [ + firebase.PhoneAuthProvider(), + ], +); +``` + +Where `context` is your `BuildContext`, `caller` and `authProviders` are the same as for the button. The method returns a nullable [UserInfo](../working-with-users) object, if the object is null the Sign-in failed, if not the Sign-in was successful. diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/06-custom-providers.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/06-custom-providers.md new file mode 100644 index 00000000..1f0892de --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/06-custom-providers.md @@ -0,0 +1,258 @@ +# Custom providers + +Serverpod's authentication module makes it easy to implement custom authentication providers. This allows you to leverage all the existing providers supplied by the module along with the specific providers your project requires. + +## Server setup + +After successfully authenticating a user through a customer provider, an auth token can be created and connected to the user to preserve the authenticated user's permissions. This token is used to identify the user and facilitate endpoint authorization validation. The token can be removed when the user signs out to prevent further access. + +### Connect user + +The authentication module provides methods to find or create users. This ensures that all authentication tokens from the same user are connected. + +Users can be identified either by their email through the `Users.findUserByEmail(...)` method or by a unique identifier through the `Users.findUserByIdentifier(...)` method. + +If no user is found, a new user can be created through the `Users.createUser(...)` method. + +```dart +UserInfo? userInfo; +userInfo = await Users.findUserByEmail(session, email); +userInfo ??= await Users.findUserByIdentifier(session, userIdentifier); +if (userInfo == null) { + userInfo = UserInfo( + userIdentifier: userIdentifier, + userName: name, + email: email, + blocked: false, + created: DateTime.now().toUtc(), + scopeNames: [], + ); + userInfo = await Users.createUser(session, userInfo, _authMethod); +} +``` + +The example above tries to find a user by email and user identifier. If no user is found, a new user is created with the provided information. + +:::note +For many authentication platforms the `userIdentifier` is the user's email, but it can also be another unique identifier such as a phone number or a social security number. +::: + +### Custom identification methods + +If other identification methods are required you can easily implement them by accessing the database directly. The `UserInfo` model can be interacted with in the same way as any other model with a database in Serverpod. + +```dart +var userInfo = await UserInfo.db.findFirstRow( + session, + where: (t) => t.fullName.equals(name), +); +``` + +The example above shows how to find a user by name using the `UserInfo` model. + +### Create auth token + +When a user has been found or created, an auth token that is connected to the user should be created. + +To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible as a static method, e.g. `UserAuthentication.signInUser`. + +The `signInUser` method takes four arguments: the first is the session object, the second is the user ID, the third is information about the method of authentication, and the fourth is a set of scopes granted to the auth token. + +```dart +var authToken = await UserAuthentication.signInUser(userInfo.id, 'myAuthMethod', scopes: { + Scope('delete'), + Scope('create'), +}); +``` + +The example above creates an auth token for a user with the unique identifier taken from the `userInfo`. The auth token preserves that it was created using the method `myAuthMethod` and has the scopes `delete` and `create`. + +:::info +The unique identifier for the user should uniquely identify the user regardless of authentication method. The information allows authentication tokens associated with the same user to be grouped. +::: + +### Send auth token to client + +Once the auth token is created, it should be sent to the client. We recommend doing this using an `AuthenticationResponse`. This ensures compatibility with the client-side authentication module. + +```dart +class MyAuthenticationEndpoint extends Endpoint { + Future login( + Session session, + String username, + String password, + ) async { + // Authenticates a user with email and password. + if (!authenticateUser(session, username, password)) { + return AuthenticationResponse(success: false); + } + + // Finds or creates a user in the database using the User methods. + var userInfo = findOrCreateUser(session, username); + + // Creates an authentication key for the user. + var authToken = await UserAuthentication.signInUser( + session, + userInfo.id!, + 'myAuth', + scopes: {}, + ); + + // Returns the authentication response. + return AuthenticationResponse( + success: true, + keyId: authToken.id, + key: authToken.key, + userInfo: userInfo, + ); + } +} +``` + +The example above shows how to create an `AuthenticationResponse` with the auth token and user information. + +### Revoking authentication keys + +Serverpod provides built-in methods for managing user authentication across multiple devices. These methods handle several critical security and state management processes automatically, ensuring consistent and secure authentication state across your servers. When using the authentication management methods (`signOutUser` or `revokeAuthKey`), the following key actions are automatically handled: + +- Closing all affected method streaming connections to maintain connection integrity. +- Synchronizing authentication state across all connected servers. +- Updating the session's authentication state with `session.updateAuthenticated(null)` if the affected user is currently authenticated. + +#### Revoking specific keys + +To revoke specific authentication keys, use the `revokeAuthKey` method: + +```dart +await UserAuthentication.revokeAuthKey( + session, + authKeyId: 'auth-key-id-here', +); +``` + +##### Fetching and revoking an authentication key using AuthenticationInfo + +To revoke a specific authentication key for the current session, you can directly access the session's authentication information and call the `revokeAuthKey` method: + +```dart +// Fetch the authentication information for the current session +var authId = (await session.authenticated)?.authId; + +// Revoke the authentication key if the session is authenticated and has an authId +if (authId != null) { + await UserAuthentication.revokeAuthKey( + session, + authKeyId: authId, + ); +} +``` + +##### Fetching and revoking a specific authentication key for a user + +To revoke a specific authentication key associated with a user, you can retrieve all authentication keys for that user and select the key you wish to revoke: + +```dart +// Fetch all authentication keys for the user +var authKeys = await AuthKey.db.find( + session, + where: (t) => t.userId.equals(userId), +); + +// Revoke a specific key (for example, the last one) +if (authKeys.isNotEmpty) { + var authKeyId = authKeys.last.id.toString(); // Convert the ID to string + await UserAuthentication.revokeAuthKey( + session, + authKeyId: authKeyId, + ); +} +``` + +##### Removing specific tokens (direct deletion) + +```dart +await AuthKey.db.deleteWhere( + session, + where: (t) => t.userId.equals(userId) & t.method.equals('username'), +); +``` + +:::warning + +Directly removing authentication tokens from the `AuthKey` table bypasses necessary processes such as closing method streaming connections and synchronizing servers state. It is strongly recommended to use `UserAuthentication.revokeAuthKey` to ensure a complete and consistent sign-out. + +::: + +#### Signing out all devices + +The `signOutUser` method signs a user out from all devices: + +```dart +await UserAuthentication.signOutUser( + session, + userId: 123, // Optional: If omitted, the currently authenticated user will be signed out +); +``` +This method deletes all authentication keys associated with the user. + +##### Signing out a specific user + +In this example, a specific `userId` is provided to sign out that user from all their devices: + +```dart +// Sign out the user with ID 123 from all devices +await UserAuthentication.signOutUser( + session, + userId: 123, +); +``` + +##### Signing out the currently authenticated user + +If no `userId` is provided, `signOutUser` will automatically sign out the user who is currently authenticated in the session: + +```dart +// Sign out the currently authenticated user +await UserAuthentication.signOutUser( + session, // No userId provided, signs out the current user +); +``` + +#### Creating a logout endpoint + +To sign out a user on all devices using an endpoint, the `signOutUser` method in the `UserAuthentication` class can be used: + +```dart +class AuthenticatedEndpoint extends Endpoint { + @override + bool get requireLogin => true; + + Future logout(Session session) async { + await UserAuthentication.signOutUser(session); + } +} +``` + +## Client setup + +The client must store and include the auth token in communication with the server. Luckily, the client-side authentication module handles this for you through the `SessionManager`. + +The session manager is responsible for storing the auth token and user information. It is initialized on client startup and will restore any existing user session from local storage. + +After a successful authentication where an authentication response is returned from the server, the user should be registered in the session manager through the `sessionManager.registerSignedInUser(...)` method. The session manager singleton is accessible by calling `SessionManager.instance`. + +```dart +var serverResponse = await caller.myAuthentication.login(username, password); + +if (serverResponse.success) { + // Store the user info in the session manager. + SessionManager sessionManager = await SessionManager.instance; + await sessionManager.registerSignedInUser( + serverResponse.userInfo!, + serverResponse.keyId!, + serverResponse.key!, + ); +} +``` + +The example above shows how to register a signed-in user in the session manager. \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/_category_.json b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/_category_.json new file mode 100644 index 00000000..e9b38091 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/04-providers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Providers", + "collapsed": true +} diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/05-custom-overrides.md b/versioned_docs/version-2.3.0/06-concepts/11-authentication/05-custom-overrides.md new file mode 100644 index 00000000..5d1572e5 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/05-custom-overrides.md @@ -0,0 +1,254 @@ +# Custom overrides + +It is recommended to use the `serverpod_auth` package but if you have special requirements not fulfilled by it, you can implement your authentication module. Serverpod is designed to make it easy to add custom authentication overrides. + +## Server setup + +When running a custom auth integration it is up to you to build the authentication model and issuing auth tokens. + +### Token validation + +The token validation is performed by providing a custom `AuthenticationHandler` callback when initializing Serverpod. The callback should return an `AuthenticationInfo` object if the token is valid, otherwise `null`. + +```dart +// Initialize Serverpod and connect it with your generated code. +final pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: (Session session, String token) async { + /// Custom validation handler + if (token != 'valid') return null; + + return AuthenticationInfo(1, {}); + }, +); +``` + +In the above example, the `authenticationHandler` callback is overridden with a custom validation method. The method returns an `AuthenticationInfo` object with user id `1` and no scopes if the token is valid, otherwise `null`. + +:::note +In the authenticationHandler callback the `authenticated` field on the session will always be `null` as it is the authenticationHandler that figures out who the user is. +::: + +:::info +By specifying the optional `authId` field in the `AuthenticationInfo` object you can link the user to a specific authentication id. This is useful when revoking authentication for a specific device. +::: + +#### Scopes + +The scopes returned from the `authenticationHandler` is used to grant access to scope restricted endpoints. The `Scope` class is a simple wrapper around a nullable `String` in dart. This means that you can format your scopes however you want as long as they are in a String format. + +Normally if you implement a JWT you would store the scopes inside the token. When extracting them all you have to do is convert the String stored in the token into a Scope object by calling the constructor. + +```dart +List scopes = extractScopes(token); +Set userScopes = scopes.map((scope) => Scope(scope)).toSet(); +``` + +### Handling revoked authentication + +When a user's authentication is revoked, the server must be notified to respect the changes (e.g. to close method streams). Invoke the `session.messages.authenticationRevoked` method and raise the appropriate event to notify the server. + +```dart +var userId = 1; +var revokedScopes = ['write']; +var message = RevokedAuthenticationScope( + scopes: revokedScopes, +); + +await session.messages.authenticationRevoked( + userId, + message, +); +``` + +##### Parameters + +- `userId` - The user id belonging to the `AuthenticationInfo` object to be revoked. +- `message` - The revoked authentication event message. See below for the different type of messages. + +#### Revoked authentication messages +There are three types of `RevokedAuthentication` messages that are used to specify the extent of the authentication revocation: + +| Message type | Description | +|-----------|-------------| +| `RevokedAuthenticationUser` | All authentication is revoked for a user. | +| `RevokedAuthenticationAuthId` | A single authentication id is revoked for the user. This should match the `authId` field in the `AuthenticationInfo` object. | +| `RevokedAuthenticationScope` | List of scopes that have been revoked for a user. | + +Each message type provides a tailored approach to revoke authentication based on different needs. + +### Send token to client + +You are responsible for implementing the endpoints to authenticate/authorize the user. But as an example such an endpoint could look like the following. + +```dart +class UserEndpoint extends Endpoint { + Future login( + Session session, + String username, + String password, + ) async { + var identifier = authenticateUser(session, username, password); + if (identifier == null) return null; + + return issueMyToken(identifier, scopes: {}); + } +} +``` + +In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client. + +## Client setup + +Enabling authentication in the client is as simple as configuring a key manager and placing any token in it. If a key manager is configured, the client will automatically query the manager for a token and include it in communication with the server. + +### Configure key manager + +Key managers need to implement the `AuthenticationKeyManager` interface. The key manager is configured when creating the client by passing it as the named parameter `authenticationKeyManager`. If no key manager is configured, the client will not include tokens in requests to the server. + +```dart +class SimpleAuthKeyManager extends AuthenticationKeyManager { + String? _key; + + @override + Future get() async { + return _key; + } + + @override + Future put(String key) async { + _key = key; + } + + @override + Future remove() async { + _key = null; + } +} + + +var client = Client('http://$localhost:8080/', + authenticationKeyManager: SimpleAuthKeyManager()) + ..connectivityMonitor = FlutterConnectivityMonitor(); +``` + +In the above example, the `SimpleAuthKeyManager` is configured as the client's authentication key manager. The `SimpleAuthKeyManager` stores the token in memory. + +:::info + +The `SimpleAuthKeyManager` is not practical and should only be used for testing. A secure implementation of the key manager is available in the `serverpod_auth_shared_flutter` package named `FlutterAuthenticationKeyManager`. It provides safe, persistent storage for the auth token. + +::: + +The key manager is then available through the client's `authenticationKeyManager` field. + +```dart +var keyManager = client.authenticationKeyManager; +``` + +### Store token + +When the client receives a token from the server, it is responsible for storing it in the key manager using the `put` method. The key manager will then include the token in all requests to the server. + +```dart +await client.authenticationKeyManager?.put(token); +``` + +In the above example, the `token` is placed in the key manager. It will now be included in communication with the server. + +### Remove token + +To remove the token from the key manager, call the `remove` method. + +```dart +await client.authenticationKeyManager?.remove(); +``` + +The above example removes any token from the key manager. + +### Retrieve token + +To retrieve the token from the key manager, call the `get` method. + +```dart +var token = await client.authenticationKeyManager?.get(); +``` + +The above example retrieves the token from the key manager and stores it in the `token` variable. + +## Authentication schemes + +By default Serverpod will pass the authentication token from client to server in accordance with the HTTP `authorization` header standard with the `basic` scheme name and encoding. This is securely transferred as the connection is TLS encrypted. + +The default implementation encodes and wraps the user-provided token in a `basic` scheme which is automatically unwrapped on the server side before being handed to the user-provided authentication handler described above. + +In other words the default transport implementation is "invisible" to user code. + +### Implementing your own authentication scheme + +If you are implementing your own authentication and are using the `basic` scheme, note that this is supported but will be automatically unwrapped i.e. decoded on the server side before being handed to your `AuthenticationHandler` implementation. It will in this case receive the decoded auth key value after the `basic` scheme name. + +If you are implementing a different authentication scheme, for example OAuth 2 using bearer tokens, you should override the default method `toHeaderValue` of `AuthenticationKeyManager`. This client-side method converts the authentication key to the format that shall be sent as a transport header to the server. + +You will also need to implement the `AuthenticationHandler` accordingly, in order to process that header value server-side. + +The header value must be compliant with the HTTP header format defined in RFC 9110 HTTP Semantics, 11.6.2. Authorization. +See: +- [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) +- [RFC 9110, 11.6.2. Authorization](https://httpwg.org/specs/rfc9110.html#field.authorization) + +An approach to adding OAuth handling might make changes to the above code akin to the following. + +Client side: + +```dart +class MyOAuthKeyManager extends AuthenticationKeyManager { + String? _key; + + @override + Future get() async { + return _key; + } + + @override + Future put(String key) async { + _key = key; + } + + @override + Future remove() async { + _key = null; + } + + @override + Future toHeaderValue(String? key) async { + if (key == null) return null; + return 'Bearer ${myBearerTokenObtainer(key)}'; + } +} + + +var client = Client('http://$localhost:8080/', + authenticationKeyManager: SimpleAuthKeyManager()) + ..connectivityMonitor = FlutterConnectivityMonitor(); +``` + +Server side: + +```dart +// Initialize Serverpod and connect it with your generated code. +final pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: (Session session, String token) async { + /// Bearer token validation handler + var (uid, scopes) = myBearerTokenValidator(token) + if (uid == null) return null; + + return AuthenticationInfo(uid, scopes); + }, +); +``` diff --git a/versioned_docs/version-2.3.0/06-concepts/11-authentication/_category_.json b/versioned_docs/version-2.3.0/06-concepts/11-authentication/_category_.json new file mode 100644 index 00000000..c2b36a37 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/11-authentication/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Authentication", + "collapsed": true + } + \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/12-file-uploads.md b/versioned_docs/version-2.3.0/06-concepts/12-file-uploads.md new file mode 100644 index 00000000..0d290dcf --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/12-file-uploads.md @@ -0,0 +1,154 @@ +# Uploading files + +Serverpod has built-in support for handling file uploads. Out of the box, your server is configured to use the database for storing files. This works well for testing but may not be performant in larger-scale applications. You should set up your server to use Google Cloud Storage or S3 in production scenarios. + +## How to upload a file + +A `public` and `private` file storage are set up by default to use the database. You can replace these or add more configurations for other file storages. + +### Server-side code + +There are a few steps required to upload a file. First, you must create an upload description on the server and pass it to your app. The upload description grants access to the app to upload the file. If you want to grant access to any file, you can add the following code to one of your endpoints. However, in most cases, you may want to restrict which files can be uploaded. + +```dart +Future getUploadDescription(Session session, String path) async { + return await session.storage.createDirectFileUploadDescription( + storageId: 'public', + path: path, + ); +} +``` + +After the file is uploaded, you should verify that the upload has been completed. If you are uploading a file to a third-party service, such as S3 or Google Cloud Storage, there is no other way of knowing if the file was uploaded or if the upload was canceled. + +```dart +Future verifyUpload(Session session, String path) async { + return await session.storage.verifyDirectFileUpload( + storageId: 'public', + path: path, + ); +} +``` + +### Client-side code + +To upload a file from the app side, first request the upload description. Next, upload the file. You can upload from either a `Stream` or a `ByteData` object. If you are uploading a larger file, using a `Stream` is better because not all of the data must be held in RAM memory. Finally, you should verify the upload with the server. + +```dart +var uploadDescription = await client.myEndpoint.getUploadDescription('myfile'); +if (uploadDescription != null) { + var uploader = FileUploader(uploadDescription); + await uploader.upload(myStream); + var success = await client.myEndpoint.verifyUpload('myfile'); +} +``` + +:::info + +In a real-world app, you most likely want to create the file paths on your server. For your file paths to be compatible with S3, do not use a leading slash; only use standard characters and numbers. E.g.: + +```dart +'profile/$userId/images/avatar.png' +``` + +::: + +## Accessing stored files + +It's possible to quickly check if an uploaded file exists or access the file itself. If a file is in a public storage, it is also accessible to the world through an URL. If it is private, it can only be accessed from the server. + +To check if a file exists, use the `fileExists` method. + +```dart +var exists = await session.storage.fileExists( + storageId: 'public', + path: 'my/file/path', +); +``` + +If the file is in a public storage, you can access it through its URL. + +```dart +var url = await session.storage.getPublicUrl( + storageId: 'public', + path: 'my/file/path', +); +``` + +You can also directly retrieve or store a file from your server. + +```dart +var myByteData = await session.storage.retrieveFile( + storageId: 'public', + path: 'my/file/path', +); +``` + +## Add a configuration for GCP + +Serverpod uses Google Cloud Storage's HMAC interoperability to handle file uploads to Google Cloud. To make file uploads work, you must make a few custom configurations in your Google Cloud console: + +1. Create a service account with the _Storage Admin_ role. +2. Under _Cloud Storage_ > _Settings_ > _Interoperability_, create a new HMAC key for your newly created service account. +3. Add the two keys you received in the previous step to your `config/password.yaml` file. The keys should be named `HMACAccessKeyId` and `HMACSecretKey`, respectively. +4. When creating a new bucket, set the _Access control_ to _Fine-grained_ and disable the _Prevent public access_ option. + +You may also want to add the bucket as a backend for your load balancer to give it a custom domain name. + +When you have set up your GCP bucket, you need to configure it in Serverpod. Add the GCP package to your `pubspec.yaml` file and import it in your `server.dart` file. + +```dart +import 'package:serverpod_cloud_storage_gcp/serverpod_cloud_storage_gcp.dart' + as gcp; +``` + +After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your GCP bucket to be accessible on a custom domain through a load balancer. You should add the cloud storage before starting your pod. The `bucket` parameter refers to the GCP bucket name (you can find it in the console) and the `publicHost` is the domain name used to access the bucket via https. + +```dart + pod.addCloudStorage(gcp.GoogleCloudStorage( + serverpod: pod, + storageId: 'public', + public: true, + region: 'auto', + bucket: 'my-bucket-name', + publicHost: 'storage.myapp.com', + )); +``` + +## Add a configuration for AWS S3 + +This section shows how to set up a storage using S3. Before you write your Dart code, you need to set up an S3 bucket. Most likely, you will also want to set up a CloudFront for the bucket, where you can use a custom domain and your own SSL certificate. Finally, you will need to get a set of AWS access keys and add them to your Serverpod password file. + +When you are all set with the AWS setup, include the S3 package in your `pubspec.yaml` file and import it in your `server.dart` file. + +```dart +import 'package:serverpod_cloud_storage_s3/serverpod_cloud_storage_s3.dart' + as s3; +``` + +After creating your Serverpod, you add a storage configuration. If you want to replace the default `public` or `private` storages, set the `storageId` to `public` or `private`. Set the public host if you have configured your S3 bucket to be accessible on a custom domain through CloudFront. You should add the cloud storage before starting your pod. + +```dart +pod.addCloudStorage(s3.S3CloudStorage( + serverpod: pod, + storageId: 'public', + public: true, + region: 'us-west-2', + bucket: 'my-bucket-name', + publicHost: 'storage.myapp.com', +)); +``` + +For your S3 configuration to work, you will also need to add your AWS credentials to the `passwords.yaml` file. You create the access keys from your AWS console when signed in as the root user. + +```yaml +shared: + AWSAccessKeyId: 'XXXXXXXXXXXXXX' + AWSSecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' +``` + +:::info + +If you are using the GCP or AWS Terraform scripts that are created with your Serverpod project, the required GCP or S3 buckets will be created automatically. The scripts will also configure your load balancer or Cloudfront and the certificates needed to access the buckets securely. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/13-health-checks.md b/versioned_docs/version-2.3.0/06-concepts/13-health-checks.md new file mode 100644 index 00000000..7598b79e --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/13-health-checks.md @@ -0,0 +1,39 @@ +# Health checks + +Serverpod automatically performs health checks while running. It measures CPU and memory usage and the response time to the database. The metrics are stored in the database every minute in the serverpod_health_metric and serverpod_health_connection_info tables. However, the best way to visualize the data is through Serverpod Insights, which gives you a graphical view. + +## Adding custom metrics + +Sometimes it is helpful to add custom health metrics. This can be for monitoring external services or internal processes within your Serverpod. To set up your custom metrics, you must create a `HealthCheckHandler` and register it with your Serverpod. + +```dart +// Create your custom health metric handler. +Future> myHealthCheckHandler( + Serverpod pod, DateTime timestamp) async { + // Actually perform some checks. + + // Return a list of health metrics for the given timestamp. + return [ + ServerHealthMetric( + name: 'MyMetric', + serverId: pod.serverId, + timestamp: timestamp, + isHealthy: true, + value: 1.0, + ), + ]; +} +``` + +Register your handler when you create your Serverpod object. + +```dart +final pod = Serverpod( + args, + Protocol(), + Endpoints(), + healthCheckHandler: myHealthCheckHandler, + ); +``` + +Once registered, your health check handler will be called once a minute to perform any health checks that you have configured. You can view the status of your checks in Serverpod Insights or in the database. diff --git a/versioned_docs/version-2.3.0/06-concepts/14-scheduling.md b/versioned_docs/version-2.3.0/06-concepts/14-scheduling.md new file mode 100644 index 00000000..e50ccb40 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/14-scheduling.md @@ -0,0 +1,83 @@ +# Scheduling + +With Serverpod you can schedule future work with the `future call` feature. Future calls are calls that will be invoked at a later time. An example is if you want to send a drip-email campaign after a user signs up. You can schedule a future call for a day, a week, or a month. The calls are stored in the database, so they will persist even if the server is restarted. + +A future call is guaranteed to only execute once across all your instances that are running, but execution failures are not handled automatically. It is your responsibility to schedule a new future call if the work was not able to complete. + +Creating a future call is simple, extend the `FutureCall` class and override the `invoke` method. The method takes two params the first being the [`Session`](sessions) object and the second being an optional SerializableModel ([See models](models)). + +:::info +The future call feature is not enabled when running Serverpod in serverless mode. +::: + +```dart +import 'package:serverpod/serverpod.dart'; + +class ExampleFutureCall extends FutureCall { + @override + Future invoke(Session session, MyModelEntity? object) async { + // Do something interesting in the future here. + } +} +``` + +To let your Server get access to the future call you have to register it in the main run method in your `server.dart` file. You register the future call by calling `registerFutureCall` on the Serverpod object and giving it an instance of the future call together with a string that gives the future call a name. The name has to be globally unique and is used to later invoke the future call. + +```dart +void run(List args) async { + final pod = Serverpod( + args, + Protocol(), + Endpoints(), + ); + + ... + + pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall'); + + ... +} +``` + +You are now able to register a future call to be invoked in the future by calling either `futureCallWithDelay` or `futureCallAtTime` depending on your needs. + +Invoke the future call 1 hour from now by calling `futureCallWithDelay`. + +```dart +await session.serverpod.futureCallWithDelay( + 'exampleFutureCall', + data, + const Duration(hours: 1), +); +``` + +Invoke the future call at a specific time and/or date in the future by calling `futureCallAtTime`. + +```dart +await session.serverpod.futureCallAtTime( + 'exampleFutureCall', + data, + DateTime(2025, 1, 1), +); +``` + +:::note +`data` is an object created from a class defined in one of your yaml files and has to be the same as the one you expect to receive in the future call. in the `model` folder, `data` may also be null if you don't need it. +::: + +When registering a future call it is also possible to give it an `identifier` so that it can be referenced later. The same identifier can be applied to multiple future calls. + +```dart +await session.serverpod.futureCallWithDelay( + 'exampleFutureCall', + data, + const Duration(hours: 1), + identifier: 'an-identifying-string', +); +``` + +This identifier can then be used to cancel all future calls registered with said identifier. + +```dart +await session.serverpod.cancelFutureCall('an-identifying-string'); +``` diff --git a/versioned_docs/version-2.3.0/06-concepts/15-streams.md b/versioned_docs/version-2.3.0/06-concepts/15-streams.md new file mode 100644 index 00000000..7fa80918 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/15-streams.md @@ -0,0 +1,214 @@ +# Streams and messaging + +For some applications, it's not enough to be able to call server-side methods. You may also want to push data from the server to the client or send data two-way. Examples include real-time games or chat applications. Luckily, Serverpod supports a framework for streaming data. It's possible to stream any serialized objects to or from any endpoint. + +Serverpod supports two ways to stream data. The first approach, [streaming methods](#streaming-methods), imitates how `Streams` work in Dart and offers a simple interface that automatically handles the connection with the server. In contrast, the second approach, [streaming endpoint](#streaming-endpoints), requires developers to manage the web socket connection. The second approach was Serverpod's initial solution for streaming data but will be removed in future updates. + +## Streaming Methods + +When an endpoint method is defined with `Stream` instead of `Future` as the return type or includes `Stream` as a method parameter, it is recognized as a streaming method. Streaming methods transmit data over a shared, self-managed web socket connection that automatically connects and disconnects from the server. + +### Defining a streaming method + +Streaming methods are defined by using the `Stream` type as either the return value or a parameter. + +Following is an example of a streaming method that echoes back any message: + +```dart +class ExampleEndpoint extends Endpoint { + Stream echoStream(Session session, Stream stream) async* { + await for (var message in stream) { + yield message; + } + } +} +``` + +The generic for the `Stream` can also be defined, e.g., `Stream`. This definition is then included in the client, enabling static type validation. + +The streaming method above can then be called from the client like this: + +```dart +var inStream = StreamController(); +var outStream = client.example.echoStream(inStream.stream); +outStream.listen((message) { + print('Received message: $message'); +}); + +inStream.add('Hello'); +inStream.add(42); + +// This will print +// Received message: Hello +// Received message: 42 +``` + +In the example above, the `echoStream` method passes back any message sent through the `outStream`. + +:::tip + +Note that we can mix different types in the stream. This stream is defined as dynamic and can contain any type that can be serialized by Serverpod. + +::: + +### Lifecycle of a streaming method + +Each time the client calls a streaming method, a new `Session` is created, and a call with that `Session` is made to the method endpoint on the server. The `Session` is automatically closed when the streaming method call is over. + +If the web socket connection is lost, all streaming methods are closed on the server and the client. + +When the streaming method is defined with a returning `Stream`, the method is kept alive until the stream subscription is canceled on the client or the method returns. + +When the streaming method returns a `Future`, the method is kept alive until the method returns. + +Streams in parameters are closed when the stream is closed. This can be done by either closing the stream on the client or canceling the subscription on the server. + +All streams in parameters are closed when the method call is over. + +### Authentication + +Authentication is seamlessly integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session. + +Authentication is validated when the stream is first established, utilizing the authentication data stored in the `Session` object. If a user's authentication is subsequently revoked—requiring denial of access to the stream—the stream will be promptly closed, and an exception will be thrown. + +For more details on handling revoked authentication, refer to the section on [handling revoked authentication](authentication/custom-overrides#Handling-revoked-authentication). + +### Error handling + +Error handling works just like in regular endpoint methods in Serverpod. If an exception is thrown on a stream, the stream is closed with an exception. If the exception thrown is a serializable exception, the exception is first serialized and passed over the stream before it is closed. + +This is supported in both directions; stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client. + +```dart +class ExampleEndpoint extends Endpoint { + Stream echoStream(Session session, Stream stream) async* { + stream.listen((message) { + // Do nothing + }, onError: (error) { + print('Server received error: $error'); + throw SerializableException('Error from server'); + }); + } +} +``` + +```dart +var inStream = StreamController(); +var outStream = client.example.echoStream(inStream.stream); +outStream.listen((message) { + // Do nothing +}, onError: (error) { + print('Client received error: $error'); +}); + +inStream.addError(SerializableException('Error from client')); + +// This will print +// Server received error: Error from client +// Client received error: Error from server +``` + +In the example above, the client sends an error to the server, which then throws an exception back to the client. And since the exception is serializable, it is passed over the stream before the stream is closed. + +Read more about serializable exceptions here: [Serializable exceptions](exceptions). + +## Streaming Endpoints + +Streaming endpoints were Serverpod's first attempt at streaming data. This approach is more manual, requiring developers to manage the WebSocket connection to the server. + +### Handling streams server-side + +The Endpoint class has three methods you override to work with streams. + +- `streamOpened` is called when a user connects to a stream on the Endpoint. +- `streamClosed` is called when a user disconnects from a stream on the Endpoint. +- `handleStreamMessage` is called when a serialized message is received from a client. + +To send a message to a client, call the `sendStreamMessage` method. You will need to include the session associated with the user. + +#### The user object + +It's often handy to associate a state together with a streaming session. Typically, you do this when a stream is opened. + +```dart +Future streamOpened(StreamingSession session) async { + setUserObject(session, MyUserObject()); +} +``` + +You can access the user object at any time by calling the `getUserObject` method. The user object is automatically discarded when a session ends. + +#### Internal server messaging + +A typical scenario when working with streams is to pass on messages from one user to another. For instance, if one client sends a chat message to the server, the server should send it to the correct user. Serverpod comes with a built-in messaging system that makes this easy. You can pass messages locally on a single server, but messages are passed through Redis by default. Passing the messages through Redis makes it possible to send the messages between multiple servers in a cluster. + +In most cases, it's easiest to subscribe to a message channel in the `streamOpened` method. The subscription will automatically be disposed of when the stream is closed. The following example will forward any message sent to a user identified by its user id. + +```dart +@override +Future streamOpened(StreamingSession session) async { + final authenticationInfo = await session.authenticated; + final userId = authenticationInfo?.userId; + session.messages.addListener( + 'user_$userId', + (message) { + sendStreamMessage(session, message); + }, + ); +} +``` + +In your `handleStreamMessage` method, you can pass on messages to the correct channel. + +```dart +@override +Future handleStreamMessage( + StreamingSession session, + SerializableModel message, +) async { + if (message is MyChatMessage) { + session.messages.postMessage( + 'user_${message.recipientId}', + message, + ); + } +} +``` + +:::tip + +For a real-world example, check out [Pixorama](https://pixorama.live). It's a multi-user drawing experience showcasing Serverpod's real-time capabilities and comes with complete source code. + +::: + +### Handling streams in your app + +Before you can access streams in your client, you need to connect to the server's web socket. You do this by calling connectWebSocket on your client. + +```dart +await client.openStreamingConnection(); + +``` + +You can monitor the state of the connection by adding a listener to the client. +Once connected to your server's web socket, you can pass and receive serialized objects. + +Listen to its web socket stream to receive updates from an endpoint on the server. + +```dart +await for (var message in client.myEndpoint.stream) { + _handleMessage(message); +} +``` + +You send messages to the server's endpoint by calling `sendStreamMessage`. + +```dart +client.myEndpoint.sendStreamMessage(MyMessage(text: 'Hello')); +``` + +:::info + +Authentication is handled automatically. If you have signed in, your web socket connection will be authenticated. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/16-backward-compatibility.md b/versioned_docs/version-2.3.0/06-concepts/16-backward-compatibility.md new file mode 100644 index 00000000..14b197e9 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/16-backward-compatibility.md @@ -0,0 +1,9 @@ +# Backward compatibility + +As your app evolves, features will be added or changed. However, your users may still use older versions of the app as not everyone will update to the latest version and automatic updates through the app stores take time. Therefore it may be essential to make updates to your server compatible with older app versions. + +Following a simple set of rules, your server will stay compatible with older app versions: + +1. __Avoid changing parameter names in endpoint methods.__ In the REST API Serverpod generates, the parameters are passed by name. This means that changing the parameter names of the endpoint methods will break backward compatibility. +2. __Do not delete endpoint methods or change their signature.__ Instead, add new methods if you must pass another set of parameters. Technically, you can add new named parameters if they are not required, but creating a new method may still feel cleaner. +3. __Avoid changing or removing fields and types in the serialized classes.__ However, you are free to add new fields as long as they are nullable. diff --git a/versioned_docs/version-2.3.0/06-concepts/17-webserver.md b/versioned_docs/version-2.3.0/06-concepts/17-webserver.md new file mode 100644 index 00000000..53f8850b --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/17-webserver.md @@ -0,0 +1,74 @@ +# Web server + +In addition to the application server, Serverpod comes with a built-in web server. The web server allows you to access your database and business layer the same way you would from a method call from an app. This makes it very easy to share data for applications that need both an app and traditional web pages. You can also use the web server to create webhooks or generate custom REST APIs to communicate with 3rd party services. + +:::caution + +Serverpod's web server is still experimental, and the APIs may change in the future. This documentation should give you some hints on getting started, but we plan to add more extensive documentation as the web server matures. + +::: + +When you create a new Serverpod project, it sets up a web server by default. When working with the web server, there are two main classes to understand; `WidgetRoute` and `Widget`. The `WidgetRoute` provides an entry point for a call to the server and returns a `Widget`. The `Widget` renders a web page or response using templates, JSON, or other custom means. + +## Creating new routes and widgets + +To add new pages to your web server, you add new routes. Typically, you do this in your server.dart file before you start the Serverpod. By default, Serverpod comes with a `RootRoute` and a static directory. + +When receiving a web request, Serverpod will search and match the routes in the order they were added. You can end a route's path with an asterisk (`*`) to match all paths with the same beginning. + +```dart +// Add a single page. +pod.webServer.addRoute(MyRoute(), '/my/page/address'); + +// Match all paths that start with /item/ +pod.webServer.addRoute(AnotherRoute(), '/item/*'); +``` + +Typically, you want to create custom routes for your pages. Do this by overriding the WidgetRoute class and implementing the build method. + +```dart +class MyRoute extends WidgetRoute { + @override + Future build(Session session, HttpRequest request) async { + return MyPageWidget(title: 'Home page'); + } +} +``` + +Your route's build method returns a Widget. The Widget consists of an HTML template file and a corresponding Dart class. Create a new custom Widget by overriding the Widget class. Then add a corresponding HTML template and place it in the `web/templates` directory. The HTML file uses the [Mustache](https://mustache.github.io/) template language. You set your template parameters by updating the `values` field of your `Widget` class. The values are converted to `String` objects before being passed to the template. This makes it possible to nest widgets, similarly to how widgets work in Flutter. + +```dart +class MyPageWidget extends Widget { + MyPageWidget({String title}) : super(name: 'my_page') { + values = { + 'title': title, + }; + } +} +``` + +:::info + +In the future, we plan to add a widget library to Serverpod with widgets corresponding to the standard widgets used by Flutter, such as Column, Row, Padding, Container, etc. This would make it possible to render server-side widgets with the same code used within Flutter. + +::: + +## Special widgets and routes + +Serverpod comes with a few useful special widgets and routes you can use out of the box. When returning these special widget types, Serverpod's web server will automatically set the correct HTTP status codes and content types. + +- `WidgetList` concatenates a list of other widgets into a single widget. +- `WidgetJson` renders a JSON document from a serializable structure of maps, lists, and basic values. +- `WidgetRedirect` creates a redirect to another URL. + +To serve a static directory, use the `RouteStaticDirectory` class. Serverpod will set the correct content types for most file types automatically. + +:::caution + +Static files are configured to be cached hard by the web browser and through Cloudfront's content delivery network (if you use the AWS deployment). If you change static files, they will need to be renamed, or users will most likely access old files. To make this easier, you can add a version number when referencing the static files. The version number will be ignored when looking up the actual file. E.g., `/static/my_image@v42.png` will serve to the `/static/my_image.png` file. More advanced cache management will be coming to a future version of Serverpod. + +::: + +## Database access and logging + +The web server passes a `Session` object to the `WidgetRoute` class' `build` method. This gives you access to all the features you typically get from a standard method call to an endpoint. Use the database, logging, or caching the same way you would in a method call. diff --git a/versioned_docs/version-2.3.0/06-concepts/18-testing/01-get-started.md b/versioned_docs/version-2.3.0/06-concepts/18-testing/01-get-started.md new file mode 100644 index 00000000..78a0b58c --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/18-testing/01-get-started.md @@ -0,0 +1,243 @@ +# Get started + +Serverpod provides simple but feature rich test tools to make testing your backend a breeze. + +:::info + +For Serverpod Mini projects, everything related to the database in this guide can be ignored. + +::: + +
+ Have an existing project? Follow these steps first! +

+For existing non-Mini projects, a few extra things need to be done: +1. Add the `server_test_tools_path` key with the value `test/integration/test_tools` to `config/generator.yaml`: + +```yaml +server_test_tools_path: test/integration/test_tools +``` + + Without this key, the test tools file is not generated. With the above config the location of the test tools file is `test/integration/test_tools/serverpod_test_tools.dart`, but this can be set to any folder (though should be outside of `lib` as per Dart's test conventions). + +2. New projects now come with a test profile in `docker-compose.yaml`. This is not strictly mandatory, but is recommended to ensure that the testing state is never polluted. Add the snippet below to the `docker-compose.yaml` file in the server directory: + +```yaml +# Add to the existing services +postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: _test + POSTGRES_PASSWORD: "" + volumes: + - _test_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test +redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test +volumes: + # ... + _test_data: +``` + +

+Or copy the complete file here. +

+ +```yaml +services: + # Development services + postgres: + image: postgres:16.3 + ports: + - '8090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: + POSTGRES_PASSWORD: "" + volumes: + - _data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - dev + redis: + image: redis:6.2.6 + ports: + - '8091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - dev + + # Test services + postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: _test + POSTGRES_PASSWORD: "" + volumes: + - _test_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test + redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test + +volumes: + _data: + _test_data: +``` + +

+
+3. Create a `test.yaml` file and add it to the `config` directory: + +```yaml +# This is the configuration file for your test environment. +# All ports are set to zero in this file which makes the server find the next available port. +# This is needed to enable running tests concurrently. To set up your server, you will +# need to add the name of the database you are connecting to and the user name. +# The password for the database is stored in the config/passwords.yaml. + +# Configuration for the main API test server. +apiServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# Configuration for the Insights test server. +insightsServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# Configuration for the web test server. +webServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# This is the database setup for your test server. +database: + host: localhost + port: 9090 + name: _test + user: postgres + +# This is the setup for your Redis test instance. +redis: + enabled: false + host: localhost + port: 9091 +``` + +4. Add this entry to `config/passwords.yaml` + +```yaml +test: + database: '' + redis: '' +``` + +5. Add a `dart_test.yaml` file to the `server` directory (next to `pubspec.yaml`) with the following contents: + +```yaml +tags: + integration: {} + +``` + +6. Finally, add the `test` and `serverpod_test` packages as dev dependencies in `pubspec.yaml`: + +```yaml +dev_dependencies: + serverpod_test: # Should be same version as the `serverpod` package + test: ^1.24.2 +``` + +That's it, the project setup should be ready to start using the test tools! +

+
+ +Go to the server directory and generate the test tools: + + ```bash + serverpod generate + ``` + +The default location for the generated file is `test/integration/test_tools/serverpod_test_tools.dart`. The folder name `test/integration` is chosen to differentiate from unit tests (see the [best practises section](best-practises#unit-and-integration-tests) for more information on this). + +The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: + +```dart +import 'package:test/test.dart'; + +// Import the generated file, it contains everything you need. +import 'test_tools/serverpod_test_tools.dart'; + +void main() { + withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { + test('when calling `hello` then should return greeting', () async { + final greeting = await endpoints.example.hello(sessionBuilder, 'Michael'); + expect(greeting, 'Hello Michael'); + }); + }); +} +``` + +A few things to note from the above example: + +- The test tools should be imported from the generated test tools file and not the `serverpod_test` package. +- The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`. + - `sessionBuilder` is used to build a `session` object that represents the server state during an endpoint call and is used to set up scenarios. + - `endpoints` contains all your Serverpod endpoints and lets you call them. + +:::tip + +The location of the test tools can be changed by changing the `server_test_tools_path` key in `config/generator.yaml`. If you remove the `server_test_tools_path` key, the test tools will stop being generated. + +::: + +Before the test can be run the Postgres and Redis also have to be started: + +```bash +docker-compose up --build --detach +``` + +By default this starts up both the `development` and `test` profiles. To only start one profile, simply add `--profile test` to the command. + +Now the test is ready to be run: + +```bash +dart test +``` + +Happy testing! diff --git a/versioned_docs/version-2.3.0/06-concepts/18-testing/02-the-basics.md b/versioned_docs/version-2.3.0/06-concepts/18-testing/02-the-basics.md new file mode 100644 index 00000000..6c2fe9a9 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/18-testing/02-the-basics.md @@ -0,0 +1,259 @@ +# The basics + +## Set up a test scenario + +The `withServerpod` helper provides a `sessionBuilder` that helps with setting up different scenarios for tests. To modify the session builder's properties, call its `copyWith` method. It takes the following named parameters: + +|Property|Description|Default| +|:---|:---|:---:| +|`authentication`|See section [Setting authenticated state](#setting-authenticated-state).|`AuthenticationOverride.unauthenticated()`| +|`enableLogging`|Whether logging is turned on for the session.|`false`| + +The `copyWith` method creates a new unique session builder with the provided properties. This can then be used in endpoint calls (see section [Setting authenticated state](#setting-authenticated-state) for an example). + +To build out a `Session` (to use for [database calls](#seeding-the-database) or [pass on to functions](advanced-examples#test-business-logic-that-depends-on-session)), simply call the `build` method: + +```dart +Session session = sessionBuilder.build(); +``` + +Given the properties set on the session builder through the `copyWith` method, this returns a Serverpod `Session` that has the corresponding state. + +### Setting authenticated state + +To control the authenticated state of the session, the `AuthenticationOverride` class can be used. + +To create an unauthenticated override (this is the default value for new sessions), call `AuthenticationOverride unauthenticated()`: + +```dart +static AuthenticationOverride unauthenticated(); +``` + +To create an authenticated override, call `AuthenticationOverride.authenticationInfo(...)`: + +```dart +static AuthenticationOverride authenticationInfo( + int userId, + Set scopes, { + String? authId, +}) +``` + +Pass these to `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: + +```dart +withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { + // Corresponds to an actual user id + const int userId = 1234; + + group('when authenticated', () { + var authenticatedSessionBuilder = sessionBuilder.copyWith( + authentication: + AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), + ); + + test('then calling `hello` should return greeting', () async { + final greeting = await endpoints.authenticatedExample + .hello(authenticatedSessionBuilder, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); + + group('when unauthenticated', () { + var unauthenticatedSessionBuilder = sessionBuilder.copyWith( + authentication: AuthenticationOverride.unauthenticated(), + ); + + test( + 'then calling `hello` should throw `ServerpodUnauthenticatedException`', + () async { + final future = endpoints.authenticatedExample + .hello(unauthenticatedSessionBuilder, 'Michael'); + await expectLater( + future, throwsA(isA())); + }); + }); +}); +``` + +### Seeding the database + +To seed the database before tests, `build` a `session` and pass it to the database call just as in production code. + +:::info + +By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. See the [rollback database configuration](#rollback-database-configuration) for how to configure this behavior. + +::: + +```dart +withServerpod('Given Products endpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insert(session, [ + Product(name: 'Apple', price: 10), + Product(name: 'Banana', price: 10) + ]); + }); + + test('then calling `all` should return all products', () async { + final products = await endpoints.products.all(sessionBuilder); + expect(products, hasLength(2)); + expect(products.map((p) => p.name), contains(['Apple', 'Banana'])); + }); +}); +``` + +## Environment + +By default `withServerpod` uses the `test` run mode and the database settings will be read from `config/test.yaml`. + +It is possible to override the default run mode by setting the `runMode` setting: + +```dart +withServerpod( + 'Given Products endpoint', + (sessionBuilder, endpoints) { + /* test code */ + }, + runMode: ServerpodRunMode.development, +); +``` + +## Configuration + +The following optional configuration options are available to pass as a second argument to `withServerpod`: + +|Property|Description|Default| +|:-----|:-----|:---:| +|`applyMigrations`|Whether pending migrations should be applied when starting Serverpod.|`true`| +|`enableSessionLogging`|Whether session logging should be enabled.|`false`| +|`rollbackDatabase`|Options for when to rollback the database during the test lifecycle (or disable it). See detailed description [here](#rollback-database-configuration).|`RollbackDatabase.afterEach`| +|`runMode`|The run mode that Serverpod should be running in.|`ServerpodRunmode.test`| +|`serverpodLoggingMode`|The logging mode used when creating Serverpod.|`ServerpodLoggingMode.normal`| +|`serverpodStartTimeout`|The timeout to use when starting Serverpod, which connects to the database among other things. Defaults to `Duration(seconds: 30)`.|`Duration(seconds: 30)`| +|`testGroupTagsOverride`|By default Serverpod test tools tags the `withServerpod` test group with `"integration"`. This is to provide a simple way to only run unit or integration tests. This property allows this tag to be overridden to something else. Defaults to `['integration']`.|`['integration']`| + +### `rollbackDatabase` {#rollback-database-configuration} + +By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case. Just like the following enum describes, the behavior of the automatic rollbacks can be configured: + +```dart +/// Options for when to rollback the database during the test lifecycle. +enum RollbackDatabase { + /// After each test. This is the default. + afterEach, + + /// After all tests. + afterAll, + + /// Disable rolling back the database. + disabled, +} +``` + +There are a few reasons to change the default setting: + +1. **Scenario tests**: when consecutive `test` cases depend on each other. While generally considered an anti-pattern, it can be useful when the set up for the test group is very expensive. In this case `rollbackDatabase` can be set to `RollbackDatabase.afterAll` to ensure that the database state persists between `test` cases. At the end of the `withServerpod` scope, all database changes will be rolled back. + +2. **Concurrent transactions in endpoints**: when concurrent calls are made to `session.db.transaction` inside an endpoint, it is no longer possible for the Serverpod test tools to do these operations as part of a top level transaction. In this case this feature should be disabled by passing `RollbackDatabase.disabled`. + +```dart +Future concurrentTransactionCalls( + Session session, +) async { + await Future.wait([ + session.db.transaction((tx) => /*...*/), + // Will throw `InvalidConfigurationException` if `rollbackDatabase` + // is not set to `RollbackDatabase.disabled` in `withServerpod` + session.db.transaction((tx) => /*...*/), + ]); +} +``` + +When setting `rollbackDatabase.disabled` to be able to test `concurrentTransactionCalls`, remember that the database has to be manually cleaned up to not leak data: + +```dart +withServerpod( + 'Given ProductsEndpoint when calling concurrentTransactionCalls', + (sessionBuilder, endpoints) { + tearDownAll(() async { + var session = sessionBuilder.build(); + // If something was saved to the database in the endpoint, + // for example a `Product`, then it has to be cleaned up! + await Product.db.deleteWhere( + session, + where: (_) => Constant.bool(true), + ); + }); + + test('then should execute and commit all transactions', () async { + var result = + await endpoints.products.concurrentTransactionCalls(sessionBuilder); + // ... + }); + }, + rollbackDatabase: RollbackDatabase.disabled, +); +``` + +Additionally, when setting `rollbackDatabase.disabled`, it may also be needed to pass the `--concurrency=1` flag to the dart test runner. Otherwise multiple tests might pollute each others database state: + +```bash +dart test -t integration --concurrency=1 +``` + +For the other cases this is not an issue, as each `withServerpod` has its own transaction and will therefore be isolated. + +3. **Database exceptions that are quelled**: There is a specific edge case where the test tools behavior deviates from production behavior. See example below: + +```dart +var transactionFuture = session.db.transaction((tx) async { + var data = UniqueData(number: 1, email: 'test@test.com'); + try { + await UniqueData.db.insertRow(session, data, transaction: tx); + await UniqueData.db.insertRow(session, data, transaction: tx); + } on DatabaseException catch (_) { + // Ignore the database exception + } +}); + +// ATTENTION: This will throw an exception in production +// but not in the test tools. +await transactionFuture; +``` + +In production, the transaction call will throw if any database exception happened during its execution, _even_ if the exception was first caught inside the transaction. However, in the test tools this will not throw an exception due to how the nested transactions are emulated. Quelling exceptions like this is not best practise, but if the code under test does this setting `rollbackDatabase` to `RollbackDatabse.disabled` will ensure the code behaves like in production. + +## Test exceptions + +The following exceptions are exported from the generated test tools file and can be thrown by the test tools in various scenarios, see below. + +|Exception|Description| +|:-----|:-----| +|`ServerpodUnauthenticatedException`|Thrown during an endpoint method call when the user was not authenticated.| +|`ServerpodInsufficientAccessException`|Thrown during an endpoint method call when the authentication key provided did not have sufficient access.| +|`ConnectionClosedException`|Thrown during an endpoint method call if a stream connection was closed with an error. For example, if the user authentication was revoked.| +|`InvalidConfigurationException`|Thrown when an invalid configuration state is found.| + +## Test helpers + +### `flushEventQueue` + +Test helper to flush the event queue. +Useful for waiting for async events to complete before continuing the test. + +```dart +Future flushEventQueue(); +``` + +For example, if depending on a generator function to execute up to its `yield`, then the +event queue can be flushed to ensure the generator has executed up to that point: + +```dart +var stream = endpoints.someEndoint.generatorFunction(session); +await flushEventQueue(); +``` + +See also [this complete example](advanced-examples#multiple-users-interacting-with-a-shared-stream). diff --git a/versioned_docs/version-2.3.0/06-concepts/18-testing/03-advanced-examples.md b/versioned_docs/version-2.3.0/06-concepts/18-testing/03-advanced-examples.md new file mode 100644 index 00000000..6d70726f --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/18-testing/03-advanced-examples.md @@ -0,0 +1,149 @@ +# Advanced examples + +## Run unit and integration tests separately + +To run unit and integration tests separately, the `"integration"` tag can be used as a filter. See the following examples: + +```bash +# All tests (unit and integration) +dart test + +# Only integration tests: add --tags (-t) flag +dart test -t integration + +# Only unit tests: add --exclude-tags (-x) flag +dart test -x integration +``` + +To change the name of this tag, see the [`testGroupTagsOverride`](the-basics#configuration) configuration option. + +## Test business logic that depends on `Session` + +It is common to break out business logic into modules and keep it separate from the endpoints. If such a module depends on a `Session` object (e.g to interact with the database), then the `withServerpod` helper can still be used and the second `endpoint` argument can simply be ignored: + +```dart +withServerpod('Given decreasing product quantity when quantity is zero', ( + sessionBuilder, + _, +) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, [ + Product( + id: 123, + name: 'Apple', + quantity: 0, + ), + ]); + }); + + test('then should throw `InvalidOperationException`', + () async { + var future = ProductsBusinessLogic.updateQuantity( + session, + id: 123, + decrease: 1, + ); + + await expectLater(future, throwsA(isA())); + }); +}); +``` + +## Multiple users interacting with a shared stream + +For cases where there are multiple users reading from or writing to a stream, such as real-time communication, it can be helpful to validate this behavior in tests. + +Given the following simplified endpoint: + +```dart +class CommunicationExampleEndpoint { + static const sharedStreamName = 'shared-stream'; + Future postNumberToSharedStream(Session session, int number) async { + await session.messages + .postMessage(sharedStreamName, SimpleData(num: number)); + } + + Stream listenForNumbersOnSharedStream(Session session) async* { + var sharedStream = + session.messages.createStream(sharedStreamName); + + await for (var message in sharedStream) { + yield message.num; + } + } +} +``` + +Then a test to verify this behavior can be written as below. Note the call to the `flushEventQueue` helper (exported by the test tools), which ensures that `listenForNumbersOnSharedStream` executes up to its first `yield` statement before continuing with the test. This guarantees that the stream was registered by Serverpod before messages are posted to it. + +```dart +withServerpod('Given CommunicationExampleEndpoint', (sessionBuilder, endpoints) { + const int userId1 = 1; + const int userId2 = 2; + + test( + 'when calling postNumberToSharedStream and listenForNumbersOnSharedStream ' + 'with different sessions then number should be echoed', + () async { + var userSession1 = sessionBuilder.copyWith( + authentication: AuthenticationOverride.authenticationInfo( + userId1, + {}, + ), + ); + var userSession2 = sessionBuilder.copyWith( + authentication: AuthenticationOverride.authenticationInfo( + userId2, + {}, + ), + ); + + var stream = + endpoints.testTools.listenForNumbersOnSharedStream(userSession1); + // Wait for `listenForNumbersOnSharedStream` to execute up to its + // `yield` statement before continuing + await flushEventQueue(); + + await endpoints.testTools.postNumberToSharedStream(userSession2, 111); + await endpoints.testTools.postNumberToSharedStream(userSession2, 222); + + await expectLater(stream.take(2), emitsInOrder([111, 222])); + }); +}); +``` + +## Optimising number of database connections + +By default, Dart's test runner runs tests concurrently. The number of concurrent tests depends on the running hosts' available CPU cores. If the host has a lot of cores it could trigger a case where the number of connections to the database exceeeds the maximum connections limit set for the database, which will cause tests to fail. + +Each `withServerpod` call will lazily create its own Serverpod instance which will connect to the database. Specifically, the code that causes the Serverpod instance to be created is `sessionBuilder.build()`, which happens at the latest in an endpoint call if not called by the test before. + +If a test needs a session before the endpoint call (e.g. to seed the database), `sessionBuilder.build()` has to be called which then triggers a database connection attempt. + +If the max connection limit is hit, there are two options: + +- Raise the max connections limit on the database. +- Build out the session in `setUp`/`setUpAll` instead of the top level scope: + +```dart +withServerpod('Given example test', (sessionBuilder, endpoints) { + // Instead of this + var session = sessionBuilder.build(); + + + // Do this to postpone connecting to the database until the test group is running + late Session session; + setUpAll(() { + session = sessionBuilder.build(); + }); + // ... +}); +``` + +:::info + +This case should be rare and the above example is not a recommended best practice unless this problem is anticipated, or it has started happening. + +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/18-testing/04-best-practises.md b/versioned_docs/version-2.3.0/06-concepts/18-testing/04-best-practises.md new file mode 100644 index 00000000..4e96a2e6 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/18-testing/04-best-practises.md @@ -0,0 +1,125 @@ +--- +# Don't display do's and don'ts in the table of contents +toc_max_heading_level: 2 +--- + +# Best practises + +## Imports + +While it's possible to import types and test helpers from the `serverpod_test`, it's completely redundant. The generated file exports everything that is needed. Adding an additional import is just unnecessary noise and will likely also be flagged as duplicated imports by the Dart linter. + +### Don't + +```dart +import 'serverpod_test_tools.dart'; +// Don't import `serverpod_test` directly. +import 'package:serverpod_test/serverpod_test.dart'; ❌ +``` + +### Do + +```dart +// Only import the generated test tools file. +// It re-exports all helpers and types that are needed. +import 'serverpod_test_tools.dart'; ✅ +``` + +### Database clean up + +Unless configured otherwise, by default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` (see [the configuration options](the-basics#rollback-database-configuration) for more info on this behavior). + +### Don't + +```dart +withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); + }); + + tearDown(() async { + await Product.db.deleteWhere( ❌ // Unnecessary clean up + session, + where: (_) => Constant.bool(true), + ); + }); + + // ... +}); +``` + +### Do + +```dart +withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + setUp(() async { + await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); + }); + + ✅ // Clean up can be omitted since the transaction is rolled back after each by default + + // ... +}); +``` + +## Calling endpoints + +While it's technically possible to instantiate an endpoint class and call its methods directly with a Serverpod `Session`, it's advised that you do not. The reason is that lifecycle events and validation that should happen before or after an endpoint method is called is taken care of by the framework. Calling endpoint methods directly would circumvent that and the code would not behave like production code. Using the test tools guarantees that the way endpoints behave during tests is the same as in production. + +### Don't + +```dart +void main() { + // ❌ Don't instantiate endpoints directly + var exampleEndpoint = ExampleEndpoint(); + + withServerpod('Given Example endpoint', ( + sessionBuilder, + _ /* not using the provided endpoints */, + ) { + var session = sessionBuilder.build(); + + test('when calling `hello` then should return greeting', () async { + // ❌ Don't call and endpoint method directly on the endpoint class. + final greeting = await exampleEndpoint.hello(session, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); +} +``` + +### Do + +```dart +void main() { + withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { + var session = sessionBuilder.build(); + + test('when calling `hello` then should return greeting', () async { + // ✅ Use the provided `endpoints` to call the endpoint that should be tested. + final greeting = + await endpoints.example.hello(session, 'Michael'); + expect(greeting, 'Hello, Michael!'); + }); + }); +} +``` + +## Unit and integration tests + +It is significantly easier to navigate a project if the different types of tests are clearly separated. + +### Don't + +❌ Mix different types of tests together. + +### Do + +✅ Have a clear structure for the different types of test. Serverpod recommends the following two folders in the `server`: + +- `test/unit`: Unit tests. +- `test/integration`: Tests for endpoints or business logic modules using the `withServerpod` helper. diff --git a/versioned_docs/version-2.3.0/06-concepts/18-testing/_category_.json b/versioned_docs/version-2.3.0/06-concepts/18-testing/_category_.json new file mode 100644 index 00000000..82138d51 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/18-testing/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Testing", + "collapsed": true +} \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/06-concepts/19-experimental.md b/versioned_docs/version-2.3.0/06-concepts/19-experimental.md new file mode 100644 index 00000000..4d742ceb --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/19-experimental.md @@ -0,0 +1,89 @@ +# Experimental features + +:::warning +Experimental features should not be used in production environments, as their stability is uncertain and they may receive breaking changes in upcoming releases. +::: + +"Experimental Features" are cutting-edge additions to Serverpod that are currently under development or testing. These features allow developers to explore new functionalities and provide feedback, helping shape the future of Serverpod. However, they may not be fully stable or complete and are subject to change. + +By default, experimental features are disabled. To opt into using them, include the `--experimental-features` flag when running the serverpod command: + +```bash +$ serverpod generate --experimental-features=all +``` + +The current options you can pass are: + +|**Feature**|Description| +|:-----|:---| +| **all** | Enables all available experimental features. | +| **inheritance** | Allows using the `extends` keyword in your model files to create class hierarchies.| + +## Inheritance + +:::warning +Adding a new subtype to a class hierarchy may introduce breaking changes for older clients. Ensure client compatibility when expanding class hierarchies to avoid deserialization issues. +::: + +Inheritance allows you to define class hierarchies in your model files by sharing fields between parent and child classes, simplifying class structures and promoting consistency by avoiding duplicate field definitions. + +### Extending a Class + +To inherit from a class, use the `extends` keyword in your model files, as shown below: + +```yaml +class: ParentClass +fields: + name: String +``` + +```yaml +class: ChildClass +extends: ParentClass +fields: + int: age +``` + +This will generate a class with both `name` and `age` field. + +```dart +class ChildClass extends ParentClass { + String name + int age +} +``` + +### Sealed Classes + +In addition to the `extends` keyword, you can also use the `sealed` keyword to create sealed class hierarchies, enabling exhaustive type checking. With sealed classes, the compiler knows all subclasses, ensuring that every possible case is handled when working with the model. + +```yaml +class: ParentClass +sealed: true +fields: + name: String +``` + +```yaml +class: ChildClass +extends: ParentClass +fields: + age: int +``` + +This will generate the following classes: + +```dart +sealed class ParentClass { + String name; +} + +class ChildClass extends ParentClass { + String name; + int age; +} +``` + +:::info +All files in a sealed hierarchy need to be located in the same directory. +::: diff --git a/versioned_docs/version-2.3.0/06-concepts/_category_.json b/versioned_docs/version-2.3.0/06-concepts/_category_.json new file mode 100644 index 00000000..8f4e3949 --- /dev/null +++ b/versioned_docs/version-2.3.0/06-concepts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Reference", + "collapsed": true +} diff --git a/versioned_docs/version-2.3.0/07-deployments/01-deployment-strategy.md b/versioned_docs/version-2.3.0/07-deployments/01-deployment-strategy.md new file mode 100644 index 00000000..68e57700 --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/01-deployment-strategy.md @@ -0,0 +1,52 @@ +# Choosing deployment strategy + +There are different options for hosting Serverpod. The minimal requirements are a single server or a serverless managed platform like Google Cloud Run and a Postgres database. Which setup you choose depends on the requirements of your architecture. + +The main two options are running Serverpod on a cluster of servers or on a serverless platform. You must run your servers on a cluster of servers (such as Google Cloud Engine) if your servers have a state. If they are stateless, you can run on a serverless platform (such as Google Cloud Run). An example of a stateful server is [Pixorama](https://pixorama.live), where the server keeps the state up to date in real time in the server's memory. If you only make API calls to retrieve data from a database, running on a serverless platform may be your best option. + +Here are some pros and cons for the different options: + +| | Server cluster | Serverless | +| :--- | :--------| :--------- | +| Pros | All features are supported. Great for real time communication. Cost efficient at scale. | Minimal starting cost. Easier configuration. Minimal maintenance. | +| Cons | Slightly higher starting cost. More complex to set up. | Limited feature set. The server cannot have a state. | + +The features that currently are not supported by the serverless option are: + +- Future calls. (Configuration is possible but requires a more advanced setup.) +- Health metrics. +- On-server caching. Caching on the server can still occur when serverless instances are kept alive but can be lost at any time. Caching with Redis is supported. +- State. You cannot store any global information in the server's memory. Instead, you must rely on external services such as Postgres, Redis, or other APIs. + +## Supported platforms + +We provide Terraform scripts for setting up your infrastructure with Google Cloud Platform or Amazon Web Services. Still, you can run Serverpod anywhere you can run Dart or host a Docker container. + +### Server cluster + +Serverpod's Terraform scripts will set up an auto-scaling group of servers and configure a database, load balancer, domain names, and certificates. Optionally, you can deploy a staging environment and additional services such as Redis and buckets for file uploads. You deploy new revisions through Github actions, where you can also set up continuous testing. + +These are approximate starting pricing for the primary required services of a minimal setup on Google Cloud Platform. The minimal setup can handle a fair amount of users at no additional cost. With more traffic, the price will be higher but typically scale well. In addition, with a server cluster you can cache data and state directly on your servers which can cut down costs as you scale. + +| Service | Min cost | +| :----------------------- | :------- | +| Compute Engine Instance | $7 / mo | +| Cloud Load Balancing | $19 / mo | +| Cloud SQL for PostgreSQL | $10 / mo | + +### Serverless + +Serverpod runs well on serverless platforms such as Google Cloud run. We do not yet provide terraform scripts for Cloud Run, but it is easy to set up using the GCP console. You can upload new revisions from your command line. + +With Cloud Run, you only pay for handling the traffic you receive. There is no starting cost, and no extra load balancer is required. + +| Service | Min cost | +| :----------------------- | :------- | +| Cloud Run | $0 / mo | +| Cloud SQL for PostgreSQL | $10 / mo | + +:::warning + +The prices shown on this page are approximations and are meant to give you a rough idea of hosting costs. Additional costs may occur, and prices may change. Make sure to do your own research before deploying your infrastructure. + +::: diff --git a/versioned_docs/version-2.3.0/07-deployments/02-deploying-to-gce-terraform.md b/versioned_docs/version-2.3.0/07-deployments/02-deploying-to-gce-terraform.md new file mode 100644 index 00000000..3a2ac487 --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/02-deploying-to-gce-terraform.md @@ -0,0 +1,296 @@ +# Google Cloud Engine with Terraform + +Serverpod makes deploying your server to Cloud Engine easy using Github, Terraform, and Docker containers. Terraform will set up and manage your infrastructure while you use Github to build your Docker container and manage versions and deployments. Creating your project using `serverpod create` will automatically generate your deployment scripts. The default setup creates a minimal configuration, but you can easily modify the configuration to suit your needs. + +:::caution + +Using Serverpod's GCP deployment may incur costs to your GCP account. Serverpod's scripts are provided as-is, and we take no responsibility for any unexpected charges for using them. + +::: + +## Prerequisites + +To use the deployment scripts, you will need the following: + +1. A paid Google Cloud Platform account. +2. Terraform [Install Terraform](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli) +3. Your Serverpod project version controlled on Github. +4. A registered custom domain name (e.g., examplepod.com), or register one through _Cloud Domains_ in the GCP console. + +If you haven't used Terraform before, it's a great idea to go through their tutorial for GCP, as it will give you a basic understanding of how everything fits together. [Get started with Terraform and GCP](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started) + +:::info + +The top directory created by Serverpod must be the root directory of your Git repository. Otherwise, the deployment scripts won't work correctly. + +::: + +:::tip + +Registering your domain through Cloud Domains in the GCP console allows you to create a hosted zone simultaneously. It also makes it easier to verify your domain, and you can skip a few of the steps below. If you use Cloud Domains, register the domain after the step where you create your service account. + +::: + +## What will be deployed? + +The deployment scripts will set up everything you need to run your Serverpod, including an autoscaling cluster of servers, a load balancer, a Postgres database, Redis (optional), Cloud Storage buckets for file uploads, Artifact Registry for your Docker container, a private network, DNS records, and SSL certificates. Some of the components are optional, and you will need to opt in. You can also create a second server cluster for a staging environment. Staging servers allow you to test your code before you deploy it to the production servers. + +You deploy your configuration with a domain name; the scripts will set up subdomains that provide access to different functions of your deployment: + +- _api.examplepod.com_: This is the entry point for your Serverpod's API server. +- _app.examplepod.com_: The Serverpod web server. If you prefer to host it on your top domain and use _www_ as a redirect, you can change this in the main Terraform configuration file. +- _insights.examplepod.com_: Provides an access point to Serverpod Insights, Serverpod's companion app. +- _database.examplepod.com_: This is how you access your database from outside the server. +- _storage.examplepod.com_: Access to public storage used by Serverpod. + +## Create a new GCP project with a service account + +Your deployment will live in a new GCP project that you create. Sign in to your [Google Cloud Console](https://console.cloud.google.com/), click the project selector and then the _New Project_ button. Enter the name of your new project and take note of the _Project ID_; you will need it when setting up your deployment scripts. + +![Create GCP Project](/img/gcp/0-create-project.jpg) + +### Create service account + +Next, you must create a service account allowing Terraform to access your account and set up the infrastructure for your Serverpod. + +Select your newly created project if it isn't already selected. Then, navigate to _IAM & Admin_ > _Service Accounts_. Click on the _Create Service Account_ button. + +Enter a name for your service account and take note of the email address associated with it. Click _Create and Continue_. + +Next, you will need to add two roles to your service account. _Basic_ > _Editor_ and _Service Networking_ > _Service Networking Admin_. These permissions will give Terraform the access it needs to create your infrastructure. When you've added the permissions, click _Continue_. + +![Assign roles to the service account](/img/gcp/1-assign-roles.jpg) + +Finally, click _Done_ to finalize the service account creation. + +### Download service account keys + +To be able to use your service account with Terraform, you will need to create a set of keys. Click on your newly created service account, then select the _Keys_ tab. Click _Add Key_ > _Create a New Key_. Select _JSON_ as the key type and click _Create_. + +![Create private keys](/img/gcp/2-private-key.jpg) + +The key is now downloaded to your computer. Rename the key to `credentials.json` and place it in your Serverpod's server directory under `deploy/gcp/terraform_gce`. E.g., the whole path would be something like `myproject_server/deploy/gcp/terraform_gce/credentials.json`. + +### Enabling APIs + +To deploy your serverpod, you must enable a set of APIs on Google Cloud. You can find which APIs are enabled or enable new ones by going to _APIs & Services_ > _Enabled APIs & Services_. These are the APIs that you should enable: + +- Artifact Registry API (artifactregistry.googleapis.com) +- Certificate Manager API (certificatemanager.googleapis.com) +- Cloud DNS API (dns.googleapis.com) +- Cloud Resource Manager API (cloudresourcemanager.googleapis.com) +- Cloud SQL Admin API (sqladmin.googleapis.com) +- Compute Engine API (compute.googleapis.com) +- Service Networking API (servicenetworking.googleapis.com) + +## Set up your domain name + +The Terraform script automatically handles your subdomains, but you must manually set up your domain zone in Google Cloud Console. This setup is also helpful if you want to add other things to your domain, such as email, or associate your domain with a website not hosted by Serverpod. + +### Register your domain + +__If you already have a domain that you want to use, you should skip this step and continue at: [Create a DNS zone](#create-a-dns-zone)__ + +Start by activating the required APIs for managing your domain. First, navigate to _Network services_ > _Cloud DNS_ and activate the service. Then navigate to _Network services_ > _Cloud Domains_ and activate it. + +Once _Cloud Domains_ is active, click the _Register Domain_ button. Search for the domain name you want to use and add it to your cart. + +In the DNS configuration, let Google's DNS servers manage the domain and connect it to a new DNS zone. Follow the steps to verify your email with Google Domains if needed. + +Your domain will automatically be verified with Google, but you must add your service account email as verified by Google's Webmaster Central. This step is required to be able to create SSL certificates for your domain. + +Go to the Google Webmaster Central: [https://www.google.com/webmasters/verification](https://www.google.com/webmasters/verification) + +Select your newly registered domain. Then, click _Add an owner_. Enter the email from the service account that you created earlier. + +Now, skip ahead to [Deploy your Serverpod code](#deploy-your-serverpod-code) + +### Create a DNS zone + +Go to _Network Services_ > _Cloud DNS_, then click _Create Zone_. Create a public zone for your domain name. Take note of the name you assign to the domain name zone, you will need it when you configure the Terraform scripts. + +![Create DNS zone](/img/gcp/3-dns-zone.jpg) + +To make the domain accessible, you must configure your registrar so that it points your domain to Google's domain name servers. Click the _NS_ entry to reveal the domain name servers to which you should point your domain. + +Now, head over to your domain name registrar and point the domain to Google's domain name servers. The domain name servers can take a while to update (worst case, up to a day, but it is usually much faster). + +![Configure domain registrar](/img/gcp/4-dns-forward.jpg) + +:::info + +Depending on your domain name registrar, the process for setting up your domain name servers may look slightly different. Also, note that the Google servers may have different names than those shown in the screenshot. + +::: + +You can test that the domain points to the correct name servers by running `dig` on the command line. It will output the domain name servers. + +```bash +$ dig +short NS examplepod.com +``` + +Should yield an output similar to this: + +```text +ns-cloud-b4.googledomains.com. +ns-cloud-b1.googledomains.com. +ns-cloud-b2.googledomains.com. +ns-cloud-b3.googledomains.com. +``` + +### Verify your domain + +Once your domain name zone is set up and has bubbled through the system, you must verify the domain with Google's Webmaster Central. This step is required to be able to create SSL certificates for your domain. + +Go to the Google Webmaster Central: +[https://www.google.com/webmasters/verification](https://www.google.com/webmasters/verification) + +Click on _Add a Property_, enter your domain name and press _Continue_. Now click the _Alternate methods_ tab and select _Domain name provider_. In the list that pops up, select _Google Domains_. A dialog showing the steps required to verify your domain will pop up. From the dialog, copy the highlighted TXT record. + +![Verify domain in Webmaster Central](/img/gcp/5-domain-verification.jpg) + +Now, open your Google Cloud Console and navigate to _Network Services_ > _Cloud DNS_. Select the DNS zone you created earlier, then click _Add Standard_ to create a new record set. Set the _Resource record type_ to _TXT_, then paste the code from the Webmaster Central in the _TXT data_ field and create the record set. In the Webmaster Central, you can now finish the verification by clicking _Verify_. + +![Add DNS record](/img/gcp/6-record-set.jpg) + +Finally, click on _Add additional owners_ and add the email from the service account you created earlier. Doing this will allow Terraform to make any required changes to your SSL certificates. + +## Deploy your Serverpod code + +Before creating our infrastructure, we must compile a Docker container with our Serverpod and deploy it to Google Cloud's Artifact Registry. The Docker container is compiled on Github and then pushed to the Artifact Registry using a Github action. + +### Create Artifact Registry repositories + +Open up the Google Cloud Console and navigate to _Artifact Registry_ > _Repositories_. Enable the API if needed. Click _Create Repository_. Set the _Name_ to `serverpod-production-container`, _Format_ to _Docker_, and _Mode_ to _Standard_. Select a _Region_ for your container. + +![Create repository in Artifact Registry](/img/gcp/7-artifact-repository.jpg) + +:::info + +The region you pick for your Artifact Registry repository must match the region you later choose for your project. The default is `us-central1`. + +::: + +Repeat the process and create a second container named `serverpod-staging-container`. + +### Configure Github + +Now that we have our Artifact Registry repositories, we can push code to it. Head to your Github repository for your project. Navigate to _Settings_ > _Secrets and variables_ > _Actions_. + +Click _New repository secret_. For the _Name_ enter `GOOGLE_CREDENTIALS`. For the _Secret_, copy the contents of the `gcp/terraform/credentials.json` file and paste its contents into the text field. + +Click _New repository secret_ again and enter `SERVERPOD_PASSWORDS` for the _Name_. Copy the contents of the `config/passwords.yaml` file and paste it into the _Secret_ text field. + +When you are done, _Repository secrets_ should look like this: + +![Add DNS record](/img/gcp/8-secrets.jpg) + +Finally, we must configure the Github action that compiles our Docker container. It's located in `.github/workflows/deployment-gcp.yml`. Open it in your favorite editor and update the values under _PROJECT_, _REGION_, and _ZONE_ with your _Project ID_ and the zone and region where you plan to deploy your server. + +Push your changes to Github, and you are ready to deploy your Docker container. + +### Deploy your Docker container + +Open your project on Github and navigate to _Actions_. On the right-hand side, click on the _Deploy to GCP_ item. The action will open up, and you can run it. Click on _Run workflow_, select the branch where you have pushed your code, set the _Target_ to _production_, and run the workflow. + +![Run Github workflow](/img/gcp/9-run-workflow.jpg) + +It will take a few moments for the workflow to show up and another minute or two to complete. When completed, it should have a green tick mark next to it. + +:::info + +In the future, just run the Github action whenever you want to release a new version of your server. You can also trigger the action by pushing code to the `deployment-gcp-production` or `deployment-gcp-staging` branches. + +::: + +## Create your infrastructure + +You now have everything you need to start creating your infrastructure. Start by configuring your Terraform scripts, then apply the configuration. + +### Configure Terraform + +You can find the configuration file for your Terraform project in your server's `deploy/gcp/terraform_gce/config.auto.tfvars` file. It is pretty self-explanatory; you only need to enter your _Project ID_ and the _Service account email_, the name of your _DNS zone_, and the _domain name_ you are deploying to. You got the details when completing the steps above, or you can find them in the Google Cloud Console. + +If you want to do more detailed configurations, you can do so in the `main.tf` file. The `main.tf` file refers to the `google_cloud_serverpod_gce` module, which handles most of the infrastructure. It contains some comments that explain how to use it, but you can also find the complete documentation [here](https://github.com/serverpod/google_cloud_serverpod_gce). + +### Configure Serverpod + +Serverpod uses different configuration files depending on whether you run your server locally, in staging, or in production. You find the configuration files in the server's `config` directory. You will need to edit the `config/production.yaml` file to match your setup. Typically, the only thing you need to change is the name of your domain. + +:::info + +If you are deploying a staging server in addition to your production server, you must edit the `config/staging.yaml` file too. + +::: + +### Deploy your infrastructure + +Once you have configured Terraform and your Serverpod, you are ready to deploy your infrastructure. Make sure that you have `cd` into your `deploy/gcp/terraform_gce` directory. Now run: + +```bash +$ terraform init +``` + +This will download the Serverpod module and initialize your Terraform configuration. Now, deploy your infrastructure by running: + +```bash +$ terraform apply +``` + +Terraform will ask you for the password to your production and staging database. You will find the passwords in your `config/passwords.yaml` file. If you are not deploying a staging server, you can leave the staging database password blank. + +Answer `yes` when Terraform asks you if you are ready to perform the changes. Deploying the complete infrastructure will take around 15 minutes, making this an excellent coffee break time. + +## Create database tables + +As a final step in your GCP deployment, you must create your database tables. Terraform has created a database with a public IP number that you can access from `database..com`. However, access is limited to connections from specific IP addresses for security reasons. + +Open the Google Cloud Console, navigate to _SQL_, and click on the `serverpod-production-database` instance. Click _Connections_ > _Networking_. Under _Public IP_ click _Add Network_. Enter the name of the place you are connecting from, e.g. _Office_ or _Home_, then enter your public IP number in the _Network_ field. + +![Add network to database](/img/gcp/10-add-network.jpg) + +:::tip + +If you are unsure what your public IP number is, Google `what is my IP`, and Google will tell you. + +::: + +Now, you can connect to your database. Use your favorite database managing tool (we use [Postico](https://eggerapps.at/postico2/)). To connect, use the password from the `config/passwords.yaml` file and the public database address (`database..com`). For _database_ enter `serverpod` and _user_ should be set to `postgres`. + +![Connect to database](/img/gcp/11-connect-database.jpg) + +Once connected, run the database definition query in the latest migration directory `migrations//definition.sql`. When you have added the tables for the database, Serverpod should be able to start. However, it could take an hour before the correct SSL certificates are created and validated. + +## Deploying new versions + +Once your infrastructure is set up, deploying new versions of your server code is easy. Push your updated code to Github and run the deployment action. Doing this will compile your code and upload it to the repository in Artifact Registry. + +Once your code is uploaded to the Artifact Registry you can restart the servers in your auto scaling group by opening the Google Cloud console and navigating to _Compute Engine_ > _Instance Groups_. Click on the _serverpod-production-group_, then _Restart/Replace VMs_. + +:::info + +You can also set up your deployment script on Github to automatically restart the servers when new code is deployed. To do this, uncomment the last section of the script in `.github/workflows/deployment-gcp.yml`, push the change to Github, and run the action. + +::: + +## Troubleshooting + +Deploying your infrastructure is somewhat involved, and you can get stuck along the way. These are a few tips on debugging your issues: + +### Access the instance logs + +You can find the logs from your running instances by navigating to _Compute Engine_ > _VM instances_. Click on one of the instances, then click on _Logging_. It will list the output from starting the instance and logs outputted by the Docker container. + +### Log into the instance + +You can log into your running instances by clicking the small _SSH_ button next to the instance row. For this to work, make sure you have set `enable_ssh` to `true` in your Terraform module (this is the default). + +Once logged in, you can check that your Docker container is up by running `docker ps`. + +### Some things take time + +Some tasks described above can take time to bubble through the system. This is particularly true for working with DNS names and certificates. Sometimes they can take hours or up to a day to update. + +### If you get stuck + +You can always find help in our [support forum](https://github.com/serverpod/serverpod/discussions) on Github. diff --git a/versioned_docs/version-2.3.0/07-deployments/03-deploying-to-gcr-console.md b/versioned_docs/version-2.3.0/07-deployments/03-deploying-to-gcr-console.md new file mode 100644 index 00000000..cdd5991d --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/03-deploying-to-gcr-console.md @@ -0,0 +1,102 @@ +# Google Cloud Run with CGP Console + +If your server does not maintain a state and you aren't using future calls, running your Serverpod on Google Cloud Run can be a great option. Cloud Run is the easiest way to deploy your server but may be less flexible as your application grows. Check the [Choosing deployment strategy](deployment-strategy) page for more information on choosing the best solution for your needs. + +## Before you begin + +Before you begin, you will need to install and configure the Google Cloud CLI tools. + +- Create a new project with billing enabled. Learn how to check if billing is enabled [here](https://cloud.google.com/billing/docs/how-to/verify-billing-enabled) +- [Install](https://cloud.google.com/sdk/docs/install) the Google Cloud CLI. +- To [initialize](https://cloud.google.com/sdk/docs/initializing) the gcloud CLI, run the following command: + +```bash +$ gcloud init +``` + +- To set the default project for your Cloud Run service: + +```bash +$ gcloud config set project +``` + +## Setup the database + +Before deploying your server, you must give it access to a Postgres database. Navigate to SQL, activate the API, then click _Create Instance_. Choose _PostgreSQL_. Pick a name for the database. + +There are many configurations you can make, pick the ones that are best for your project, but make sure to: + +- Use the production database password from your `config/passwords.yaml` file for the admin password. +- Use database version: PostgreSQL 14. +- Remember the region that you pick (you will use the same region for Cloud Run). +- Under _Customize your instance_ > _Connections_, make sure that _Public IP_ is enabled. + +When you are happy with your choices, click _Create Instance_. Creating your database can take up to 20 minutes, so this is a good time for a quick coffee break. + +### Create database user and database tables + +When the Postgres instance creation has finished, you must add a database and an approved IP number you can connect from to access the database. Click on your database instance to open up its settings. + +- Click _Databases_ > _Create Database_. For _Database name_, enter `serverpod`. +- Click _Connections_ > _Networking_. Then, click _Add Network_. Enter a name for where you will be connecting from, e.g., _Home_ or _Office_. For _Network_ enter your public IP number. If you are not sure, you can Google _what is my IP_ to find out. + +Now you can connect to your database with your favorite Postgres tool. Postico is a good option if you are on a Mac. Click on the _Overview_ tab of the database and take note of the _Public IP address_. Use it, together with the user `postgres`, the database `serverpod`, and the password from your `passwords.yaml` file to connect to the database. + +Run the database definition query from the latest migration directory `migrations//definition.sql`. + +## Create a service account + +For Cloud Run to access your database, you will need to create a service account with the _Cloud SQL Client_ role. + +- Navigate to _IAM & Admin_ > _Service Accounts_. +- Click on _Create Service Account_. +- Choose a name for the account, e.g., _Cloud Run_. +- Add the _Basic_ > _Editor_ role and the _Cloud SQL_ > _Cloud SQL CLient_ role. Click _Continue_ and then _Done_. + +Take note of the email of the newly created service account. You will need it when you deploy your server. + +## Configure Serverpod + +You will connect to Postgres from Cloud Run with the Cloud SQL Proxy. In your Postgres instance's _Overview_ page, copy the _Connection name_. + +Open the `config/production.yaml` file. Under `database`, replace the host with the following string, but replace the connection name that you copied in the previous step: `/cloudsql//.s.PGSQL.5432`. Also, add the `isUnixSocket` option and set it to `true`. Your configuration should look something like this: + +```yaml +database: + isUnixSocket: true + host: /cloudsql/my-project:us-central1:database-name/.s.PGSQL.5432 + port: 5432 + name: serverpod + user: postgres +``` + +## Deploy your server + +Your server is now ready to be deployed. When you created your project, Serverpod also created a script for deploying your server. Copy it to the root of your server directory and make it executable. Make sure you are in your server directory (e.g., `myproject_server`). Then run the following command: + +```bash +$ cp deploy/gcp/console_gcr/cloud-run-deploy.sh . +$ chmod u+x cloud-run-deploy.sh +``` + +Open up the script in your favorite editor. You will need to fill in your _database instance's connection name_ and the _email of your service account_. + +Now, deploy your server by running the following: + +```bash +$ ./cloud-run-deploy.sh +``` + +The script runs two deployment commands, one for your API and one for the Insights API used by the Serverpod app. While running, it may ask you to enable the Cloud Run and SQL Admin services. Answer yes to all these questions. + +It will take a minute or two for the deployment to complete. Afterward, you can access your server through the URLs printed on the command line. + +:::tip + +You can deploy a new version of your server at any time by running `./cloud-run-deploy.sh` again. + +::: + +## Next steps + +You may want to assign a custom domain name to your Cloud Run instances. You can manage domain name mappings in the Cloud Run Console. There you can also add a Redis instance (you can find it under _Integrations_). Redis allows you to cache data and communicate between servers. diff --git a/versioned_docs/version-2.3.0/07-deployments/04-deploying-to-aws.md b/versioned_docs/version-2.3.0/07-deployments/04-deploying-to-aws.md new file mode 100644 index 00000000..6cc5b827 --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/04-deploying-to-aws.md @@ -0,0 +1,289 @@ +# AWS EC2 with Terraform + +Serverpod makes it easy to deploy your server to AWS using Github and Terraform. Terraform will set up and manage your infrastructure while you use Github actions to manage versions and deployments. Creating your project using `serverpod create` Serverpod will automatically generate your deployment scripts. The default setup uses a minimal configuration that will fit within the AWS free tier, but you can easily modify the configuration to suit your needs. + +:::caution + +Using Serverpod’s AWS deployment may incur costs to your AWS account. Serverpod’s scripts are provided as-is, and we take no responsibility for any unexpected charges for using them. + +::: + +## Prerequisites + +To use the deployment scripts, you will need the following: + +1. An AWS account. It may take up to 24 hours to get your AWS account up and running. +2. AWS CLI, configured with your credentials. [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +3. Terraform. [Install Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) +4. Your Serverpod project version controlled on Github. + +If you haven’t used Terraform before, it’s a great idea to go through their tutorial for AWS, as it will give you a basic understanding of how everything fits together. [Get started with Terraform and AWS](https://learn.hashicorp.com/collections/terraform/aws-get-started) + +:::info + +The top directory created by Serverpod must be the root directory of your Git repository. Otherwise, the deployment scripts won’t work correctly. + +::: + +## What will be deployed? + +The deployment scripts will set up everything you need to run your Serverpod, including an autoscaling cluster of servers, load balancers, a Postgres database, Redis, S3 buckets for file uploads, CloudFront, CodeDeploy, DNS records, and SSL certificates. Some of the components are optional, and you will need to opt in. You can also create a second server cluster for a staging environment. The staging servers allow you to test code before you deploy it to the production servers. + +You deploy your configuration with a domain name; the scripts will set up subdomains that provide access to different functions of your deployment: + +- _api.examplepod.com:_ This is the entry point for your main Serverpod server. +- _app.examplepod.com:_ The Serverpod web server. If you prefer to host it on your top domain and use _www_ as a redirect, you can change this in the main Terraform configuration file. +- _insights.examplepod.com:_ Provides access to the Serverpod UI and gets insights from your server while it is running. +- _database.examplepod.com:_ This is how you access your database from outside the server. +- _storage.examplepod.com:_ Access to public storage used by Serverpod. + +## Set up your domain name and certificates + +Before deploying your server, you must configure your server’s top domain in AWS. You can register your domain through any registrar, but you need to set up a public hosted zone in Route 53 in AWS. + +1. Sign in to the AWS console and navigate to _Route 53 > Hosted zones_. +2. Click _Create hosted zone_. +3. Enter your domain name and click _Create hosted zone_. +4. The console will display a number of DNS names for Amazon’s name servers. You will need to have your domain registrar point to these name servers. Depending on your registrar, this process will be slightly different. +5. Expand the _Hosted zone details_ and take note of your _Hosted zone ID_ (you will need it in your Terraform configuration later). + +![AWS hosted zone record](/img/1-hosted-zone.jpg) + +_Finding the domain name servers for your hosted zone._ + +Next, you need to create two SSL certificates for your domain. Navigate to _AWS Certificate Manager_. Here it’s important in which regions you create your certificates. We are deploying to _Oregon (us-west-2)_ in this example, but you can deploy to any region. + +1. In the top-right corner, choose your preferred region, then click the _Request_ button to request a new certificate. +2. Choose to request a public certificate. +3. For the Fully qualified domain name, enter your domain name. +4. Click _Add another name to this certificate_. +5. Enter a wildcard for any subdomain. E.g., *.examplepod.com +6. Validate the domain using DNS validation. AWS can automatically create the required entries in Route 53. Just follow the instructions. +7. Save the ARN of your newly created certificate (you will need it in your Terraform configuration later). + +![Request certificate](/img/2-request-certificate.jpg) + +_Create a wildcard certificate for your domain._ + +Finally, you must create a second wildcard certificate in the _US East N. Virginia (us-east-1)_ region. AWS Cloudfront can only access certificates created in the _us-east-1_ region. Change the region, and repeat the steps from when you created your first certificate. Save the ARN of your second certificate. + +## Configure Github + +To allow Github to manage deployments to your AWS server cluster, you need to provide it with access keys and our `passwords.yaml` file from the Serverpod project. You can use the same AWS access keys as you used to configure AWS CLI or generate a new pair. + +Sign in to Github and find your project’s settings. Navigate to _Secrets > Actions_. Here you need to add three secrets for deployment to work correctly. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are your AWS credentials. Github also needs access to your `config/passwords.yaml` file. Copy the contents of the passwords file and add it to `SERVERPOD_PASSWORDS`. + +![Github secrets](/img/3-github-secrets.jpg) + +_Your Github Action secrets after they have been configured._ + +## Configure Dart Version + +You should run your deployment using the same dart version as your set-up locally. There are 2 different places that you need to specify for your AWS deployment: + +1. In file `.github/workflows/deployment-aws.yml`, under the steps **Setup Dart SDK**, you can set your Dart SDK version. +It is sufficient to specify to minor version. For example, if you are using Dart `3.5.1`, you can write `3.5`. + + ```yaml + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1.6.5 + with: + sdk: ${MINOR_DART_SDK_VERSION} + ``` + +1. In file `mypod_server/deploy/aws/scripts/install_dependencies`, you can specify on top of the page the version of Dart SDK that you are using after `DART_VERSION=`. +For example if you are using Dart 3.5.1, you can specify like the following: + + ```bash + #!/bin/bash + DART_VERSION=3.5.1 + + ``` + +:::caution + +For users who generated the project with the Serverpod CLI version `<=2.0.2`. You can upgrade dart by adding a few lines the code under `mypod_server/deploy/aws/scripts/install_dependencies`. +You can copy the lines of code needed as indicated in the following code block. + +```bash +#!/bin/bash + +#### COPY THE CODE FROM HERE +DART_VERSION=3.5.1 + +# Uncomment the following code if you have already generated the project with the older version of serverpod cli +# What this code do is to remove our previous way of setting dart version in the launch template +if [ -f "/etc/profile.d/script.sh" ]; then + sudo rm /etc/profile.d/script.sh +fi + +## install specified dart version if it is not present on the machine + +if [ ! -d "/usr/lib/dart$DART_VERSION" ]; then + wget -q https://storage.googleapis.com/dart-archive/channels/stable/release/$DART_VERSION/sdk/dartsdk-linux-x64-release.zip -P /tmp + cd /tmp || exit + unzip -q dartsdk-linux-x64-release.zip + sudo mv dart-sdk/ /usr/lib/dart$DART_VERSION/ + sudo chmod -R 755 /usr/lib/dart$DART_VERSION/ + rm -rf dartsdk-linux-x64-release.zip +fi + +## make symlink to use this dart as default +sudo ln -sf "/usr/lib/dart$DART_VERSION/bin/dart" /usr/local/bin/dart + +#### STOP COPYING THE CODE FROM HERE + +#### THE FOLLOWING SHOULD BE THE SAME AS THE PREVIOUS CODE +cat > /lib/systemd/system/serverpod.service << EOF +[Unit] +Description=Serverpod server +After=multi-user.target + +[Service] +User=ec2-user +WorkingDirectory=/home/ec2-user +ExecStart=/home/ec2-user/serverpod/active/mypod_server/deploy/aws/scripts/run_serverpod +Restart=always + +[Install] +WantedBy=muti-user.target +WantedBy=network-online.target +EOF + +systemctl daemon-reload +``` + +This install the Dart SDK on the machine and change the default Dart SDK version on the machine + +::: + +## Configure Serverpod + +You acquired a hosted zone id and two certificate ARNs from AWS in the previous steps. You will need these to configure your Serverpod deployment scripts. You find the Terraform scripts in your server’s `aws/terraform` folder. Open up the `config.auto.fvars` file. Most of the file is already configured, including your project’s name. You will need to fill in the variables: `hosted_zone_id`, `top_domain`, `certificate_arn`, and `cloudfront_certificate_arn`. + +:::info + +If you deploy your servers in a region other than Oregon (us-west-2), you will need to update the `instance_ami` variable. Instructions are in the configuration file. In addition, you will also need to update the region in your Github deployment file located in `.github/workflows/deployment-aws.yml`. + +::: + +By default, the Terraform scripts are configured to use a minimal setup to reduce costs for running your Serverpod. You can quickly turn on additional features, such as enabling Redis or adding a staging server by changing values in the script. You can also change these values later and redo the deployment step. + +Finally, to complete your Serverpod configuration, you will need to edit the `config/staging.yaml` and `config/production.yaml` files. In these files you should: + +1. Replace the `examplepod.com` domain with the domain you are using for your server. + +2. Replace the database with the database url from the RDS. Replace the url with the corresponding environment yaml file in `mypod_server/config` under the `database.host` section. The following command should help you retrieve the URL for your database. + +```bash +aws rds describe-db-instances --db-instance-identifier ${YOUR_DB_INSTANCE_ID} | jq ".DBInstances.[0].Endpoint.Address" -r +``` + +:::info + +Ensure that you have ssl enabled for the corresponding environment as RDS enable ssl by default. +You can do so by adding `requireSsl: true` in your config file in `server/config/production.yaml` and/or `server/config/staging.yaml` + +Example: + +```yaml +database: + host: redis.private-production.examplepod.com + port: 5432 + name: serverpod + user: postgres + requireSsl: true +``` + +::: + +## Deploy your infrastructure + +Your Serverpod should now be configured and ready to be deployed. Exciting times! Open up a terminal and `cd` into your server `aws/terraform` directory. First, you need to add an environment variable so that Terraform can correctly set the password for the Postgres database. You will find the production password for the database in your `config/passwords.yaml` file. + +```bash +$ export TF_VAR_DATABASE_PASSWORD_PRODUCTION="" +``` + +Next, we are ready to initialize Terraform and deploy our server. You will only need to run the `terraform init` command the first time you deploy the configuration. + +```bash +$ terraform init +$ terraform apply +``` + +Terraform will now run some checks and make a plan for the deployment. If everything looks good, it will ask you if you are ready to go ahead with the changes. Type `yes` and hit the return key. Applying the changes can take up to five minutes as AWS creates the different resources needed for your Serverpod. + +## Create database tables + +For your Serverpod to function correctly, you will need to create its required database tables and any tables specific to your setup. The database queries needed to set up your tables are found in the latest migration `migrations//definition.sql`. The `definition.sql` file configures all tables required by your project. Use your favorite tool to connect to the database ([Postico](https://eggerapps.at/postico/) is a good option if you are on a Mac). Connect to `database.examplepod.com` (replace `examplepod.com` with your domain); the user is `postgres`, the port is 5432, and the database is `serverpod`. Use the production password from the `config/password.yaml` file. + +![Github secrets](/img/5-database-connect.jpg) + +_Connect to the database with Postico._ + +## Deploy your code + +:::caution + +Using an old version of Serverpod cli will generate the Github action file containing old dart version that might not be the one you are using. +You can fix this by the following example. In the example, we are using the dart version v3.5.1. You can adjust to the dart version that you are using. + +1. In `./github/workflows/deployment-aws.yml` use + +```yaml + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1.6.5 + with: + sdk: 3.5 +``` + +1. In `server/deploy/aws/terraform/init-script.sh` change the version of dart installed on the machine. + +```bash +wget -q https://storage.googleapis.com/dart-archive/channels/stable/release/3.5.1/sdk/dartsdk-linux-x64-release.zip +``` + +::: + +We now have our servers, load balancers, database, and other required infrastructure. The only missing part is that our code is not yet up and running. There are two ways to deploy the code from our Github project. We can either push the code to a branch called `deployment-aws-production` or manually trigger the deployment action from the Github webpage. + +:::info + +If you have set up a staging server, you can also push code to a branch called `deployment-aws-staging`. This will deploy your code to the staging environment. + +::: + +To manually trigger a deployment, navigate to your project on the Github webpage. Click _Actions > Deploy to AWS > Run workflow_. This will open up a small dialog that allows you to choose which branch you want to use and if you’re going to target the production or staging servers. Next, click _Run workflow_. This will trigger the action to deploy your code to your Serverpod. Usually, the deployment process takes around 30 seconds to complete. + +![Request certificate](/img/4-github-workflow.jpg) + +_Manually deploy your server with Github actions._ + +## Test your deployment + +Your Serverpod should now be up and running! To check if everything is working, open up your web browser and go to `https://api.examplepod.com`. (You should replace your `examplepod.com` with your own domain name.) If everything is correctly configured, you will get a message similar to this: + +OK 2022-05-19 14:29:16.974160Z + +## Troubleshooting and tips and tricks + +Chances are that if you followed the instructions, you have a Serverpod deployment that you won't have to touch very often. However, this section gives you some pointers on where to start looking if things go wrong. + +### Signing in to your instances + +You can find a list of your currently running EC2 instances by navigating to _EC2 > Instances_. Click on one of the instances to go to its summary page. From there, click _Connect_. On the _Connect to instance_ page, click _Connect_, and AWS will open up a console window with access to your EC2 instance. + +After Signing in to your instance, you should check if the service is running with `systemctl status serverpod.service` + +If the service is running, you can look into the serverpod error log in `serverpod.err` and server log in `serverpod.log` in the home directory. + +If all checks out, try to use `curl localhost:8080` to see if the service can be reached from local. If we get the expected response, we know that the service is running properly. + +### External dependencies and submodules + +The deployment scripts support using submodules and external dependencies. Place any such dependencies in a directory called `vendor` at the root of your Github project. + +### Troubleshooting deployments + +The deployment process involves a couple of steps. When you trigger a deployment from Github, the action will create a deployment and upload it to an S3 bucket. Then, CodeDeploy on AWS is triggered. You can find the logs from the Github action under the _Actions_ tab of your project. If the deployment process fails later, those logs are available on AWS by navigating to _CodeDeploy > Deploy > Deployments_. diff --git a/versioned_docs/version-2.3.0/07-deployments/05-general.md b/versioned_docs/version-2.3.0/07-deployments/05-general.md new file mode 100644 index 00000000..bc27776c --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/05-general.md @@ -0,0 +1,52 @@ +# Hosting elsewhere + +You can host Serverpod anywhere, running Dart directly or through a Docker container. This page contains helpful information if you want to deploy Serverpod on a custom platform. + +## Required services + +Serverpod will not run without a link to a Postgres database with the correct tables added (unless you're running Serverpod mini). Serverpod can also optionally use Redis. You enable Redis in your configuration files. + +## Configuration files + +Serverpod has three main configuration files, depending on which mode the server is running; `development`, `staging`, or `production`. The files are located in the `config/` directory. By default, the server will start in development mode. To use another configuration file, use the `--mode` option when starting the server. If you are running multiple servers in a cluster, use the `--server-id` option to specify the id of each server. By default, the server will run as id `default`. For instance, to start the server in production mode with id `2`, run the following command: + +```bash +$ dart bin/main.dart --mode production --server-id 2 +``` + +:::info + +It may be totally valid to run all servers with the same id. If you are using something like AWS Fargate it's hard to configure individual server ids. + +::: + +By default, Serverpod will listen on ports 8080, 8081, and 8082. The ports are used by the API server, Serverpod Insights, and Relic (Serverpod's web server). You can configure the ports in the configuration files. Most often, you will want to place your server or servers behind a load balancer that handles the SSL certificates for your server and maps the traffic to different domain addresses and ports (typically 443 for HTTPS). + +## Server roles + +Serverpod can assume different roles depending on your configuration. If you run Serverpod on a single server or a cluster of servers, you typically will want to use the default `monolith` role. If you use a serverless service, use the `serverless` role. When Serverpod runs as a monolith, it will handle all maintenance tasks, such as health checks and future calls. If you run it serverless, you will need to schedule a cron job to start Serverpod in the `maintenance` role once every minute if you need support for future calls and health checks. + +| Role | Function | +| :------------ | :------- | +| `monolith` | Handles incoming connections and maintenance tasks. Allows the server to contain a state. Default role. | +| `serverless` | Only handles incoming connections. | +| `maintenance` | Runs the maintenance tasks once, then exits. | + +You can specify the role of your server when you launch it by setting the `--role` argument. + +```bash +$ dart bin/main.dart --role serverless +``` + +## Docker container + +Running Serverpod through a Docker container is often the best option as it provides a well-defined environment. It's also easy to integrate into your build and deployment process and runs well on most platforms. + +You will get a `Dockerfile` created in your server directory when you set up a new project. The file works out of the box but can be tailored to your needs. The file has no build options, but you can define environment variables when running it. The following variables are supported. + +| Environment variable | Meaning | +| :------------------- | :------ | +| `runmode` | The run mode to start the server in, possible values are `development` (default), `staging`, or `production`. | +| `serverid` | Identifier of your server, default is `default` | +| `logging` | Logging mode at startup, default is `normal`, but you can specify `verbose` to get more information during startup which can help with debugging. | +| `role` | The role that the server will assume, possible values are `monolith` (default), `serverless`, or `maintenance`. | diff --git a/versioned_docs/version-2.3.0/07-deployments/_category_.json b/versioned_docs/version-2.3.0/07-deployments/_category_.json new file mode 100644 index 00000000..fd27356d --- /dev/null +++ b/versioned_docs/version-2.3.0/07-deployments/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Deploying Serverpod" +} diff --git a/versioned_docs/version-2.3.0/08-upgrading/01-upgrade-from-mini.md b/versioned_docs/version-2.3.0/08-upgrading/01-upgrade-from-mini.md new file mode 100644 index 00000000..126b6b88 --- /dev/null +++ b/versioned_docs/version-2.3.0/08-upgrading/01-upgrade-from-mini.md @@ -0,0 +1,11 @@ +# Upgrade from Mini to full + +If you have started with Serverpod Mini, you can upgrade to the full Serverpod version anytime. Before you upgrade, it's good practice to back up your project, as some configuration files can be overwritten if you have manually created them. + +To upgrade your project, change directory into your server, then run: + +```bash +serverpod create . +``` + +Serverpod will now add the configuration files required for the full version. diff --git a/versioned_docs/version-2.3.0/08-upgrading/02-upgrade-to-one-point-two.md b/versioned_docs/version-2.3.0/08-upgrading/02-upgrade-to-one-point-two.md new file mode 100644 index 00000000..501a3581 --- /dev/null +++ b/versioned_docs/version-2.3.0/08-upgrading/02-upgrade-to-one-point-two.md @@ -0,0 +1,267 @@ +# Upgrade to 1.2 + +Serverpod 1.2. is backward compatible with Serverpod 1.0 and Serverpod 1.1. There are a few changes to the database layer, meaning you probably want to use the new methods. The old methods still works, but have been deprecated and will be permanently removed with the upcoming version 2. + +Database migrations are new in Serverpod 1.2. You can still opt to manage your database manually, but it is recommended that you move to the new migration system. Using the new migration will make keeping your database up-to-date much easier. + +## Updating your CLI + +To update you Serverpod command line interface to the latest version, run: + +```bash +dart pub global activate serverpod_cli +``` + +You can verify that you have the latest version installed by running: + +```bash +serverpod version +``` + +## Updating your pubspecs + +To move to Serverpod 1.2, you will need to update the `pubspec.yaml` files of your `server`, `client`, and `flutter` directories. Anywhere `serverpod` is mentioned, change the version to `1.2.0` (or any later version of Serverpod 1.2). It is recommended to use explicit versions of the Serverpod packages, to make sure that they are all compatible. + +### Update to Dart 3 + +This update has bumped the minimum required dart version to `3.0.0`. You will have to change the Dart SDK version in all your `pubspec.yaml` files. + +Old pubspec.yaml configuration: + +```yaml +... +environment: + sdk: '>=2.19.0 <4.0.0' +``` + +Updated pubspec.yaml configuration: + +```yaml +... +environment: + sdk: '>=3.0.0 <4.0.0' +``` + +The `Dockerfile` in your project should be updated with the new Dart version: + +```docker +FROM dart:3.0 AS build + +... +``` + +After updating your `pubspec.yaml` files, make sure to run `dart pub update` on all your packages. You must also run `serverpod generate` in your `server` directory. + +## Deprecated methods + +In this version, we have completely reworked the database layer of Serverpod. The new methods have been placed under a static `db` field on the generated models. The old methods are still available, but the deprecation warnings will guide you toward moving to the updated API. + +:::important + +A few of the methods work slightly differently in their new versions. Most notably, the `insertRow` method will not modify the model you pass to it. Instead, it will return a modified copy with the inserted row `id`. + +::: + +```dart +// The new find method is a drop-in replacement. +Example.find(...); // old +Example.db.find(...); // new + +// The old findSingleRow method has changed name to findFirstRow but is otherwise a drop-in replacement. +Example.findSingleRow(...); +Example.db.findFirstRow(...); + +// The new findById method is a drop-in replacement. +Example.findById(...); // old +Example.db.findById(...); // new + +// The old delete method has been renamed to deleteWhere and now returns a list of ids of rows that was deleted. +Example.delete(...); +Example.db.deleteWhere(...); + +// The new findById method is a drop-in replacement but returns the id of the row deleted. +Example.deleteRow(...); // old +Example.db.deleteRow(...); // new + +// The old update method has been renamed too updateRow and now returns the entire updated object as a new copy. +Example.update(...); +Example.db.updateRow(...); + +// The old insert method has been renamed too insertRow. The object you pass in is no longer modified, instead a new copy with the added row is returned which contains the inserted id. This means no mutations of the input object. +Example.insert(...); +Example.db.insertRow(...); + +// The new count method is a drop-in replacement. +Example.count(...); +Example.db.count(...); +``` + +## Model changes + +We have made some improvements to the Serverpod model files (previously referred to as protocol files or serializable entities). By default, the model files are now located in the `lib/src/models/` directory, although using `lib/src/protocol` still works. + +When making the improvements to the model files, we made additions and changes to the syntax. All old keywords still work, but `serverpod generate` will give deprecation warnings, guiding you toward updating your models. The changes are listed below. + +The keyword `api` has been deprecated and replaced with the new keyword `!persist` as a drop-in replacement. + +Old syntax: + +```yaml +class: Example +table: example +fields: + name: String + apiField: String, api +``` + +New syntax: + +```yaml +class: Example +table: example +fields: + name: String + apiField: String, !persist +``` + +The keyword `database` has been deprecated and replaced with the new keyword `scope` with the value `serverOnly` as a drop-in replacement. + +Old syntax: + +```yaml +class: Example +table: example +fields: + name: String + serverField: String, database +``` + +New syntax: + +```yaml +class: Example +table: example +fields: + name: String + serverField: String, scope=serverOnly +``` + +The keyword `parent` has been moved and should be placed inside the new `relation` keyword, see the section on [relations](../concepts/database/relations/one-to-one) for the full new feature set. + +Old syntax: + +```yaml +class: Example +table: example +fields: + name: String + parentId: int, parent=example +``` + +New Syntax: + +```yaml +class: Example +table: example +fields: + name: String + parentId: int, relation(parent=example) +``` + +## Moved and renamed SQL file + +Serverpod has moved and renamed the generated SQL file for the complete database schema. Instead of the file `generated/tables.pgsql`, Serverpod now includes it as a part of each migration located in the `migrations` directory, under the name `definition.sql`. + +## Initialize the migration system + +If your project was created before migrations were introduced in Serverpod, you need to run a one-time setup to make it compatible with the new migration system. There are two guides to help you upgrade: one for projects that don't need to preserve any data and another for those that do. + +The guides assume that you have already installed Serverpod 1.2 and that you have a project created with an earlier version of Serverpod. + +### No data to preserve + +If it is not important to preserve the data that is in your database, you can simply remove the database and let the migration system create a new one. + +1. Generate the project. + + This ensures that the project is up to date with the latest version of Serverpod. Navigate to your project's `server` package directory and run the `generate` command. + + ```bash + $ serverpod generate + ``` + +2. Create a migration for your project. + + The migration system will create a migration as if the database needs to be initialized from scratch. Navigate to your project's `server` package directory and run the `create-migration` command. + + ```bash + $ serverpod create-migration + ``` + +3. Recreate database. + + In a Serverpod development project, the database is hosted in a docker container. To remove the existing database and start a new one run the following commands: + + ```bash + $ docker-compose down -v + $ docker-compose up --build --detach + ``` + + The command first removes the running container along with its volume and the second command starts a new database from scratch. + +4. Initialize database from migration. + + Initialize the database by applying the migration to it using the `--apply-migrations` flag when starting the server. + + ```bash + $ dart run bin/main.dart --apply-migrations + ``` + +### Data to preserve + +If your project already has data in the database that should be preserved, we can use the repair migration system to bring the project up to date with the migration system. + +1. Generate the project. + + This ensures that the project is up to date with the latest version of Serverpod. Navigate to your project's `server` package directory and run the `generate` command. + + ```bash + $ serverpod generate + ``` + +2. Create a migration for your project. + + The migration system will create a migration as if the database needs to be initialized from scratch. Navigate to your project's `server` package directory and run the `create-migration` command. + + ```bash + $ serverpod create-migration + ``` + +3. Create a repair migration. + + The repair migration system will create a repair migration that makes your live database schema match the newly created migration. To enable the command to fetch your live database schema it requires a running server. Navigate to your project's `server` package directory and start the server, then run the `create-repair-migration` command. + + ```bash + $ dart run bin/main.dart + $ serverpod create-repair-migration + ``` + + :::info + When starting the server, warnings will be displayed about the database schema not matching the target database schema. These warnings are expected and can safely be ignored when creating the repair migration. + ::: + + Use the `--mode` option to specify the database source to use. By default, the repair migration system connects to your `development` database using the information specified in your Serverpod config. + +4. Apply the repair migration to your database. + + To apply the repair migration to your database, restart the server using the `--apply-repair-migration` flag. + + ```bash + $ dart run bin/main.dart --apply-repair-migration + ``` + +## Closing remarks + +Your project is now compatible with the database migration system in Serverpod 1.2. + +Happy coding! diff --git a/versioned_docs/version-2.3.0/08-upgrading/03-upgrade-to-two.md b/versioned_docs/version-2.3.0/08-upgrading/03-upgrade-to-two.md new file mode 100644 index 00000000..54e3b54d --- /dev/null +++ b/versioned_docs/version-2.3.0/08-upgrading/03-upgrade-to-two.md @@ -0,0 +1,405 @@ +# Upgrade to 2.0 + +## Changes to authentication + +The base auth implementation has been removed from Serverpod core and moved into the `serverpod_auth` package. If you are not using authentication at all this change does not impact you. If you are using the auth module already the transition is simple. + +The default authentication handler will now throw an `UnimplementedError`. It is now required to supply the authentication handler to the Serverpod object, in your server.dart file make the following change: + +```dart +import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; + +void run(List args) async { + var pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: auth.authenticationHandler, // Add this line + ); + + ... +} +``` + +### Advanced integrations + +The methods `signInUser` and `signOutUser` now takes the session object as a param and is no longer available on the session object. Instead import the class `UserAuthentication` from the auth module to access these static methods. + +```dart +UserAuthentication.signInUser(session, userId, 'provider'); + +UserAuthentication.signOutUser(session); +``` + +The table `serverpod_auth_key` has been removed from Serverpod core but is available in the serverpod_auth module instead. This means that if you wrote a custom integration before without using the serverpod_auth module you have to take care of managing your token implementation. + +Adding the definition of the `serverpod_auth_key` table to your project is the simplest way to do a seamless migration. + +The table was defined in the following way: + +```yaml +### Provides a method of access for a user to authenticate with the server. +class: AuthKey +table: serverpod_auth_key +fields: + ### The id of the user to provide access to. + userId: int + + ### The hashed version of the key. + hash: String + + ### The key sent to the server to authenticate. + key: String?, !persist + + ### The scopes this key provides access to. + scopeNames: List + + ### The method of signing in this key was generated through. This can be email + ### or different social logins. + method: String +indexes: + serverpod_auth_key_userId_idx: + fields: userId +``` + +Your are then responsible for creating/removing entries in this table, the old `signInUser` and `signOutUser` that used to provide this functionality can be found [here](https://github.com/serverpod/serverpod/blob/13795a7bd4c0cc5a03101b6f378cb914673046dd/packages/serverpod/lib/src/server/session.dart#L359-L394). + +## Changes to the Session Object + +### Removed deprecated fields + +With Serverpod 2.0, we have removed the deprecated legacy database layer from the `Session` object. The `Session` object now incorporates the new database layer, accessed via the `dbNext` field in Serverpod 1.2, under the `db` field. + +```dart +session.dbNext.find(...); +``` + +becomes + +```dart +session.db.find(...); +``` + +### Authenticated user information retrieval + +In Serverpod 2.0, we have removed the getters `scopes` and `authenticatedUser` from session. This information is now retrievable through the `authenticated` getter as fields of the returned object. + +Replace this: + +```dart +int? userId = await session.auth.authenticatedUser; + +Set? scopes = await session.scopes; +``` + +With this: + +```dart +final authenticated = await session.authenticated; + +//Read authenticated userId +int? userId = authenticated?.userId; + +//Read scopes +Set? scopes = authenticated?.scopes; +``` + +If the `authenticated` property is set on the session it effectively means there is an authenticated user making the request. + +### Authentication helpers + +The field `auth` has been removed and the methods `signInUser` and `signOutUser` have been moved to the `serverpod_auth` module. + +## Changes to database queries + +### Removed unsafeQueryMappedResults(...) + +The `unsafeQueryMappedResults(...)` method has been removed. A similar result can now instead be formatted from the `unsafeQuery(...)` result by calling the `toColumnMap()` method for each row of the result. `toColumnMap` returns a map containing the query alias for the column as key and the row-column value as value. + +Given a query that performs a join like this: + +```sql +SELECT + "company"."id" AS "company.id", + "company"."name" AS "company.name", + "company"."townId" AS "company.townId", + "company_town_town"."id" AS "company_town_town.id", + "company_town_town"."name" AS "company_town_town.name", + "company_town_town"."mayorId" AS "company_town_town.mayorId" +FROM + "company" +LEFT JOIN + "town" AS "company_town_town" ON "company"."townId" = "company_town_town"."id" +ORDER BY + "company"."name" +``` + +The return type from `unsafeQueryMappedResults(...)` in 1.2 was: + +```json +[ + { + "company": { + "company.id": 40, + "company.name": "Apple", + "company.townId": 64 + }, + "town": { + "company_town_town.id": 64, + "company_town_town.name": "San Francisco", + "company_town_town.mayorId": null + } + }, + { + "company": { + "company.id": 39, + "company.name": "Serverpod", + "company.townId": 63 + }, + "town": { + "company_town_town.id": 63, + "company_town_town.name": "Stockholm", + "company_town_town.mayorId": null + } + } +] +``` + +And if `result.map((row) => row.toColumnMap())` is used to format the result from `unsafeQuery(...)` in 2.0, the following result is obtained: + +```json +[ + { + "company.id": 38, + "company.name": "Apple", + "company.townId": 62, + "company_town_town.id": 62, + "company_town_town.name": "San Francisco", + "company_town_town.mayorId": null + }, + { + "company.id": 37, + "company.name": "Serverpod", + "company.townId": 61, + "company_town_town.id": 61, + "company_town_town.name": "Stockholm", + "company_town_town.mayorId": null + } +] +``` + +or for a simple query without aliases: + +```sql +SELECT + "id", + "name", + "townId" +FROM + "company" +ORDER BY + "name" +``` + +the return type from `unsafeQueryMappedResults(...)` in 1.2 was: + +```json +[ + { + "company": { + "id": 54, + "name": "Apple", + "townId": 86 + } + }, + { + "company": { + "id": 53, + "name": "Serverpod", + "townId": 85 + } + } +] +``` + +and if `result.map((row) => row.toColumnMap())` is used to format the result from `unsafeQuery(...)` in 2.0, the following result is obtained: + +```json + [ + { + "id": 54, + "name": "Apple", + "townId": 86 + }, + { + "id": 53, + "name": "Serverpod", + "townId": 85 + } +] +``` + +### Update return type for delete operations + +The return type for all delete operations has been changed from the `id` of the deleted rows to the actual deleted rows. This makes the return type for the delete operations consistent with the return type of the other database operations. It also dramatically simplifies retrieving and removing rows in concurrent environments. + +Return type before the change: + +```dart +int companyId = await Company.db.deleteRow(session, company); +List companyIds = await Company.db.delete(session, [company]); +List companyIds = await Company.db.deleteWhere(session, where: (t) => t.name.like('%Ltd')); +``` + +Return types after the change: + +```dart +Company company = await Company.db.deleteRow(session, company); +List companies = await Company.db.delete(session, [company]); +List companies = await Company.db.deleteWhere(session, where: (t) => t.name.like('%Ltd')); +``` + +## Changes to database tables + +### Integer representation changed to bigint + +Integer representation in the database has changed from `int` to `bigint`. From now on, models with `int` fields will generate database migrations where that field is defined as a `bigint` type in the database. + +This change also applies to the `id` field of models where `bigserial` is now used to generate the id. + +The change is compatible with existing databases. Existing migrations therefore, won't be changed by the Serverpod migration system. No manual modification to the database is required if this data representation is not essential for the application. However, all new migrations will be created with the new representation. + +#### Why is this change made? + +The change was made to ensure that [Dart](https://dart.dev/guides/language/numbers) and the database representation of integers is consistent. Dart uses 64-bit integers, and the `int` type in Dart is a 64-bit integer. The `int` type in PostgreSQL is a 32-bit integer. This means that the `int` type in Dart can represent larger numbers than the `int` type in PostgreSQL. By using `bigint` in PostgreSQL, the integer representation is consistent between Dart and the database. + +In terms of performance, there are usually no significant drawbacks with using `bigint` instead of `int`. In most cases a good index strategy will be more important than the integer representation. Here is a guide that benchmarks the performance of `int` and `bigint` in PostgreSQL: [Use BIGINT in Postgres](https://blog.rustprooflabs.com/2021/06/postgres-bigint-by-default) + +#### Ensuring new databases are created with the new representation + +Since existing migrations won't be changed, databases that are created with these will still use `int` to represent integers. + +To ensure new databases are created with the new representation, the latest migration should be generated using Serverpod 2.0. It is enough to have an empty migration to ensure new databases use the new representation. + +A new empty migration can be created by running the following command in the terminal: + +```bash +$ serverpod create-migration --force +``` + +#### Migration of existing tables + +The migration of existing tables to use the new representation will vary depending on the database content. Utilizing the wrong migration strategy might cause downtime for your application. That is the reason Serverpod does not automatically migrate existing tables. + +##### Small tables + +A simple way to migrate for small tables is to execute the following sql query to the database: + +```sql +ALTER SEQUENCE "my_table_id_seq" AS bigint; +ALTER TABLE "my_table" ALTER "id" TYPE bigint; +ALTER TABLE "my_table" ALTER "myNumber" TYPE bigint; +``` + +The first two lines modify the id sequence for a table named `"my_table"` to use `bigint` instead of `int`. The last line modifies a column of the same table to use `bigint`. The drawback of this approach is that it locks the table during the migration. Therefore, this strategy is not recommended for large tables. + +##### Large tables + +Migrating large tables without application downtime is a more complex operation, and the approach will vary depending on the data structure. Below are some gathered resources on the subject. + +- [Zemata - Column migration from INT to BIGINT](http://zemanta.github.io/2021/08/25/column-migration-from-int-to-bigint-in-postgresql/) +- [AM^2 - Changing a column from int to bigint, without downtime](https://am2.co/2019/12/changing-a-column-from-int-to-bigint-without-downtime/) +- [Crunch data - The integer at the End of the Universe](https://www.crunchydata.com/blog/the-integer-at-the-end-of-the-universe-integer-overflow-in-postgres) + +## Changes in the authentication module + +### Unsecure random disabled by default + +The authentication module's default value for allowing unsecure random number generation is now `false`. An exception will be thrown when trying to hash a password if no secure random number generator is available. To preserve the old behavior and enable unsecure random number generation, set the `allowUnsecureRandom` property in the `AuthConfig` to `true`. + +```dart +auth.AuthConfig.set(auth.AuthConfig( + allowUnsecureRandom: true, +)); +``` + +## Updates to Serialization in Serverpod 2.0 + +### General Changes to Model Serialization + +Serverpod 2.0 significantly streamlines the model serialization process. In earlier versions, the `fromJson` factory constructors needed a `serializationManager` parameter to handle object deserialization. This parameter has now been removed, enhancing simplicity and usability. + +#### Before change + +```dart +final Map json = classInstance.toJson(); +final SerializationManager serializationManager = Protocol(); +final ClassName test = ClassName.fromJson(json, serializationManager); +``` + +#### After change + +```dart +final Map json = classInstance.toJson(); +final ClassName test = ClassName.fromJson(json); +``` + +### Enhancements for Custom Serialization + +The removal of the `serializationManager` parameter in Serverpod 2.0 simplifies the serialization process not only for general models but also significantly enhances custom serialization workflows. +For custom classes that previously utilized unique serialization logic with the `serializationManager`, adjustments may be necessary. + +#### Previous Implementation + +In the previous versions, models required the `serializationManager` to be passed explicitly, as shown in the following code snippet: + +```dart +factory ClassName.fromJson( + Map json, + SerializationManager serializationManager, + ) { + return ClassName( + json['name'], + ); + } +``` + +#### Updated Implementation + +With the release of Serverpod 2.0, the `fromJson` constructor has been simplified and the `serializationManager` has been removed: + +```dart +factory ClassName.fromJson( + Map json, + ) { + return ClassName( + json['name'], + ); + } +``` + +## Deprecation Notice for `SerializableEntity` + +The `SerializableEntity` class is deprecated and will be removed in version 3. Please implement the `SerializableModel` interface instead for creating serializable models. + +### Migration Guide + +To migrate your code from `SerializableEntity` to `SerializableModel`, replace `extends SerializableEntity` with `implements SerializableModel` in your model classes. + +#### Example + +**Before:** + +```dart +class CustomClass extends SerializableEntity { + // Your code here +} +``` + +**After:** + +```dart +class CustomClass implements SerializableModel { + // Your code here +} +``` diff --git a/versioned_docs/version-2.3.0/08-upgrading/04-upgrade-to-two-point-two.md b/versioned_docs/version-2.3.0/08-upgrading/04-upgrade-to-two-point-two.md new file mode 100644 index 00000000..807e2d26 --- /dev/null +++ b/versioned_docs/version-2.3.0/08-upgrading/04-upgrade-to-two-point-two.md @@ -0,0 +1,184 @@ +# Upgrade to 2.2 + +Serverpod 2.2 includes new test tools that make it easy to create tests for endpoint methods. For new projects they are configured by default, but existing projects need to go through some steps to enable it (see below). The full documentation of this feature can also be found [here](../concepts/testing/get-started). + +## Add test tools to existing projects + +For existing non-Mini projects, a few extra things need to be done: + +1. Add the `server_test_tools_path` key with the value `test/integration/test_tools` to `config/generator.yaml`: + +```yaml +server_test_tools_path: test/integration/test_tools +``` + + Without this key, the test tools file is not generated. With the above config the location of the test tools file is `test/integration/test_tools/serverpod_test_tools.dart`, but this can be set to any folder (though should be outside of `lib` as per Dart's test conventions). + + +2. New projects now come with a test profile in `docker-compose.yaml`. This is not strictly mandatory, but is recommended to ensure that the testing state is never polluted. Add the snippet below to the `docker-compose.yaml` file in the server directory: + +```yaml +# Add to the existing services +postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: _test + POSTGRES_PASSWORD: "" + volumes: + - _test_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test +redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test +volumes: + # ... + _test_data: +``` + +
+Or copy the complete file here. +

+ +```yaml +services: + # Development services + postgres: + image: postgres:16.3 + ports: + - '8090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: + POSTGRES_PASSWORD: "" + volumes: + - _data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - dev + redis: + image: redis:6.2.6 + ports: + - '8091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - dev + + # Test services + postgres_test: + image: postgres:16.3 + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: _test + POSTGRES_PASSWORD: "" + volumes: + - _test_data:/var/lib/postgresql/data + profiles: + - '' # Default profile + - test + redis_test: + image: redis:6.2.6 + ports: + - '9091:6379' + command: redis-server --requirepass "" + environment: + - REDIS_REPLICATION_MODE=master + profiles: + - '' # Default profile + - test + +volumes: + _data: + _test_data: +``` + +

+
+3. Create a `test.yaml` file and add it to the `config` directory: + +```yaml +# This is the configuration file for your test environment. +# All ports are set to zero in this file which makes the server find the next available port. +# This is needed to enable running tests concurrently. To set up your server, you will +# need to add the name of the database you are connecting to and the user name. +# The password for the database is stored in the config/passwords.yaml. + +# Configuration for the main API test server. +apiServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# Configuration for the Insights test server. +insightsServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# Configuration for the web test server. +webServer: + port: 0 + publicHost: localhost + publicPort: 0 + publicScheme: http + +# This is the database setup for your test server. +database: + host: localhost + port: 9090 + name: _test + user: postgres + +# This is the setup for your Redis test instance. +redis: + enabled: false + host: localhost + port: 9091 +``` + + +4. Add this entry to `config/passwords.yaml` + +```yaml +test: + database: '' + redis: '' +``` + + +5. Add a `dart_test.yaml` file to the `server` directory (next to `pubspec.yaml`) with the following contents: + +```yaml +tags: + integration: {} + +``` + + +6. Finally, add the `test` and `serverpod_test` packages as dev dependencies in `pubspec.yaml`: + +```yaml +dev_dependencies: + serverpod_test: # Should be same version as the `serverpod` package + test: ^1.24.2 +``` + +That's it, the project setup should be ready to start using the test tools! diff --git a/versioned_docs/version-2.3.0/08-upgrading/_category_.json b/versioned_docs/version-2.3.0/08-upgrading/_category_.json new file mode 100644 index 00000000..76ee203a --- /dev/null +++ b/versioned_docs/version-2.3.0/08-upgrading/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Upgrading" + } + \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/09-tools/01-insights.md b/versioned_docs/version-2.3.0/09-tools/01-insights.md new file mode 100644 index 00000000..35d65a77 --- /dev/null +++ b/versioned_docs/version-2.3.0/09-tools/01-insights.md @@ -0,0 +1,14 @@ +# Serverpod Insights + +Serverpod has a companion app. It is currently available for Mac and Windows, but Linux is coming soon. The app has support for viewing your server's logs and health metrics, but we are adding many more features in version 1.2. You must use a version of the app that matches the version of Serverpod you use in your project, or not all features may work correctly. + +![Serverpod Insights](https://serverpod.dev/assets/img/serverpod-screenshot.webp) + +## Downloads + +| App version | MacOS | Windows | +| :------------------------- | :-------------------------------------------------------------------- | :------------ | +| Serverpod 2.0.0 (latest) | [Download](https://downloads.serverpod.dev/macos/Serverpod-2.0.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-2.0.0.zip) | +| Serverpod 1.2.0 | [Download](https://downloads.serverpod.dev/macos/Serverpod-1.2.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-1.2.0.zip) | +| Serverpod 1.1.0 | [Download](https://downloads.serverpod.dev/macos/Serverpod-1.1.0.zip) | [Download](https://downloads.serverpod.dev/windows/serverpod-1.1.0.zip) | +| Serverpod 1.0.0 | [Download](https://serverpod.dev/insights/Serverpod-1.0.0.zip) | n/a | diff --git a/versioned_docs/version-2.3.0/09-tools/02-lsp.md b/versioned_docs/version-2.3.0/09-tools/02-lsp.md new file mode 100644 index 00000000..2374cf57 --- /dev/null +++ b/versioned_docs/version-2.3.0/09-tools/02-lsp.md @@ -0,0 +1,13 @@ +# LSP server + +The [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) is a standardized protocol designed to provide development environments with language-specific functionalities. In the context of Serverpod, the LSP server specifically offers diagnostics for YAML protocol files, aiding developers in identifying and resolving potential issues within these files. + +To start the Serverpod LSP server, use the following command: + +```bash +$ serverpod language-server +``` + +:::info +If you use [VS Code](https://code.visualstudio.com/) you can instead use the [Serverpod extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod). +::: diff --git a/versioned_docs/version-2.3.0/09-tools/_category_.json b/versioned_docs/version-2.3.0/09-tools/_category_.json new file mode 100644 index 00000000..6c2c8395 --- /dev/null +++ b/versioned_docs/version-2.3.0/09-tools/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Tools", + "collapsed": true + } + \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/10-contribute.md b/versioned_docs/version-2.3.0/10-contribute.md new file mode 100644 index 00000000..f104d911 --- /dev/null +++ b/versioned_docs/version-2.3.0/10-contribute.md @@ -0,0 +1,145 @@ +# Roadmap & contributions + +Serverpod is built by the community for the community. Pull requests are very much welcome. If you are making something more significant than just a tiny bug fix, please get in touch with Serverpod's lead developer [Viktor Lidholt](https://www.linkedin.com/in/viktorlidholt/) before you get started. This makes sure that your contribution aligns with Serverpod's overall vision and roadmap and that multiple persons don't do the same work. + +
+ +## Roadmap +If you want to contribute, please view our [roadmap](https://github.com/orgs/serverpod/projects/4) to make sure your contribution is in-line with our plans for future development. This will make it much more likely that we can include the new features you are building. You can also check our list of [good first issues](https://github.com/serverpod/serverpod/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). + +:::important + +For us to be able to accept your code, you must follow the guidelines below. __We cannot accept contributions unless there are tests written for it.__ We also cannot accept features that are not complete for all use cases. In very rare circumstances, we may still be able to use code that doesn't comply with the guidelines, but it may take a long time for us to free up a resource that can clean up the code or write missing tests. + +::: + +## Working on Serverpod + +The main [Serverpod repository](https://github.com/serverpod/serverpod) contains all Serverpod code and code for tests and official modules and integrations. Send any pull requests to the `main` branch. + +### Writing code + +We are very conscious about keeping the Serverpod code base clean. When you write your code, make sure to use `dart format` and that you don't get any errors or lints from `dart analyze`. + +### Running all tests + +Continuous integration tests are automatically run when sending a pull request to the `main` branch. You can run the tests locally by changing your working directory into the root serverpod directory and running: + +```bash +$ util/run_tests +``` + +:::caution + +Tests may not yet work if running on a Windows machine. Mac or Linux is recommended for Serverpod development. + +::: + +### Running individual tests + +Running single individual tests is useful when you are working on a specific feature. To do it, you will need to manually start the test server, then run the integration tests from the `serverpod` package. + +1. Add entries for the test server, postgres and redis at the end of your `/etc/hosts file`. + + ```text + 127.0.0.1 serverpod_test_server + 127.0.0.1 postgres + 127.0.0.1 redis + ``` + +2. Start the Docker container for the test server. + + ```bash + $ cd tests/serverpod_test_server/docker-local + $ docker-compose up --build --detach + $ ./setup-tables + ``` + +3. Start the test server. + + ```bash + $ cd tests/serverpod_test_server + $ dart bin/main.dart + ``` + +4. Run an individual test + + ```bash + $ cd tests/serverpod_test_server + $ dart test test/connection_test.dart + ``` + +### Command line tools + +To run the `serverpod` command from your cloned repository, you will need to: + +```bash +$ cd tools/serverpod_cli +$ dart pub get +$ dart pub global activate --source path . +``` + +Depending on your Dart version you may need to run the `dart pub global` command above every time you've made changes in the Serverpod tooling. + +:::info + +If you run the local version of the `serverpod` command line interface, you will need to set the `SERVERPOD_HOME` environment variable. It should point to the root your cloned `serverpod` monorepo. (E.g. `/Users/myuser/MyRepos/serverpod`) + +If you use `serverpod create` to set up a new project with a local version of the tooling, you may need to edit the pubspec files in the created packages to point to your local serverpod packages. + +::: + +### Editing the pubspec.yaml files + +First off, we are restrictive about which new packages we include in the Serverpod project. So before starting to add new dependencies, you should probably get in touch with the maintainers of Serverpod to clear it. + +Secondly, you shouldn't edit the `pubspec.yaml` files directly. Instead, make changes to the files in the `templates/` directory. When you've made changes, run the `update_pubspecs` command to generate the `pubspec.yaml` files. + +```bash +$ util/update_pubspecs +``` + +## Submitting your pull request + +To keep commits clean, Serverpod squashes them when merging pull requests. Therefore, it is essential that each pull request only contains a single feature or bug fix. Keeping the pull requests smaller also makes it faster and easier to review the code. + +If you are contributing new code, you will also need to provide tests for your code. The tests should be placed in the `tests/serverpod_test_server` package. + +## Getting support + +Feel free to post on [Serverpod's discussion board](https://github.com/serverpod/serverpod/discussions) if you have any questions. We check the board daily. + + +## Repository overview + +Serverpod is a large project and contains many parts. Here is a quick overview of how Serverpod is structured and where to find relevant files. + +### `packages` + +Here, you find the core serverpod Dart packages. + +- __`serverpod`__: Contains the main Serverpod package, the ORM, basic authentication, messaging, and cache. It also contains the endpoints of the Serverpod Insights API. +- __`serverpod_client`__: The client classes that are not generated by the CLI tooling. +- `serverpod_flutter`: Client code that relies on Flutter. It contains some concrete classes defined in `serverpod_client`. +- __`serverpod_serialization`__: Code for handling serialization, which is shared between the `serverpod` package and `serverpod_client`. +- __`serverpod_service_client`__: This is the generated API for Serverpod Insights. +- __`serverpod_shared`__: Code that is shared between serverpod and Serverpod's tooling (i.e., `serverpod_cli`). + +### `templates` + +The template directory contains templates for all pubspec files. To generate the real pubspec files from the templates, use the `util/update_pubspecs` script. The template directory also contains templates for the `serverpod create` command. + +### `tools` + +Here, you will find the code for Serverpod's tooling. + +- __`serverpod_cli`__: Serverpod's command line interface. The CLI also contains code for Serverpod's analyzer and code generation. +- __`serverpod_vs_code_extension`__: The VS Code extension is built around the CLI. + +### `modules` + +These are 1st party modules for Serverpod. Currently, we maintain an authentication module and a chat module. Modules contain server, client, Flutter code, and definitions for database tables. + +### `integrations` + +These are integrations for 3rd party services, such as Cloud storage. \ No newline at end of file diff --git a/versioned_docs/version-2.3.0/index.md b/versioned_docs/version-2.3.0/index.md new file mode 100644 index 00000000..d632ff1d --- /dev/null +++ b/versioned_docs/version-2.3.0/index.md @@ -0,0 +1,43 @@ +--- +sidebar_position: -1 +--- + +# Installing Serverpod + +Serverpod is an open-source, scalable app server written in Dart for the Flutter community. Serverpod automatically generates your model and client-side code by analyzing your server. Calling a remote endpoint is as easy as making a local method call. + +
+ +## Command line tools + +Serverpod is tested on Mac, Windows, and Linux. Before you can install Serverpod, you need to have __[Flutter](https://flutter.dev/docs/get-started/install)__ installed. + + +Install Serverpod by running: + +```bash +$ dart pub global activate serverpod_cli +``` + +Now test the installation by running: + +```bash +$ serverpod +``` + +If everything is correctly configured, the help for the `serverpod` command is now displayed. + +## VS Code Extension + +The Serverpod VS Code extension makes it easy to work with your Serverpod projects. It provides real-time diagnostics and syntax highlighting for model files in your project. + +![Serverpod extension](/img/syntax-highlighting.png) + +Install the extension from the VS Code Marketplace: __[Serverpod extension](https://marketplace.visualstudio.com/items?itemName=serverpod.serverpod)__ + +## Serverpod Insights + +__[Serverpod Insights](tools/insights)__ is a companion app bundled with Serverpod. It allows you to access your server's logs and health metrics. Insights is available for Mac and Windows, but we will be adding support for Linux in the future. + +![Serverpod Insights](https://serverpod.dev/assets/img/serverpod-screenshot.webp) + diff --git a/versioned_sidebars/version-2.3.0-sidebars.json b/versioned_sidebars/version-2.3.0-sidebars.json new file mode 100644 index 00000000..caea0c03 --- /dev/null +++ b/versioned_sidebars/version-2.3.0-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/versions.json b/versions.json index d97c55e9..0cba2c40 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ [ + "2.3.0", "2.2.0", "2.1.0", "2.0.0",