Created
April 25, 2025 21:18
-
-
Save nogipx/aa5fa13554d84b81b09c9d87e16d51ec to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
└── versioned_docs | |
└── version-2.6.0 | |
└── 06-concepts | |
├── 01-working-with-endpoints.md | |
├── 02-models.md | |
├── 03-serialization.md | |
├── 04-exceptions.md | |
├── 05-sessions.md | |
├── 06-database | |
├── 01-connection.md | |
├── 02-models.md | |
├── 03-relations | |
│ ├── 01-one-to-one.md | |
│ ├── 02-one-to-many.md | |
│ ├── 03-many-to-many.md | |
│ ├── 04-self-relations.md | |
│ ├── 05-referential-actions.md | |
│ └── 06-modules.md | |
├── 04-indexing.md | |
├── 05-crud.md | |
├── 06-filter.md | |
├── 07-relation-queries.md | |
├── 08-sort.md | |
├── 08-transactions.md | |
├── 09-pagination.md | |
├── 10-raw-access.md | |
└── 11-migrations.md | |
├── 07-configuration.md | |
├── 08-caching.md | |
├── 09-logging.md | |
├── 10-modules.md | |
├── 11-authentication | |
├── 01-setup.md | |
├── 02-basics.md | |
├── 03-working-with-users.md | |
├── 04-providers | |
│ ├── 01-email.md | |
│ ├── 02-google.md | |
│ ├── 03-apple.md | |
│ ├── 05-firebase.md | |
│ └── 06-custom-providers.md | |
└── 05-custom-overrides.md | |
├── 12-file-uploads.md | |
├── 13-health-checks.md | |
├── 14-scheduling.md | |
├── 15-streams.md | |
├── 16-server-events.md | |
├── 17-backward-compatibility.md | |
├── 18-webserver.md | |
├── 19-testing | |
├── 01-get-started.md | |
├── 02-the-basics.md | |
├── 03-advanced-examples.md | |
└── 04-best-practises.md | |
├── 20-security-configuration.md | |
└── 21-experimental.md | |
/versioned_docs/version-2.6.0/06-concepts/01-working-with-endpoints.md: | |
-------------------------------------------------------------------------------- | |
1 | # Working with endpoints | |
2 | | |
3 | 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. | |
4 | | |
5 | ```dart | |
6 | import 'package:serverpod/serverpod.dart'; | |
7 | | |
8 | class ExampleEndpoint extends Endpoint { | |
9 | Future<String> hello(Session session, String name) async { | |
10 | return 'Hello $name'; | |
11 | } | |
12 | } | |
13 | ``` | |
14 | | |
15 | 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. | |
16 | | |
17 | On the client side, you can now call the method by calling: | |
18 | | |
19 | ```dart | |
20 | var result = await client.example.hello('World'); | |
21 | ``` | |
22 | | |
23 | The client is initialized like this: | |
24 | | |
25 | ```dart | |
26 | // Sets up a singleton client object that can be used to talk to the server from | |
27 | // anywhere in our app. The client is generated from your server code. | |
28 | // The client is set up to connect to a Serverpod running on a local server on | |
29 | // the default port. You will need to modify this to connect to staging or | |
30 | // production servers. | |
31 | var client = Client('http://$localhost:8080/') | |
32 | ..connectivityMonitor = FlutterConnectivityMonitor(); | |
33 | ``` | |
34 | | |
35 | 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). | |
36 | | |
37 | 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. | |
38 | | |
39 | ```yaml | |
40 | # your_project_server/config/development.yaml | |
41 | | |
42 | apiServer: | |
43 | port: 8080 | |
44 | publicHost: localhost # Change this line | |
45 | publicPort: 8080 | |
46 | publicScheme: http | |
47 | ... | |
48 | ``` | |
49 | | |
50 | :::info | |
51 | | |
52 | 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. | |
53 | | |
54 | ::: | |
55 | | |
56 | ## Passing parameters | |
57 | | |
58 | 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`, `Uri`, `BigInt`, 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. | |
59 | | |
60 | You can also pass `List`, `Map`, `Record` and `Set` as parameters, but they need to be strictly typed with one of the types mentioned above. | |
61 | | |
62 | :::warning | |
63 | | |
64 | 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: | |
65 | | |
66 | ```yaml | |
67 | maxRequestSize: 1048576 | |
68 | ``` | |
69 | | |
70 | ::: | |
71 | | |
72 | ## Return types | |
73 | | |
74 | The return type must be a typed Future. Supported return types are the same as for parameters. | |
75 | | |
76 | ## Ignore endpoint definition | |
77 | | |
78 | ### Ignore an entire `Endpoint` class | |
79 | | |
80 | If you want the code generator to ignore an endpoint definition, you can annotate either the entire class or individual methods with `@ignoreEndpoint`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it. | |
81 | | |
82 | ```dart | |
83 | import 'package:serverpod/serverpod.dart'; | |
84 | | |
85 | @ignoreEndpoint | |
86 | class ExampleEndpoint extends Endpoint { | |
87 | Future<String> hello(Session session, String name) async { | |
88 | return 'Hello $name'; | |
89 | } | |
90 | } | |
91 | ``` | |
92 | | |
93 | The above code will not generate any server or client bindings for the example endpoint. | |
94 | | |
95 | ### Ignore individual `Endpoint` methods | |
96 | | |
97 | Alternatively, you can disable single methods by annotation them with `@ignoreEndpoint`. | |
98 | | |
99 | ```dart | |
100 | import 'package:serverpod/serverpod.dart'; | |
101 | | |
102 | class ExampleEndpoint extends Endpoint { | |
103 | Future<String> hello(Session session, String name) async { | |
104 | return 'Hello $name'; | |
105 | } | |
106 | | |
107 | @ignoreEndpoint | |
108 | Future<String> goodbye(Session session, String name) async { | |
109 | return 'Bye $name'; | |
110 | } | |
111 | } | |
112 | ``` | |
113 | | |
114 | In this case the `ExampleEndpoint` will only expose the `hello` method, whereas the `goodbye` method will not be accessible externally. | |
115 | | |
116 | ## Endpoint method inheritance | |
117 | | |
118 | Endpoints can be based on other endpoints using inheritance, like `class ChildEndpoint extends ParentEndpoint`. If the parent endpoint was marked as `abstract` or `@ignoreEndpoint`, no client code is generated for it, but a client will be generated for your subclass – as long as it does not opt out again. | |
119 | Inheritance gives you the possibility to modify the behavior of `Endpoint` classes defined in other Serverpod modules. | |
120 | | |
121 | Currently, there are the following possibilities to extend another `Endpoint` class: | |
122 | | |
123 | ### Inheriting from an `Endpoint` class | |
124 | | |
125 | Given an existing `Endpoint` class, it is possible to extend or modify its behavior while retaining the already exposed methods. | |
126 | | |
127 | ```dart | |
128 | import 'package:serverpod/serverpod.dart'; | |
129 | | |
130 | class CalculatorEndpoint extends Endpoint { | |
131 | Future<int> add(Session session, int a, int b) async { | |
132 | return a + b; | |
133 | } | |
134 | } | |
135 | | |
136 | class MyCalculatorEndpoint extends CalculatorEndpoint { | |
137 | Future<int> subtract(Session session, int a, int b) async { | |
138 | return a - b; | |
139 | } | |
140 | } | |
141 | ``` | |
142 | | |
143 | The generated client code will now be able to access both `CalculatorEndpoint` and `MyCalculatorEndpoint`. | |
144 | Whereas the `CalculatorEndpoint` only exposes the original `add` method, `MyCalculatorEndpoint` now exposes both the inherited `add` and its own `subtract` methods. | |
145 | | |
146 | ### Inheriting from an `Endpoint` class marked `abstract` | |
147 | | |
148 | Endpoints marked as `abstract` are not added to the server. But if they are subclassed, their methods will be exposed through the subclass. | |
149 | | |
150 | ```dart | |
151 | import 'package:serverpod/serverpod.dart'; | |
152 | | |
153 | abstract class CalculatorEndpoint extends Endpoint { | |
154 | Future<int> add(Session session, int a, int b) async { | |
155 | return a + b; | |
156 | } | |
157 | } | |
158 | | |
159 | class MyCalculatorEndpoint extends CalculatorEndpoint {} | |
160 | ``` | |
161 | | |
162 | The generated client code will only be able to access `MyCalculatorEndpoint`, as the abstract `CalculatorEndpoint` is not exposed on the server. | |
163 | `MyCalculatorEndpoint` exposes the `add` method it inherited from `CalculatorEndpoint`. | |
164 | | |
165 | #### Extending an `abstract` `Endpoint` class | |
166 | | |
167 | In the above example, the `MyCalculatorEndpoint` only exposed the inherited `add` method. It can be further extended with custom methods like this: | |
168 | | |
169 | ```dart | |
170 | import 'package:serverpod/serverpod.dart'; | |
171 | | |
172 | class MyCalculatorEndpoint extends CalculatorEndpoint { | |
173 | Future<int> subtract(Session session, int a, int b) async { | |
174 | return a - b; | |
175 | } | |
176 | } | |
177 | ``` | |
178 | | |
179 | In this case, it will expose both an `add` and a `subtract` method. | |
180 | | |
181 | ### Inheriting from an `Endpoint` class annotated with `@ignoreEndpoint` | |
182 | | |
183 | Suppose you had an `Endpoint` class marked with `@ignoreEndpoint` and a subclass that extends it: | |
184 | | |
185 | ```dart | |
186 | import 'package:serverpod/serverpod.dart'; | |
187 | | |
188 | @ignoreEndpoint | |
189 | class CalculatorEndpoint extends Endpoint { | |
190 | Future<int> add(Session session, int a, int b) async { | |
191 | return a + b; | |
192 | } | |
193 | } | |
194 | | |
195 | class MyCalculatorEndpoint extends CalculatorEndpoint {} | |
196 | ``` | |
197 | | |
198 | Since `CalculatorEndpoint` is marked as `@ignoreEndpoint` it will not be exposed on the server. Only `MyCalculatorEndpoint` will be accessible from the client, which provides the inherited `add` methods from its parent class. | |
199 | | |
200 | ### Overriding endpoint methods | |
201 | | |
202 | It is possible to override methods of the superclass. This can be useful when you want to modify the behavior of specific methods but preserve the rest. | |
203 | | |
204 | ```dart | |
205 | import 'package:serverpod/serverpod.dart'; | |
206 | | |
207 | abstract class GreeterBaseEndpoint extends Endpoint { | |
208 | Future<String> greet(Session session, String name) async { | |
209 | return 'Hello $name'; | |
210 | } | |
211 | } | |
212 | | |
213 | class ExcitedGreeterEndpoint extends GreeterBaseEndpoint { | |
214 | @override | |
215 | Future<String> greet(Session session, String name) async { | |
216 | return '${super.hello(session, name)}!!!'; | |
217 | } | |
218 | } | |
219 | ``` | |
220 | | |
221 | Since `GreeterBaseEndpoint` is `abstract`, it will not be exposed on the server. The `ExcitedGreeterEndpoint` will expose a single `greet` method, and its implementation will augment the superclass's one by adding `!!!` to that result. | |
222 | | |
223 | This way, you can modify the behavior of endpoint methods while still sharing the implementation through calls to `super`. Be aware that the method signature has to be compatible with the base class per Dart's rules, meaning you can add optional parameters, but can not add required parameters or change the return type. | |
224 | | |
225 | ### Hiding endpoint methods with `@ignoreEndpoint` | |
226 | | |
227 | In case you want to hide methods from an endpoint use `@ignoreEndpoint` in the child class like so: | |
228 | | |
229 | ```dart | |
230 | import 'package:serverpod/serverpod.dart'; | |
231 | | |
232 | abstract class CalculatorEndpoint extends Endpoint { | |
233 | Future<int> add(Session session, int a, int b) async { | |
234 | return a + b; | |
235 | } | |
236 | | |
237 | Future<int> subtract(Session session, int a, int b) async { | |
238 | return a - b; | |
239 | } | |
240 | } | |
241 | | |
242 | class AdderEndpoint extends CalculatorEndpoint { | |
243 | @ignoreEndpoint | |
244 | Future<int> subtract(Session session, int a, int b) async { | |
245 | throw UnimplementedError(); | |
246 | } | |
247 | } | |
248 | ``` | |
249 | | |
250 | Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `AdderEndpoint` inherits all methods from its parent class, but since it opts to hide `subtract` by annotating it with `@ignoreEndpoint` only the `add` method will be exposed. | |
251 | Don't worry about the exception in the `subtract` implementation. That is only added to satisfy the Dart compiler – in practice, nothing will ever call this method on `AdderEndpoint`. | |
252 | | |
253 | Hiding endpoints from a super class is only appropriate in case the parent `class` is `abstract` or annotated with `@ignoreEndpoint`. Otherwise, the method that should be hidden on the child would still be accessible via the parent class. | |
254 | | |
255 | ### Unhiding endpoint methods annotated with `@ignoreEndpoint` in the super class | |
256 | | |
257 | The reverse of the previous example would be a base endpoint that has a method marked with `@ignoreEndpoint`, which you now want to expose on the subclass. | |
258 | | |
259 | ```dart | |
260 | import 'package:serverpod/serverpod.dart'; | |
261 | | |
262 | abstract class CalculatorEndpoint extends Endpoint { | |
263 | Future<int> add(Session session, int a, int b) async { | |
264 | return a + b; | |
265 | } | |
266 | | |
267 | // Ignored, as this expensive computation should not be exposed by default | |
268 | @ignoreEndpoint | |
269 | Future<BigInt> addBig(Session session, BigInt a, BigInt b) async { | |
270 | return a + b; | |
271 | } | |
272 | } | |
273 | | |
274 | class MyCalculatorEndpoint extends CalculatorEndpoint { | |
275 | @override | |
276 | Future<BigInt> addBig(Session session, BigInt a, BigInt b) async { | |
277 | return super.addBig(session, a, b); | |
278 | } | |
279 | } | |
280 | ``` | |
281 | | |
282 | Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `MyCalculatorEndpoint` will expose both the `add` and `addBig` methods, since `addBig` was overridden and thus lost the `@ignoreEndpoint` annotation. | |
283 | | |
284 | ### Building base endpoints for behavior | |
285 | | |
286 | Endpoint subclassing is not just useful to inherit (or hide) methods, it can also be used to pre-configure any other property of the `Endpoint` class. | |
287 | | |
288 | For example, you could define a base class that requires callers to be logged in: | |
289 | | |
290 | ```dart | |
291 | abstract class LoggedInEndpoint extends Endpoint { | |
292 | @override | |
293 | bool get requireLogin => true; | |
294 | } | |
295 | ``` | |
296 | | |
297 | And now every endpoint that extends `LoggedInEndpoint` will check that the user is logged in. | |
298 | | |
299 | Similarly, you could wrap up a specific set of required scopes in a base endpoint, which you can then easily use for the app's endpoints instead of repeating the scopes in each: | |
300 | | |
301 | ```dart | |
302 | abstract class AdminEndpoint extends Endpoint { | |
303 | @override | |
304 | Set<Scope> get requiredScopes => {Scope.admin}; | |
305 | } | |
306 | ``` | |
307 | | |
308 | Again, just have your custom endpoint extend `AdminEndpoint` and you can be sure that the user has the appropriate permissions. | |
309 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/03-serialization.md: | |
-------------------------------------------------------------------------------- | |
1 | # Custom serialization | |
2 | | |
3 | 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: | |
4 | | |
5 | 1. Your objects must have a method called `toJson()` which returns a JSON serialization of the object. | |
6 | | |
7 | ```dart | |
8 | Map<String, dynamic> toJson() { | |
9 | return { | |
10 | name: 'John Doe', | |
11 | }; | |
12 | } | |
13 | ``` | |
14 | | |
15 | 2. There must be a constructor or factory called `fromJson()`, which takes a JSON serialization as parameters. | |
16 | | |
17 | ```dart | |
18 | factory ClassName.fromJson( | |
19 | Map<String, dynamic> json, | |
20 | ) { | |
21 | return ClassName( | |
22 | name: json['name'] as String, | |
23 | ); | |
24 | } | |
25 | ``` | |
26 | | |
27 | 3. There must be a method called `copyWith()`, which returns a new instance of the object with the specified fields replaced. | |
28 | :::tip | |
29 | 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. | |
30 | ::: | |
31 | | |
32 | ```dart | |
33 | ClassName copyWith({ | |
34 | String? name, | |
35 | }) { | |
36 | return ClassName( | |
37 | name: name ?? this.name, | |
38 | ); | |
39 | } | |
40 | ``` | |
41 | | |
42 | 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. | |
43 | | |
44 | ```yaml | |
45 | ... | |
46 | extraClasses: | |
47 | - package:my_project_shared/my_project_shared.dart:ClassName | |
48 | ``` | |
49 | | |
50 | ## Setup example | |
51 | | |
52 | 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 <my_project>_shared` in the root folder of your project. | |
53 | | |
54 | Your folder structure should then look like this: | |
55 | | |
56 | ```text | |
57 | ├── my_project_client | |
58 | ├── my_project_flutter | |
59 | ├── my_project_server | |
60 | ├── my_project_shared | |
61 | ``` | |
62 | | |
63 | 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. | |
64 | | |
65 | ```yaml | |
66 | dependencies: | |
67 | ... | |
68 | my_project_shared: | |
69 | path: ../my_project_shared | |
70 | ... | |
71 | ``` | |
72 | | |
73 | Now you can create your custom class in your new shared package: | |
74 | | |
75 | ```dart | |
76 | class ClassName { | |
77 | String name; | |
78 | ClassName(this.name); | |
79 | | |
80 | toJson() { | |
81 | return { | |
82 | 'name': name, | |
83 | }; | |
84 | } | |
85 | | |
86 | factory ClassName.fromJson( | |
87 | Map<String, dynamic> jsonSerialization, | |
88 | ) { | |
89 | return ClassName( | |
90 | jsonSerialization['name'], | |
91 | ); | |
92 | } | |
93 | } | |
94 | ``` | |
95 | | |
96 | 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. | |
97 | | |
98 | In your server project, you can create an endpoint returning your custom object. | |
99 | | |
100 | ```dart | |
101 | import 'package:relation_test_shared/relation_test_shared.dart'; | |
102 | import 'package:serverpod/serverpod.dart'; | |
103 | | |
104 | class ExampleEndpoint extends Endpoint { | |
105 | Future<ClassName> getMyCustomClass(Session session) async { | |
106 | return ClassName( | |
107 | 'John Doe', | |
108 | ); | |
109 | } | |
110 | } | |
111 | ``` | |
112 | | |
113 | ## Custom class with Freezed | |
114 | | |
115 | Serverpod also has support for using custom classes created with the [Freezed](https://pub.dev/packages/freezed) package. | |
116 | | |
117 | ```dart | |
118 | import 'package:freezed_annotation/freezed_annotation.dart'; | |
119 | | |
120 | part 'freezed_custom_class.freezed.dart'; | |
121 | part 'freezed_custom_class.g.dart'; | |
122 | | |
123 | @freezed | |
124 | class FreezedCustomClass with _$FreezedCustomClass { | |
125 | const factory FreezedCustomClass({ | |
126 | required String firstName, | |
127 | required String lastName, | |
128 | required int age, | |
129 | }) = _FreezedCustomClass; | |
130 | | |
131 | factory FreezedCustomClass.fromJson( | |
132 | Map<String, Object?> json, | |
133 | ) => | |
134 | _$FreezedCustomClassFromJson(json); | |
135 | } | |
136 | ``` | |
137 | | |
138 | In the config/generator.yaml, you declare the package and the class: | |
139 | | |
140 | ```yaml | |
141 | extraClasses: | |
142 | - package:my_shared_package/my_shared_package.dart:FreezedCustomClass | |
143 | ``` | |
144 | | |
145 | ## Custom class with ProtocolSerialization | |
146 | | |
147 | 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. | |
148 | | |
149 | ### Implementation Example | |
150 | | |
151 | Here’s how you can implement it: | |
152 | | |
153 | ```dart | |
154 | class CustomClass implements ProtocolSerialization { | |
155 | final String? value; | |
156 | final String? serverSideValue; | |
157 | | |
158 | ....... | |
159 | | |
160 | // Serializes fields specifically for protocol communication | |
161 | Map<String, dynamic> toJsonForProtocol() { | |
162 | return { | |
163 | "value":value, | |
164 | }; | |
165 | } | |
166 | | |
167 | // Serializes all fields, including those intended only for server-side use | |
168 | Map<String, dynamic> toJson() { | |
169 | return { | |
170 | "value": value, | |
171 | "serverSideValue": serverSideValue, | |
172 | }; | |
173 | } | |
174 | } | |
175 | ``` | |
176 | | |
177 | This structure ensures that sensitive or server-only data is not exposed to the client, enhancing security and data integrity. | |
178 | | |
179 | Importantly, this implementation is not required for client-side custom models. | |
180 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/04-exceptions.md: | |
-------------------------------------------------------------------------------- | |
1 | # Error handling and exceptions | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | :::tip | |
8 | | |
9 | 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. | |
10 | | |
11 | ::: | |
12 | | |
13 | ## Serializable exceptions | |
14 | | |
15 | 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`. | |
16 | | |
17 | ```yaml | |
18 | exception: MyException | |
19 | fields: | |
20 | message: String | |
21 | errorType: MyEnum | |
22 | ``` | |
23 | | |
24 | After you run `serverpod generate`, you can throw that exception when processing a call to the server. | |
25 | | |
26 | ```dart | |
27 | class ExampleEndpoint extends Endpoint { | |
28 | Future<void> doThingy(Session session) { | |
29 | // ... do stuff ... | |
30 | if (failure) { | |
31 | throw MyException( | |
32 | message: 'Failed to do thingy', | |
33 | errorType: MyEnum.thingyError, | |
34 | ); | |
35 | } | |
36 | } | |
37 | } | |
38 | ``` | |
39 | | |
40 | In your app, catch the exception as you would catch any exception. | |
41 | | |
42 | ```dart | |
43 | try { | |
44 | client.example.doThingy(); | |
45 | } | |
46 | on MyException catch(e) { | |
47 | print(e.message); | |
48 | } | |
49 | catch(e) { | |
50 | print('Something else went wrong.'); | |
51 | } | |
52 | ``` | |
53 | | |
54 | ### Default values in exceptions | |
55 | | |
56 | 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. | |
57 | | |
58 | :::info | |
59 | 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. | |
60 | ::: | |
61 | | |
62 | **Example:** | |
63 | | |
64 | ```yaml | |
65 | exception: MyException | |
66 | fields: | |
67 | message: String, default="An error occurred" | |
68 | errorCode: int, default=1001 | |
69 | ``` | |
70 | | |
71 | In this example: | |
72 | | |
73 | - The `message` field will default to `"An error occurred"` if not provided. | |
74 | - The `errorCode` field will default to `1001`. | |
75 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/05-sessions.md: | |
-------------------------------------------------------------------------------- | |
1 | # Sessions | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | :::tip | |
8 | | |
9 | 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. | |
10 | | |
11 | ```dart | |
12 | session as MethodCallSession; | |
13 | var ipAddress = session.httpRequest.remoteIpAddress; | |
14 | ``` | |
15 | | |
16 | ::: | |
17 | | |
18 | ## Creating sessions | |
19 | | |
20 | 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. | |
21 | | |
22 | :::note | |
23 | | |
24 | 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. | |
25 | | |
26 | ::: | |
27 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/01-connection.md: | |
-------------------------------------------------------------------------------- | |
1 | # Connection | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | | |
8 | ### Connection details | |
9 | | |
10 | Each environment configuration contains a `database` keyword that specifies the connection details. | |
11 | For your development build you can find the connection details in the `config/development.yaml` file. | |
12 | | |
13 | This is an example: | |
14 | | |
15 | ```yaml | |
16 | ... | |
17 | database: | |
18 | host: localhost | |
19 | port: 8090 | |
20 | name: <YOUR_PROJECT_NAME> | |
21 | user: postgres | |
22 | ... | |
23 | ``` | |
24 | | |
25 | 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. | |
26 | | |
27 | :::caution | |
28 | | |
29 | 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. | |
30 | | |
31 | ::: | |
32 | | |
33 | #### Configure search paths | |
34 | | |
35 | You can customize the search paths for your database connection—helpful if you're working with multiple schemas. By default, Postgres uses the `public` schema unless otherwise specified. | |
36 | | |
37 | To override this, use the optional `searchPaths` setting in your configuration: | |
38 | | |
39 | ```yaml | |
40 | ... | |
41 | database: | |
42 | host: localhost | |
43 | port: 8090 | |
44 | name: <YOUR_PROJECT_NAME> | |
45 | user: postgres | |
46 | searchPaths: custom, public | |
47 | ... | |
48 | ``` | |
49 | | |
50 | In this example, Postgres will look for tables in the `custom` schema first, and then fall back to `public` if needed. This gives you more control over where your data lives and how it’s accessed | |
51 | | |
52 | | |
53 | ### Database password | |
54 | | |
55 | 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. | |
56 | | |
57 | An example of this could look like this: | |
58 | | |
59 | ```yaml | |
60 | ... | |
61 | development: | |
62 | database: '<MY DATABASE PASSWORD>' | |
63 | ... | |
64 | ``` | |
65 | | |
66 | ## Development database | |
67 | | |
68 | 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: | |
69 | | |
70 | ```bash | |
71 | $ docker compose up --build --detach | |
72 | ``` | |
73 | | |
74 | To stop the database run: | |
75 | | |
76 | ```bash | |
77 | $ docker compose stop | |
78 | ``` | |
79 | | |
80 | To remove the database and __delete__ all associated data, run: | |
81 | | |
82 | ```bash | |
83 | $ docker compose down -v | |
84 | ``` | |
85 | | |
86 | ## Connecting to a custom Postgres instance | |
87 | | |
88 | 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: | |
89 | | |
90 | - Make sure that your Postgres instance is up and running and is reachable from your Serverpod server. | |
91 | - You will need to create a user with a password, and a database. | |
92 | | |
93 | ### Connecting to a local Postgres server | |
94 | | |
95 | 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. | |
96 | | |
97 | ### Connecting to a remote Postgres server | |
98 | | |
99 | To connect to a remote Postgres server (that you have installed on a VPS or VDS), you need to follow a couple of steps: | |
100 | | |
101 | - Make sure that the Postgres server has a reachable network address and that it accepts incoming traffic. | |
102 | - You may need to open the database port on the machine. This may include configuring its firewall. | |
103 | - Update your Serverpod `database` config to use the public network address, database name, port, user, and password. | |
104 | | |
105 | | |
106 | ### Connecting to Google Cloud SQL | |
107 | | |
108 | You can connect to a Google Cloud SQL Postgres instance in two ways: | |
109 | | |
110 | 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_. | |
111 | 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`. | |
112 | | |
113 | The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. | |
114 | | |
115 | :::info | |
116 | | |
117 | 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. | |
118 | | |
119 | ::: | |
120 | | |
121 | ### Connecting to AWS RDS | |
122 | | |
123 | You can connect to an AWS RDS Instance in two ways: | |
124 | 1. Enable public access to the database and configure VPC/Subnets to accept your Serverpod's IP address. | |
125 | 2. Use the Endpoint `database-name.some-unique-id.server-location.rds.amazonaws.com` to connect to it from AWS ECS. | |
126 | | |
127 | The next step is to update the database password in `passwords.yaml` and the connection details for the desired environment in the `config` folder. | |
128 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/02-models.md: | |
-------------------------------------------------------------------------------- | |
1 | # Models | |
2 | | |
3 | It's possible to map serializable models to tables in your database. To do this, add the `table` key to your yaml file: | |
4 | | |
5 | ```yaml | |
6 | class: Company | |
7 | table: company | |
8 | fields: | |
9 | name: String | |
10 | ``` | |
11 | | |
12 | 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. | |
13 | | |
14 | :::info | |
15 | | |
16 | 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. | |
17 | | |
18 | ::: | |
19 | | |
20 | ### Non persistent fields | |
21 | | |
22 | You can opt out of creating a column in the database for a specific field by using the `!persist` keyword. | |
23 | | |
24 | ```yaml | |
25 | class: Company | |
26 | table: company | |
27 | fields: | |
28 | name: String, !persist | |
29 | ``` | |
30 | | |
31 | All fields are persisted by default and have an implicit `persist` set on each field. | |
32 | | |
33 | ### Data representation | |
34 | | |
35 | 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. | |
36 | | |
37 | ```yaml | |
38 | class: Company | |
39 | table: company | |
40 | fields: | |
41 | address: Address # Stored as a json column | |
42 | ``` | |
43 | | |
44 | 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. | |
45 | | |
46 | This creates a database relation between two tables and always keeps the data in sync. | |
47 | | |
48 | ```yaml | |
49 | class: Company | |
50 | table: company | |
51 | fields: | |
52 | address: Address?, relation | |
53 | ``` | |
54 | | |
55 | For a complete guide on how to work with relations see the [relation section](relations/one-to-one). | |
56 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/01-one-to-one.md: | |
-------------------------------------------------------------------------------- | |
1 | # One-to-one | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Defining the Relationship | |
6 | | |
7 | In the following examples we show how to configure a 1:1 relationship between `User` and `Address`. | |
8 | | |
9 | ### With an id field | |
10 | | |
11 | In the most simple case, all we have to do is add an `id` field on one of the models. | |
12 | | |
13 | ```yaml | |
14 | # address.yaml | |
15 | class: Address | |
16 | table: address | |
17 | fields: | |
18 | street: String | |
19 | | |
20 | # user.yaml | |
21 | class: User | |
22 | table: user | |
23 | fields: | |
24 | addressId: int, relation(parent=address) // Foreign key field | |
25 | indexes: | |
26 | user_address_unique_idx: | |
27 | fields: addressId | |
28 | unique: true | |
29 | ``` | |
30 | | |
31 | 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. | |
32 | | |
33 | 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?`. | |
34 | | |
35 | When fetching a `User` from the database the `addressId` field will automatically be populated with the related `Address` object `id`. | |
36 | | |
37 | ### With an object | |
38 | | |
39 | 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. | |
40 | | |
41 | ```yaml | |
42 | # address.yaml | |
43 | class: Address | |
44 | table: address | |
45 | fields: | |
46 | street: String | |
47 | | |
48 | # user.yaml | |
49 | class: User | |
50 | table: user | |
51 | fields: | |
52 | address: Address?, relation // Object relation field | |
53 | indexes: | |
54 | user_address_unique_idx: | |
55 | fields: addressId | |
56 | unique: true | |
57 | ``` | |
58 | | |
59 | 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?`. | |
60 | | |
61 | 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`. | |
62 | | |
63 | The object field, in this case `address`, must always be nullable (as indicated by `Address?`). | |
64 | | |
65 | 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. | |
66 | | |
67 | No `parent` keyword is needed here because the relational table is inferred from the type on the field. | |
68 | | |
69 | ### Optional relation | |
70 | | |
71 | ```yaml | |
72 | # user.yaml | |
73 | class: User | |
74 | table: user | |
75 | fields: | |
76 | address: Address?, relation(optional) | |
77 | indexes: | |
78 | user_address_unique_idx: | |
79 | fields: addressId | |
80 | unique: true | |
81 | ``` | |
82 | | |
83 | 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. | |
84 | | |
85 | ### Custom foreign key field | |
86 | | |
87 | Serverpod also provides a way to customize the name of the foreign key field used in an object relation. | |
88 | | |
89 | ```yaml | |
90 | # user.yaml | |
91 | class: User | |
92 | table: user | |
93 | fields: | |
94 | customIdField: int | |
95 | address: Address?, relation(field=customIdField) | |
96 | indexes: | |
97 | user_address_unique_idx: | |
98 | fields: customIdField | |
99 | unique: true | |
100 | ``` | |
101 | | |
102 | 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. | |
103 | | |
104 | 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. | |
105 | | |
106 | ### Generated SQL | |
107 | | |
108 | The following code block shows how to set up the same relation with raw SQL. Serverpod will generate this code behind the scenes. | |
109 | | |
110 | ```sql | |
111 | CREATE TABLE "address" ( | |
112 | "id" serial PRIMARY KEY, | |
113 | "street" text NOT NULL | |
114 | ); | |
115 | | |
116 | CREATE TABLE "user" ( | |
117 | "id" serial PRIMARY KEY, | |
118 | "addressId" integer NOT NULL | |
119 | ); | |
120 | | |
121 | | |
122 | CREATE UNIQUE INDEX "user_address_unique_idx" ON "user" USING btree ("addressId"); | |
123 | | |
124 | ALTER TABLE ONLY "user" | |
125 | ADD CONSTRAINT "user_fk_0" | |
126 | FOREIGN KEY("addressId") | |
127 | REFERENCES "address"("id") | |
128 | ON DELETE CASCADE | |
129 | ON UPDATE NO ACTION; | |
130 | ``` | |
131 | | |
132 | ## Independent relations defined on both sides | |
133 | | |
134 | 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. | |
135 | | |
136 | ```yaml | |
137 | # user.yaml | |
138 | class: User | |
139 | table: user | |
140 | fields: | |
141 | friendsAddress: Address?, relation | |
142 | indexes: | |
143 | user_address_unique_idx: | |
144 | fields: friendsAddressId | |
145 | unique: true | |
146 | | |
147 | # address.yaml | |
148 | class: Address | |
149 | table: address | |
150 | fields: | |
151 | street: String | |
152 | resident: User?, relation | |
153 | indexes: | |
154 | address_user_unique_idx: | |
155 | fields: residentId | |
156 | unique: true | |
157 | ``` | |
158 | | |
159 | Both relations operate independently of each other, resulting in two distinct relationships with their respective unique indexes. | |
160 | | |
161 | ## Bidirectional relations | |
162 | | |
163 | If access to the same relation is desired from both sided, a bidirectional relation can be defined. | |
164 | | |
165 | ```yaml | |
166 | # user.yaml | |
167 | class: User | |
168 | table: user | |
169 | fields: | |
170 | addressId: int | |
171 | address: Address?, relation(name=user_address, field=addressId) | |
172 | indexes: | |
173 | user_address_unique_idx: | |
174 | fields: addressId | |
175 | unique: true | |
176 | | |
177 | # address.yaml | |
178 | class: Address | |
179 | table: address | |
180 | fields: | |
181 | street: String | |
182 | user: User?, relation(name=user_address) | |
183 | ``` | |
184 | | |
185 | The example illustrates a 1:1 relationship between User and Address where both sides of the relationship are explicitly specified. | |
186 | | |
187 | 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. | |
188 | | |
189 | Without specifying the `name` parameter, you'd end up with two unrelated relationships. | |
190 | | |
191 | 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. | |
192 | | |
193 | In a relationship where there is an object on both sides a unique index is always **required** on the foreign key field. | |
194 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/02-one-to-many.md: | |
-------------------------------------------------------------------------------- | |
1 | # One-to-many | |
2 | | |
3 | 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. | |
4 | | |
5 | 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`). | |
6 | | |
7 | ## Defining the relationship | |
8 | | |
9 | In the following examples we show how to configure a 1:n relationship between `Company` and `Employee`. | |
10 | | |
11 | ### Implicit definition | |
12 | | |
13 | With an implicit setup, Serverpod determines and establishes the relationship based on the table and class structures. | |
14 | | |
15 | ```yaml | |
16 | # company.yaml | |
17 | class: Company | |
18 | table: company | |
19 | fields: | |
20 | name: String | |
21 | employees: List<Employee>?, relation | |
22 | | |
23 | # employee.yaml | |
24 | class: Employee | |
25 | table: employee | |
26 | fields: | |
27 | name: String | |
28 | ``` | |
29 | | |
30 | In the example, we define a 1:n relation between `Company` and `Employee` by using the `List<Employee>` type on the `employees` field together with the `relation` keyword. | |
31 | | |
32 | The corresponding foreign key field is automatically integrated into the 'many' side (e.g., `Employee`) as a concealed column. | |
33 | | |
34 | 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). | |
35 | | |
36 | ### Explicit definition | |
37 | | |
38 | In an explicit definition, you directly specify the relationship in a one-to-many relation. | |
39 | | |
40 | This can be done by through an [object relation](one-to-one#with-an-object): | |
41 | | |
42 | ```yaml | |
43 | # company.yaml | |
44 | class: Company | |
45 | table: company | |
46 | fields: | |
47 | name: String | |
48 | | |
49 | # employee.yaml | |
50 | class: Employee | |
51 | table: employee | |
52 | fields: | |
53 | name: String | |
54 | company: Company?, relation | |
55 | ``` | |
56 | | |
57 | Or through a [foreign key field](one-to-one#with-an-id-field): | |
58 | | |
59 | ```yaml | |
60 | # company.yaml | |
61 | class: Company | |
62 | table: company | |
63 | fields: | |
64 | name: String | |
65 | | |
66 | # employee.yaml | |
67 | class: Employee | |
68 | table: employee | |
69 | fields: | |
70 | name: String | |
71 | companyId: int, relation | |
72 | ``` | |
73 | | |
74 | 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. | |
75 | | |
76 | ## Bidirectional relations | |
77 | | |
78 | For a more comprehensive representation, you can define the relationship from both sides. | |
79 | | |
80 | Either through an [object relation](one-to-one#with-an-object) on the many side: | |
81 | | |
82 | ```yaml | |
83 | # company.yaml | |
84 | class: Company | |
85 | table: company | |
86 | fields: | |
87 | name: String | |
88 | employees: List<Employee>?, relation(name=company_employees) | |
89 | | |
90 | # employee.yaml | |
91 | class: Employee | |
92 | table: employee | |
93 | fields: | |
94 | name: String | |
95 | company: Company?, relation(name=company_employees) | |
96 | ``` | |
97 | | |
98 | Or through a [foreign key field](one-to-one#with-an-id-field) on the many side: | |
99 | | |
100 | ```yaml | |
101 | # company.yaml | |
102 | class: Company | |
103 | table: company | |
104 | fields: | |
105 | name: String | |
106 | employees: List<Employee>?, relation(name=company_employees) | |
107 | | |
108 | # employee.yaml | |
109 | class: Employee | |
110 | table: employee | |
111 | fields: | |
112 | name: String | |
113 | companyId: int, relation(name=company_employees, parent=company) | |
114 | ``` | |
115 | | |
116 | Just as in the 1:1 examples, the `name` parameter with a unique string that links both sides together. | |
117 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/03-many-to-many.md: | |
-------------------------------------------------------------------------------- | |
1 | # Many-to-many | |
2 | | |
3 | 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. | |
4 | | |
5 | The Serverpod framework supports these complex relationships by explicitly creating a separate model, often called a junction or bridge table, that records the relation. | |
6 | | |
7 | ## Overview | |
8 | | |
9 | 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. | |
10 | | |
11 | Modeling the relationship between `Student` and `Course`, we would create an `Enrollment` model as a junction table to store the relationship explicitly. | |
12 | | |
13 | ## Defining the relationship | |
14 | | |
15 | In the following examples we show how to configure a n:m relationship between `Student` and `Course`. | |
16 | | |
17 | ### Many tables | |
18 | | |
19 | Both the `Course` and `Student` tables have a direct relationship with the `Enrollment` table but no direct relationship with each other. | |
20 | | |
21 | ```yaml | |
22 | # course.yaml | |
23 | class: Course | |
24 | table: course | |
25 | fields: | |
26 | name: String | |
27 | enrollments: List<Enrollment>?, relation(name=course_enrollments) | |
28 | ``` | |
29 | | |
30 | ```yaml | |
31 | # student.yaml | |
32 | class: Student | |
33 | table: student | |
34 | fields: | |
35 | name: String | |
36 | enrollments: List<Enrollment>?, relation(name=student_enrollments) | |
37 | ``` | |
38 | | |
39 | 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`. | |
40 | | |
41 | ### Junction table | |
42 | | |
43 | The `Enrollment` table acts as the bridge between `Course` and `Student`. It contains foreign keys from both tables, representing the many-to-many relationship. | |
44 | | |
45 | ```yaml | |
46 | # enrollment.yaml | |
47 | class: Enrollment | |
48 | table: enrollment | |
49 | fields: | |
50 | student: Student?, relation(name=student_enrollments) | |
51 | course: Course?, relation(name=course_enrollments) | |
52 | indexes: | |
53 | enrollment_index_idx: | |
54 | fields: studentId, courseId | |
55 | unique: true | |
56 | ``` | |
57 | | |
58 | 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. | |
59 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/04-self-relations.md: | |
-------------------------------------------------------------------------------- | |
1 | # Self-relations | |
2 | | |
3 | 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. | |
4 | | |
5 | ## One-to-one | |
6 | | |
7 | 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. | |
8 | | |
9 | ```yaml | |
10 | class: Post | |
11 | table: post | |
12 | fields: | |
13 | content: String | |
14 | previous: Post?, relation(name=next_previous_post) | |
15 | nextId: int? | |
16 | next: Post?, relation(name=next_previous_post, field=nextId, onDelete=SetNull) | |
17 | indexes: | |
18 | next_unique_idx: | |
19 | fields: nextId | |
20 | unique: true | |
21 | ``` | |
22 | | |
23 | 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. | |
24 | | |
25 | ## One-to-many | |
26 | | |
27 | 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. | |
28 | | |
29 | ```yaml | |
30 | class: Cat | |
31 | table: cat | |
32 | fields: | |
33 | name: String | |
34 | mother: Cat?, relation(name=cat_kittens, optional, onDelete=SetNull) | |
35 | kittens: List<Cat>?, relation(name=cat_kittens) | |
36 | ``` | |
37 | | |
38 | 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. | |
39 | | |
40 | ## Many-to-many | |
41 | | |
42 | 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. | |
43 | | |
44 | 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. | |
45 | | |
46 | ```yaml | |
47 | class: Member | |
48 | table: member | |
49 | fields: | |
50 | name: String | |
51 | blocking: List<Blocking>?, relation(name=member_blocked_by_me) | |
52 | blockedBy: List<Blocking>?, relation(name=member_blocking_me) | |
53 | ``` | |
54 | | |
55 | ```yaml | |
56 | class: Blocking | |
57 | table: blocking | |
58 | fields: | |
59 | blocked: Member?, relation(name=member_blocking_me, onDelete=Cascade) | |
60 | blockedBy: Member?, relation(name=member_blocked_by_me, onDelete=Cascade) | |
61 | indexes: | |
62 | blocking_blocked_unique_idx: | |
63 | fields: blockedId, blockedById | |
64 | unique: true | |
65 | ``` | |
66 | | |
67 | 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. | |
68 | | |
69 | The cascade delete means that if a member is deleted all the blocking entries are also removed for that member. | |
70 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/05-referential-actions.md: | |
-------------------------------------------------------------------------------- | |
1 | # Referential actions | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Available referential actions | |
6 | | |
7 | | Action | Description | | |
8 | | --- | --- | | |
9 | | **NoAction** | If any constraint violation occurs, no action will be taken, and an error will be raised. | | |
10 | | **Restrict** | If any referencing rows still exist when the constraint is checked, an error is raised. | | |
11 | | **SetDefault** | The field will revert to its default value. Note: This action necessitates that a default value is configured for the field. | | |
12 | | **Cascade** | Any action taken on the parent (update/delete) will be mirrored in the child. | | |
13 | | **SetNull** | The field value is set to null. This action is permissible only if the field has been marked as optional. | | |
14 | | |
15 | ## Syntax | |
16 | | |
17 | Use the following syntax to apply referential actions | |
18 | | |
19 | ```yaml | |
20 | relation(onUpdate=<ACTION>, onDelete=<ACTION>) | |
21 | ``` | |
22 | | |
23 | ## Default values | |
24 | If no referential actions are specified, the default behavior will be applied. | |
25 | | |
26 | 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. | |
27 | | |
28 | ```yaml | |
29 | parent: Model?, relation(onUpdate=NoAction, onDelete=NoAction) | |
30 | ``` | |
31 | | |
32 | | |
33 | 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. | |
34 | | |
35 | | |
36 | ```yaml | |
37 | parentId: int?, relation(parent=model_table, onUpdate=NoAction, onDelete=Cascade) | |
38 | ``` | |
39 | | |
40 | :::info | |
41 | | |
42 | The sequence of onUpdate and onDelete is interchangeable. | |
43 | | |
44 | ::: | |
45 | | |
46 | ### Full example | |
47 | | |
48 | ```yaml | |
49 | class: Example | |
50 | table: example | |
51 | fields: | |
52 | parentId: int?, relation(parent=example, onUpdate=SetNull, onDelete=NoAction) | |
53 | ``` | |
54 | | |
55 | 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. | |
56 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/03-relations/06-modules.md: | |
-------------------------------------------------------------------------------- | |
1 | # Relations with modules | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | ```yaml | |
8 | class: User | |
9 | table: user | |
10 | fields: | |
11 | userInfo: module:auth:UserInfo?, relation | |
12 | age: int | |
13 | indexes: | |
14 | user_info_id_unique_idx: | |
15 | fields: userInfoId | |
16 | unique: true | |
17 | ``` | |
18 | | |
19 | Or by referencing the table name if you only want to access the id. | |
20 | | |
21 | ```yaml | |
22 | class: User | |
23 | table: user | |
24 | fields: | |
25 | userInfoId: int, relation(parent=serverpod_user_info) | |
26 | age: int | |
27 | indexes: | |
28 | user_info_id_unique_idx: | |
29 | fields: userInfoId | |
30 | unique: true | |
31 | ``` | |
32 | | |
33 | 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). | |
34 | | |
35 | ## Advanced example | |
36 | | |
37 | A one-to-many relation with the "bridge" table could look like this. | |
38 | | |
39 | ```yaml | |
40 | class: User | |
41 | table: user | |
42 | fields: | |
43 | userInfo: module:auth:UserInfo?, relation | |
44 | age: int | |
45 | company: Company?, relation(name=company_employee) | |
46 | indexes: | |
47 | user_info_id_unique_idx: | |
48 | fields: userInfoId | |
49 | unique: true | |
50 | company_unique_idx: | |
51 | fields: companyId | |
52 | unique: true | |
53 | ``` | |
54 | | |
55 | ```yaml | |
56 | class: Company | |
57 | table: company | |
58 | fields: | |
59 | name: String | |
60 | employees: List<User>?, relation(name=company_employee) | |
61 | ``` | |
62 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/04-indexing.md: | |
-------------------------------------------------------------------------------- | |
1 | # Indexing | |
2 | | |
3 | For performance reasons, you may want to add indexes to your database tables. These are added in the YAML-files defining the serializable objects. | |
4 | | |
5 | ### Add an index | |
6 | | |
7 | 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. | |
8 | | |
9 | ```yaml | |
10 | class: Company | |
11 | table: company | |
12 | fields: | |
13 | name: String | |
14 | indexes: | |
15 | company_name_idx: | |
16 | fields: name | |
17 | ``` | |
18 | | |
19 | 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. | |
20 | | |
21 | ```yaml | |
22 | class: Company | |
23 | table: company | |
24 | fields: | |
25 | name: String | |
26 | foundedAt: DateTime | |
27 | indexes: | |
28 | company_idx: | |
29 | fields: name, foundedAt | |
30 | ``` | |
31 | | |
32 | ### Making fields unique | |
33 | | |
34 | 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. | |
35 | | |
36 | ```yaml | |
37 | class: Company | |
38 | table: company | |
39 | fields: | |
40 | name: String | |
41 | indexes: | |
42 | company_name_idx: | |
43 | fields: name | |
44 | unique: true | |
45 | ``` | |
46 | | |
47 | 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. | |
48 | | |
49 | ### Specifying index type | |
50 | | |
51 | It is possible to add a type key to specify the index type. | |
52 | | |
53 | ```yaml | |
54 | class: Company | |
55 | table: company | |
56 | fields: | |
57 | name: String | |
58 | indexes: | |
59 | company_name_idx: | |
60 | fields: name | |
61 | type: brin | |
62 | ``` | |
63 | | |
64 | 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`. | |
65 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/05-crud.md: | |
-------------------------------------------------------------------------------- | |
1 | # CRUD | |
2 | | |
3 | 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. | |
4 | | |
5 | For the following examples we will use this model: | |
6 | | |
7 | ```yaml | |
8 | class: Company | |
9 | table: company | |
10 | fields: | |
11 | name: String | |
12 | ``` | |
13 | | |
14 | :::note | |
15 | | |
16 | 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. | |
17 | | |
18 | ::: | |
19 | | |
20 | ## Create | |
21 | | |
22 | There are two ways to create a new row in the database. | |
23 | | |
24 | ### Inserting a single row | |
25 | | |
26 | 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. | |
27 | | |
28 | ```dart | |
29 | var row = Company(name: 'Serverpod'); | |
30 | var company = await Company.db.insertRow(session, row); | |
31 | ``` | |
32 | | |
33 | ### Inserting several rows | |
34 | | |
35 | 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. | |
36 | | |
37 | ```dart | |
38 | var rows = [Company(name: 'Serverpod'), Company(name: 'Google')]; | |
39 | var companies = await Company.db.insert(session, rows); | |
40 | ``` | |
41 | | |
42 | :::info | |
43 | 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. | |
44 | ::: | |
45 | | |
46 | ## Read | |
47 | | |
48 | There are three different read operations available. | |
49 | | |
50 | ### Finding by id | |
51 | | |
52 | You can retrieve a single row by its `id`. | |
53 | | |
54 | ```dart | |
55 | var company = await Company.db.findById(session, companyId); | |
56 | ``` | |
57 | | |
58 | This operation either returns the model or `null`. | |
59 | | |
60 | ### Finding a single row | |
61 | | |
62 | You can find a single row using an expression. | |
63 | | |
64 | ```dart | |
65 | var company = await Company.db.findFirstRow( | |
66 | session, | |
67 | where: (t) => t.name.equals('Serverpod'), | |
68 | ); | |
69 | ``` | |
70 | | |
71 | This operation returns the first model matching the filtering criteria or `null`. See [filter](filter) and [sort](sort) for all filter operations. | |
72 | | |
73 | :::info | |
74 | 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. | |
75 | ::: | |
76 | | |
77 | ### Finding multiple rows | |
78 | | |
79 | To find multiple rows, use the same principle as for finding a single row. | |
80 | | |
81 | ```dart | |
82 | var companies = await Company.db.find( | |
83 | session, | |
84 | where: (t) => t.id < 100, | |
85 | limit: 50, | |
86 | ); | |
87 | ``` | |
88 | | |
89 | This operation returns a `List` of your models matching the filtering criteria. | |
90 | | |
91 | See [filter](filter) and [sort](sort) for all filter and sorting operations and [pagination](pagination) for how to paginate the result. | |
92 | | |
93 | ## Update | |
94 | | |
95 | There are two update operations available. | |
96 | | |
97 | ### Update a single row | |
98 | | |
99 | To update a single row, use the `updateRow` method. | |
100 | | |
101 | ```dart | |
102 | var company = await Company.db.findById(session, companyId); // Fetched company has its id set | |
103 | company.name = 'New name'; | |
104 | var updatedCompany = await Company.db.updateRow(session, company); | |
105 | ``` | |
106 | | |
107 | 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. | |
108 | | |
109 | ### Update several rows | |
110 | | |
111 | To batch update several rows use the `update` method. | |
112 | | |
113 | ```dart | |
114 | var companies = await Company.db.find(session); | |
115 | companies = companies.map((c) => c.copyWith(name: 'New name')).toList(); | |
116 | var updatedCompanies = await Company.db.update(session, companies); | |
117 | ``` | |
118 | | |
119 | 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. | |
120 | | |
121 | ### Update a specific column | |
122 | | |
123 | 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. | |
124 | | |
125 | Update a single row, the following code will update the company name, but will not change the address column. | |
126 | | |
127 | ```dart | |
128 | var company = await Company.db.findById(session, companyId); | |
129 | company.name = 'New name'; | |
130 | company.address = 'Baker street'; | |
131 | var updatedCompany = await Company.db.updateRow(session, company, columns: (t) => [t.name]); | |
132 | ``` | |
133 | | |
134 | The same syntax is available for multiple rows. | |
135 | | |
136 | ```dart | |
137 | var companies = await Company.db.find(session); | |
138 | companies = companies.map((c) => c.copyWith(name: 'New name', address: 'Baker Street')).toList(); | |
139 | var updatedCompanies = await Company.db.update(session, companies, columns: (t) => [t.name]); | |
140 | ``` | |
141 | | |
142 | ## Delete | |
143 | | |
144 | Deleting rows from the database is done in a similar way to updating rows. However, there are three delete operations available. | |
145 | | |
146 | ### Delete a single row | |
147 | | |
148 | To delete a single row, use the `deleteRow` method. | |
149 | | |
150 | ```dart | |
151 | var company = await Company.db.findById(session, companyId); // Fetched company has its id set | |
152 | var companyDeleted = await Company.db.deleteRow(session, company); | |
153 | ``` | |
154 | | |
155 | The input object needs to have the `id` field set. The `deleteRow` method returns the deleted model. | |
156 | | |
157 | ### Delete several rows | |
158 | | |
159 | To batch delete several rows, use the `delete` method. | |
160 | | |
161 | ```dart | |
162 | var companiesDeleted = await Company.db.delete(session, companies); | |
163 | ``` | |
164 | | |
165 | 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. | |
166 | | |
167 | ### Delete by filter | |
168 | | |
169 | You can also do a [filtered](filter) delete and delete all entries matching a `where` query, by using the `deleteWhere` method. | |
170 | | |
171 | ```dart | |
172 | var companiesDeleted = await Company.db.deleteWhere( | |
173 | session, | |
174 | where: (t) => t.name.like('%Ltd'), | |
175 | ); | |
176 | ``` | |
177 | | |
178 | The above example will delete any row that ends in *Ltd*. The `deleteWhere` method returns a `List` of the models deleted. | |
179 | | |
180 | ## Count | |
181 | | |
182 | Count is a special type of query that helps counting the number of rows in the database that matches a specific [filter](filter). | |
183 | | |
184 | ```dart | |
185 | var count = await Company.db.count( | |
186 | session, | |
187 | where: (t) => t.name.like('s%'), | |
188 | ); | |
189 | ``` | |
190 | | |
191 | The return value is an `int` for the number of rows matching the filter. | |
192 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/06-filter.md: | |
-------------------------------------------------------------------------------- | |
1 | # Filter | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Column operations | |
6 | | |
7 | The following column operations are supported in Serverpod, each column datatype supports a different set of operations that make sense for that type. | |
8 | | |
9 | :::info | |
10 | 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. | |
11 | ::: | |
12 | | |
13 | ### Equals | |
14 | | |
15 | Compare a column to an exact value, meaning only rows that match exactly will remain in the result. | |
16 | | |
17 | ```dart | |
18 | await User.db.find( | |
19 | where: (t) => t.name.equals('Alice') | |
20 | ); | |
21 | ``` | |
22 | | |
23 | In the example we fetch all users with the name Alice. | |
24 | | |
25 | Not equals is the negated version of equals. | |
26 | | |
27 | ```dart | |
28 | await User.db.find( | |
29 | where: (t) => t.name.notEquals('Bob') | |
30 | ); | |
31 | ``` | |
32 | | |
33 | 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. | |
34 | | |
35 | ### Comparison operators | |
36 | | |
37 | Compare a column to a value, these operators are support for `int`, `double`, `Duration`, and `DateTime`. | |
38 | | |
39 | ```dart | |
40 | await User.db.find( | |
41 | where: (t) => t.age > 25 | |
42 | ); | |
43 | ``` | |
44 | | |
45 | In the example we fetch all users that are older than 25 years old. | |
46 | | |
47 | ```dart | |
48 | await User.db.find( | |
49 | where: (t) => t.age >= 25 | |
50 | ); | |
51 | ``` | |
52 | | |
53 | In the example we fetch users that are 25 years old or older. | |
54 | | |
55 | ```dart | |
56 | await User.db.find( | |
57 | where: (t) => t.age < 25 | |
58 | ); | |
59 | ``` | |
60 | | |
61 | In the example we fetch all users that are younger than 25 years old. | |
62 | | |
63 | ```dart | |
64 | await User.db.find( | |
65 | where: (t) => t.age <= 25 | |
66 | ); | |
67 | ``` | |
68 | | |
69 | In the example we fetch all users that are 25 years old or younger. | |
70 | | |
71 | ### Between | |
72 | | |
73 | The between method takes two values and checks if the columns value is between the two input variables *inclusively*. | |
74 | | |
75 | ```dart | |
76 | await User.db.find( | |
77 | where: (t) => t.age.between(18, 65) | |
78 | ); | |
79 | ``` | |
80 | | |
81 | 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)`. | |
82 | | |
83 | The 'not between' operation functions similarly to 'between' but it negates the condition. It also works inclusively with the boundaries. | |
84 | | |
85 | ```dart | |
86 | await User.db.find( | |
87 | where: (t) => t.age.notBetween(18, 65) | |
88 | ); | |
89 | ``` | |
90 | | |
91 | 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)`. | |
92 | | |
93 | ### In set | |
94 | | |
95 | 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. | |
96 | | |
97 | ```dart | |
98 | await User.db.find( | |
99 | where: (t) => t.name.inSet({'Alice', 'Bob'}) | |
100 | ); | |
101 | ``` | |
102 | | |
103 | 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. | |
104 | | |
105 | The 'not in set' operation functions similarly to `inSet`, but it negates the condition. | |
106 | | |
107 | ```dart | |
108 | await User.db.find( | |
109 | where: (t) => t.name.notInSet({'Alice', 'Bob'}) | |
110 | ); | |
111 | ``` | |
112 | | |
113 | 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. | |
114 | | |
115 | ### Like | |
116 | | |
117 | 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. | |
118 | | |
119 | Two special characters enables matching against partial entries. | |
120 | | |
121 | - **`%`** Matching any sequence of character. | |
122 | - **`_`** Matching any single character. | |
123 | | |
124 | | String | Matcher | Is matching | | |
125 | |--|--|--| | |
126 | | abc | a% | true | | |
127 | | abc | _b% | true | | |
128 | | abc | a_c | true | | |
129 | | abc | b_ | false | | |
130 | | |
131 | We use like to match against a partial string. | |
132 | | |
133 | ```dart | |
134 | await User.db.find( | |
135 | where: (t) => t.name.like('A%') | |
136 | ); | |
137 | ``` | |
138 | | |
139 | In the example we fetch all users with a name that starts with A. | |
140 | | |
141 | There is a negated version of like that can be used to exclude rows from the result. | |
142 | | |
143 | ```dart | |
144 | await User.db.find( | |
145 | where: (t) => t.name.notLike('B%') | |
146 | ); | |
147 | ``` | |
148 | | |
149 | In the example we fetch all users with a name that does not start with B. | |
150 | | |
151 | ### ilike | |
152 | | |
153 | `ilike` works the same as `like` but is case-insensitive. | |
154 | | |
155 | ```dart | |
156 | await User.db.find( | |
157 | where: (t) => t.name.ilike('a%') | |
158 | ); | |
159 | ``` | |
160 | | |
161 | In the example we fetch all users with a name that starts with a or A. | |
162 | | |
163 | There is a negated version of `ilike` that can be used to exclude rows from the result. | |
164 | | |
165 | ```dart | |
166 | await User.db.find( | |
167 | where: (t) => t.name.notIlike('b%') | |
168 | ); | |
169 | ``` | |
170 | | |
171 | In the example we fetch all users with a name that does not start with b or B. | |
172 | | |
173 | ### Logical operators | |
174 | | |
175 | Logical operators are also supported when filtering, allowing you to chain multiple statements together to create more complex queries. | |
176 | | |
177 | The `&` operator is used to chain two statements together with an `and` operation. | |
178 | | |
179 | ```dart | |
180 | await User.db.find( | |
181 | where: (t) => (t.name.equals('Alice') & (t.age > 25)) | |
182 | ); | |
183 | ``` | |
184 | | |
185 | In the example we fetch all users with the name "Alice" *and* are older than 25. | |
186 | | |
187 | The `|` operator is used to chain two statements together with an `or` operation. | |
188 | | |
189 | ```dart | |
190 | await User.db.find( | |
191 | where: (t) => (t.name.like('A%') | t.name.like('B%')) | |
192 | ); | |
193 | ``` | |
194 | | |
195 | In the example we fetch all users that has a name that starts with A *or* B. | |
196 | | |
197 | ## Relation operations | |
198 | | |
199 | 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. | |
200 | | |
201 | ### One-to-one | |
202 | | |
203 | For 1:1 relations the columns of the relation can be accessed directly on the relation field. This enables filtering on related objects properties. | |
204 | | |
205 | ```dart | |
206 | await User.db.find( | |
207 | where: (t) => t.address.street.like('%road%') | |
208 | ); | |
209 | ``` | |
210 | | |
211 | 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". | |
212 | | |
213 | ### One-to-many | |
214 | | |
215 | 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. | |
216 | | |
217 | #### Count | |
218 | | |
219 | 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. | |
220 | | |
221 | ```dart | |
222 | await User.db.find( | |
223 | where: (t) => t.orders.count() > 3 | |
224 | ); | |
225 | ``` | |
226 | | |
227 | In the example we fetch all users with more than three orders. | |
228 | | |
229 | We can apply a sub-filter to the `count` operator filter the related entries before they are counted. | |
230 | | |
231 | ```dart | |
232 | await User.db.find( | |
233 | where: (t) => t.orders.count((o) => o.itemType.equals('book')) > 3 | |
234 | ); | |
235 | ``` | |
236 | | |
237 | In the example we fetch all users with more than three "book" orders. | |
238 | | |
239 | #### None | |
240 | | |
241 | 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. | |
242 | | |
243 | ```dart | |
244 | await User.db.find( | |
245 | where: (t) => t.orders.none() | |
246 | ); | |
247 | ``` | |
248 | | |
249 | In the example we fetch all users that have no orders. | |
250 | | |
251 | 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. | |
252 | | |
253 | ```dart | |
254 | await User.db.find( | |
255 | where:((t) => t.orders.none((o) => o.itemType.equals('book'))) | |
256 | ); | |
257 | ``` | |
258 | | |
259 | In the example we fetch all users that have no "book" orders. | |
260 | | |
261 | #### Any | |
262 | | |
263 | Any works similarly to the `any` method on lists in Dart. If there exists any related entry then include the row in the result. | |
264 | | |
265 | ```dart | |
266 | await User.db.find( | |
267 | where: (t) => t.orders.any() | |
268 | ); | |
269 | ``` | |
270 | | |
271 | In the example we fetch all users that have any order. | |
272 | | |
273 | 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. | |
274 | | |
275 | ```dart | |
276 | await User.db.find( | |
277 | where:((t) => t.orders.any((o) => o.itemType.equals('book'))) | |
278 | ); | |
279 | ``` | |
280 | | |
281 | In the example we fetch all users that have any "book" order. | |
282 | | |
283 | #### Every | |
284 | | |
285 | 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. | |
286 | | |
287 | ```dart | |
288 | await User.db.find( | |
289 | where: (t) => t.orders.every((o) => o.itemType.equals('book')) | |
290 | ); | |
291 | ``` | |
292 | | |
293 | In the example we fetch all users that have only "book" orders. | |
294 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/07-relation-queries.md: | |
-------------------------------------------------------------------------------- | |
1 | # Relation queries | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Include relational data | |
6 | | |
7 | 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. | |
8 | | |
9 | ```dart | |
10 | var employee = await Employee.db.findById( | |
11 | session, | |
12 | employeeId, | |
13 | include: Employee.include( | |
14 | address: Address.include(), | |
15 | ), | |
16 | ); | |
17 | ``` | |
18 | | |
19 | The example above return a employee including the related address object. | |
20 | | |
21 | ### Nested includes | |
22 | | |
23 | It is also possible to include deeply nested objects. | |
24 | | |
25 | ```dart | |
26 | var employee = await Employee.db.findById( | |
27 | session, | |
28 | employeeId, | |
29 | include: Employee.include( | |
30 | company: Company.include( | |
31 | address: Address.include(), | |
32 | ), | |
33 | ), | |
34 | ); | |
35 | ``` | |
36 | | |
37 | The example above returns an employee including the related company object that has the related address object included. | |
38 | | |
39 | 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. | |
40 | | |
41 | ```dart | |
42 | var user = await Employee.db.findById( | |
43 | session, | |
44 | employeeId, | |
45 | include: Employee.include( | |
46 | address: Address.include(), | |
47 | company: Company.include( | |
48 | address: Address.include(), | |
49 | ), | |
50 | ), | |
51 | ); | |
52 | ``` | |
53 | | |
54 | The example above includes several different objects configured by specifying the named parameters. | |
55 | | |
56 | ## Include relational lists | |
57 | | |
58 | 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. | |
59 | | |
60 | ```dart | |
61 | var user = await Company.db.findById( | |
62 | session, | |
63 | employeeId, | |
64 | include: Company.include( | |
65 | employees: Employee.includeList(), | |
66 | ), | |
67 | ); | |
68 | ``` | |
69 | | |
70 | The example above returns a company with all related employees included. | |
71 | | |
72 | ### Nested includes | |
73 | | |
74 | 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. | |
75 | | |
76 | ```dart | |
77 | var user = await Company.db.findById( | |
78 | session, | |
79 | employeeId, | |
80 | include: Company.include( | |
81 | employees: Employee.includeList( | |
82 | includes: Employee.include( | |
83 | address: Address.include(), | |
84 | ), | |
85 | ), | |
86 | ), | |
87 | ); | |
88 | ``` | |
89 | | |
90 | The example above returns a company with all related employees included. Each employee will have the related address object included. | |
91 | | |
92 | It is even possible to include lists within lists. | |
93 | | |
94 | ```dart | |
95 | var user = await Company.db.findById( | |
96 | session, | |
97 | employeeId, | |
98 | include: Company.include( | |
99 | employees: Employee.includeList( | |
100 | includes: Employee.include( | |
101 | tools: Tool.includeList(), | |
102 | ), | |
103 | ), | |
104 | ), | |
105 | ); | |
106 | ``` | |
107 | | |
108 | The example above returns a company with all related employees included. Each employee will have the related tools list included. | |
109 | | |
110 | :::note | |
111 | For each call to includeList (nested or not) the Serverpod Framework will perform one additional query to the database. | |
112 | ::: | |
113 | | |
114 | ### Filter and sort | |
115 | | |
116 | 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: | |
117 | | |
118 | #### Filter | |
119 | | |
120 | Use the `where` clause to filter the results based on certain conditions. | |
121 | | |
122 | ```dart | |
123 | var user = await Company.db.findById( | |
124 | session, | |
125 | employeeId, | |
126 | include: Company.include( | |
127 | employees: Employee.includeList( | |
128 | where: (t) => t.name.ilike('a%') | |
129 | ), | |
130 | ), | |
131 | ); | |
132 | ``` | |
133 | | |
134 | The example above retrieves only employees whose names start with the letter 'a'. | |
135 | | |
136 | #### Sort | |
137 | | |
138 | The orderBy clause lets you sort the results based on a specific field. | |
139 | | |
140 | ```dart | |
141 | var user = await Company.db.findById( | |
142 | session, | |
143 | employeeId, | |
144 | include: Company.include( | |
145 | employees: Employee.includeList( | |
146 | orderBy: (t) => t.name, | |
147 | ), | |
148 | ), | |
149 | ); | |
150 | ``` | |
151 | | |
152 | The example above sorts the employees by their names in ascending order. | |
153 | | |
154 | ### Pagination | |
155 | | |
156 | [Paginate](pagination) results by specifying a limit on the number of records and an offset. | |
157 | | |
158 | ```dart | |
159 | var user = await Company.db.findById( | |
160 | session, | |
161 | employeeId, | |
162 | include: Company.include( | |
163 | employees: Employee.includeList( | |
164 | limit: 10, | |
165 | offset: 10, | |
166 | ), | |
167 | ), | |
168 | ); | |
169 | ``` | |
170 | | |
171 | The example above retrieves the next 10 employees starting from the 11th record: | |
172 | | |
173 | Using these methods in conjunction provides a powerful way to query, filter, and sort relational data efficiently. | |
174 | | |
175 | ## Update | |
176 | | |
177 | Managing relationships between tables is a common task. Serverpod provides methods to link (attach) and unlink (detach) related records: | |
178 | | |
179 | ### Attach single row | |
180 | | |
181 | Link an individual employee to a company. This operation associates an employee with a specific company: | |
182 | | |
183 | ```dart | |
184 | var company = await Company.db.findById(session, companyId); | |
185 | var employee = await Employee.db.findById(session, employeeId); | |
186 | | |
187 | await Company.db.attachRow.employees(session, company!, employee!); | |
188 | ``` | |
189 | | |
190 | ### Bulk attach rows | |
191 | | |
192 | 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: | |
193 | | |
194 | ```dart | |
195 | var company = await Company.db.findById(session, companyId); | |
196 | var employee = await Employee.db.findById(session, employeeId); | |
197 | | |
198 | await Company.db.attach.employees(session, company!, [employee!]); | |
199 | ``` | |
200 | | |
201 | ### Detach single row | |
202 | | |
203 | To remove the association between an employee and a company, use the detach row method: | |
204 | | |
205 | ```dart | |
206 | var employee = await Employee.db.findById(session, employeeId); | |
207 | | |
208 | await Company.db.detachRow.employees(session, employee!); | |
209 | ``` | |
210 | | |
211 | ### Bulk detach rows | |
212 | | |
213 | In cases where you need to remove associations for multiple employees simultaneously, use the bulk detach method. This operation is atomic: | |
214 | | |
215 | ```dart | |
216 | var employee = await Employee.db.findById(session, employeeId); | |
217 | | |
218 | await Company.db.detach.employees(session, [employee!]); | |
219 | ``` | |
220 | | |
221 | :::note | |
222 | When using the attach and detach methods the objects passed to them have to have the `id` field set. | |
223 | | |
224 | 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. | |
225 | ::: | |
226 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/08-sort.md: | |
-------------------------------------------------------------------------------- | |
1 | # Sort | |
2 | | |
3 | 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. | |
4 | | |
5 | ```dart | |
6 | var companies = await Company.db.find( | |
7 | session, | |
8 | orderBy: (t) => t.name, | |
9 | ); | |
10 | ``` | |
11 | | |
12 | In the example we fetch all companies and sort them by their name. | |
13 | | |
14 | By default the order is set to ascending, this can be changed to descending by setting the param `orderDecending: true`. | |
15 | | |
16 | ```dart | |
17 | var companies = await Company.db.find( | |
18 | session, | |
19 | orderBy: (t) => t.name, | |
20 | orderDescending: true, | |
21 | ); | |
22 | ``` | |
23 | | |
24 | In the example we fetch all companies and sort them by their name in descending order. | |
25 | | |
26 | To order by several different columns use `orderByList`, note that this cannot be used in conjunction with `orderBy` and `orderDescending`. | |
27 | | |
28 | ```dart | |
29 | var companies = await Company.db.find( | |
30 | session, | |
31 | orderByList: (t) => [ | |
32 | Order(column: t.name, orderDescending: true), | |
33 | Order(column: t.id), | |
34 | ], | |
35 | ); | |
36 | ``` | |
37 | | |
38 | In the example we fetch all companies and sort them by their name in descending order, and then by their id in ascending order. | |
39 | | |
40 | ## Sort on relations | |
41 | | |
42 | To sort based on a field from a related model, use the chained field reference. | |
43 | | |
44 | ```dart | |
45 | var companies = await Company.db.find( | |
46 | session, | |
47 | orderBy: (t) => t.ceo.name, | |
48 | ); | |
49 | ``` | |
50 | | |
51 | In the example we fetch all companies and sort them by their CEO's name. | |
52 | | |
53 | You can order results based on the count of a list relation (1:n). | |
54 | | |
55 | ```dart | |
56 | var companies = await Company.db.find( | |
57 | session, | |
58 | orderBy: (t) => t.employees.count(), | |
59 | ); | |
60 | ``` | |
61 | | |
62 | In the example we fetch all companies and sort them by the number of employees. | |
63 | | |
64 | The count used for sorting can also be filtered using a sub-filter. | |
65 | | |
66 | ```dart | |
67 | var companies = await Company.db.find( | |
68 | session, | |
69 | orderBy: (t) => t.employees.count( | |
70 | (employee) => employee.role.equals('developer'), | |
71 | ), | |
72 | ); | |
73 | ``` | |
74 | | |
75 | In the example we fetch all companies and sort them by the number of employees with the role of "developer". | |
76 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/08-transactions.md: | |
-------------------------------------------------------------------------------- | |
1 | # Transactions | |
2 | | |
3 | 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. | |
4 | | |
5 | Serverpod handles database transactions through the `session.db.transaction` method. The method takes a callback function that receives a transaction object. | |
6 | | |
7 | 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. | |
8 | | |
9 | Simply pass the transaction object to each database operation method to include them in the same atomic operation: | |
10 | | |
11 | ```dart | |
12 | var result = await session.db.transaction((transaction) async { | |
13 | // Do some database queries here. | |
14 | await Company.db.insertRow(session, company, transaction: transaction); | |
15 | await Employee.db.insertRow(session, employee, transaction: transaction); | |
16 | | |
17 | // Optionally return a value. | |
18 | return true; | |
19 | }); | |
20 | ``` | |
21 | | |
22 | 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`. | |
23 | | |
24 | ## Transaction isolation | |
25 | | |
26 | 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. | |
27 | | |
28 | :::info | |
29 | | |
30 | At the time of writing, the default isolation level for the PostgreSQL database engine is `IsolationLevel.readCommitted`. | |
31 | | |
32 | ::: | |
33 | | |
34 | To set the isolation level, configure the `isolationLevel` property of the `TransactionSettings` object: | |
35 | | |
36 | ```dart | |
37 | await session.db.transaction( | |
38 | (transaction) async { | |
39 | await Company.db.insertRow(session, company, transaction: transaction); | |
40 | await Employee.db.insertRow(session, employee, transaction: transaction); | |
41 | }, | |
42 | settings: TransactionSettings(isolationLevel: IsolationLevel.serializable), | |
43 | ); | |
44 | ``` | |
45 | | |
46 | In the example the isolation level is set to `IsolationLevel.serializable`. | |
47 | | |
48 | The available isolation levels are: | |
49 | | |
50 | | Isolation Level | Constant | Description | | |
51 | |-----------------|-----------------------|-------------| | |
52 | | Read uncommitted | `IsolationLevel.readUncommitted` | Exhibits the same behavior as `IsolationLevel.readCommitted` in PostgresSQL | | |
53 | | Read committed | `IsolationLevel.readCommitted` | Each statement in the transaction sees a snapshot of the database as of the beginning of that statement. | | |
54 | | 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. | | |
55 | | Serializable | `IsolationLevel.serializable` | Gives the same guarantees as `IsolationLevel.repeatableRead` but also throws if read rows are updated by other transactions. | | |
56 | | |
57 | For a detailed explanation of the different isolation levels, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/transaction-iso.html). | |
58 | | |
59 | ## Savepoints | |
60 | | |
61 | 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. | |
62 | | |
63 | Read more about savepoints in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-savepoint.html). | |
64 | | |
65 | ### Creating savepoints | |
66 | To create a savepoint, call the `createSavepoint` method on the transaction object: | |
67 | | |
68 | ```dart | |
69 | await session.db.transaction((transaction) async { | |
70 | await Company.db.insertRow(session, company, transaction: transaction); | |
71 | // Create savepoint | |
72 | var savepoint = await transaction.createSavepoint(); | |
73 | await Employee.db.insertRow(session, employee, transaction: transaction); | |
74 | }); | |
75 | ``` | |
76 | | |
77 | 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. | |
78 | | |
79 | #### Rolling back to savepoints | |
80 | | |
81 | Once a savepoint is created, you can roll back to it by calling the `rollback` method on the savepoint object: | |
82 | | |
83 | ```dart | |
84 | await session.db.transaction((transaction) async { | |
85 | // Changes preserved in the database | |
86 | await Company.db.insertRow(session, company, transaction: transaction); | |
87 | | |
88 | // Create savepoint | |
89 | var savepoint = await transaction.createSavepoint(); | |
90 | | |
91 | await Employee.db.insertRow(session, employee, transaction: transaction); | |
92 | // Changes rolled back | |
93 | await savepoint.rollback(); | |
94 | }); | |
95 | ``` | |
96 | | |
97 | 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. | |
98 | | |
99 | #### Releasing savepoints | |
100 | | |
101 | 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. | |
102 | | |
103 | To release a savepoint, call the `release` method on the savepoint object: | |
104 | | |
105 | ```dart | |
106 | await session.db.transaction((transaction) async { | |
107 | // Create two savepoints | |
108 | var savepoint = await transaction.createSavepoint(); | |
109 | var secondSavepoint = await transaction.createSavepoint(); | |
110 | | |
111 | await Company.db.insertRow(session, company, transaction: transaction); | |
112 | await savepoint.release(); | |
113 | }); | |
114 | ``` | |
115 | | |
116 | 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. | |
117 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/09-pagination.md: | |
-------------------------------------------------------------------------------- | |
1 | # Pagination | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Limit | |
6 | | |
7 | 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. | |
8 | | |
9 | ```dart | |
10 | var companies = await Company.db.find( | |
11 | session, | |
12 | limit: 10, | |
13 | ); | |
14 | ``` | |
15 | | |
16 | In the example we fetch the first 10 companies. | |
17 | | |
18 | ## Offset | |
19 | | |
20 | The `offset` parameter determines the starting point from which to retrieve records. It essentially skips the first `n` records. | |
21 | | |
22 | ```dart | |
23 | var companies = await Company.db.find( | |
24 | session, | |
25 | limit: 10, | |
26 | offset: 30, | |
27 | ); | |
28 | ``` | |
29 | | |
30 | In the example we skip the first 30 rows and fetch the 31st to 40th company. | |
31 | | |
32 | ## Using limit and offset for pagination | |
33 | | |
34 | Together, `limit` and `offset` can be used to implement pagination. | |
35 | | |
36 | ```dart | |
37 | int page = 3; | |
38 | int companiesPerPage = 10; | |
39 | | |
40 | var companies = await Company.db.find( | |
41 | session, | |
42 | orderBy: (t) => t.id, | |
43 | limit: companiesPerPage, | |
44 | offset: (page - 1) * companiesPerPage, | |
45 | ); | |
46 | ``` | |
47 | | |
48 | In the example we fetch the third page of companies, with 10 companies per page. | |
49 | | |
50 | ### Tips | |
51 | | |
52 | 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. | |
53 | 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. | |
54 | 3. **Page numbering**: Page numbers usually start from 1. Adjust the offset calculation accordingly. | |
55 | | |
56 | ## Cursor-based pagination | |
57 | | |
58 | A limit-offset pagination may not be the best solution if the table is changed frequently and rows are added or removed between requests. | |
59 | | |
60 | 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. | |
61 | | |
62 | ### How it works | |
63 | | |
64 | 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`. | |
65 | | |
66 | ### Implementing cursor-based pagination | |
67 | | |
68 | 1. **Initial request**: | |
69 | For the initial request, where no cursor is provided, retrieve the first `n` records: | |
70 | | |
71 | ```dart | |
72 | int recordsPerPage = 10; | |
73 | | |
74 | var companies = await Company.db.find( | |
75 | session, | |
76 | orderBy: (t) => t.id, | |
77 | limit: recordsPerPage, | |
78 | ); | |
79 | ``` | |
80 | | |
81 | 2. **Subsequent requests**: | |
82 | For the subsequent requests, use the cursor (for example, the last `id` from the previous result) to fetch the next set of records: | |
83 | | |
84 | ```dart | |
85 | int cursor = lastCompanyIdFromPreviousPage; // This is typically sent by the client | |
86 | | |
87 | var companies = await Company.db.find( | |
88 | session, | |
89 | where: Company.t.id > cursor, | |
90 | orderBy: (t) => t.id, | |
91 | limit: recordsPerPage, | |
92 | ); | |
93 | ``` | |
94 | | |
95 | 3. **Returning the cursor**: | |
96 | When returning data to the client, also return the cursor, so it can be used to compute the starting point for the next page. | |
97 | | |
98 | ```dart | |
99 | return { | |
100 | 'data': companies, | |
101 | 'lastCursor': companies.last.id, | |
102 | }; | |
103 | ``` | |
104 | | |
105 | ### Tips | |
106 | | |
107 | 1. **Choosing a cursor**: While IDs are commonly used as cursors, timestamps or other unique, sequentially ordered fields can also serve as effective cursors. | |
108 | 2. **Backward pagination**: To implement backward pagination, use the first item from the current page as the cursor and adjust the query accordingly. | |
109 | 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. | |
110 | 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. | |
111 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/10-raw-access.md: | |
-------------------------------------------------------------------------------- | |
1 | # Raw access | |
2 | | |
3 | The library provides methods to execute raw SQL queries directly on the database for advanced scenarios. | |
4 | | |
5 | ## `unsafeQuery` | |
6 | | |
7 | 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. | |
8 | | |
9 | ```dart | |
10 | DatabaseResult result = await session.db.unsafeQuery( | |
11 | r'SELECT * FROM mytable WHERE id = @id', | |
12 | parameters: QueryParameters.named({'id': 1}), | |
13 | ); | |
14 | ``` | |
15 | | |
16 | ## `unsafeExecute` | |
17 | | |
18 | 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. | |
19 | | |
20 | ```dart | |
21 | int result = await session.db.unsafeExecute( | |
22 | r'DELETE FROM mytable WHERE id = @id', | |
23 | parameters: QueryParameters.named({'id': 1}), | |
24 | ); | |
25 | ``` | |
26 | | |
27 | ## `unsafeSimpleQuery` | |
28 | | |
29 | 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.** | |
30 | | |
31 | Simple query mode is suitable for: | |
32 | | |
33 | * Queries containing multiple statements. | |
34 | * Situations where the extended query protocol is not available (e.g., replication mode or with proxies like PGBouncer). | |
35 | | |
36 | | |
37 | ```dart | |
38 | DatabaseResult result = await session.db.unsafeSimpleQuery( | |
39 | r'SELECT * FROM mytable WHERE id = 1; SELECT * FROM othertable;' | |
40 | ); | |
41 | ``` | |
42 | | |
43 | ## `unsafeSimpleExecute` | |
44 | | |
45 | Similar to `unsafeExecute`, but uses the simple query protocol. It does not return any results. **Use with extreme caution and only when absolutely necessary.** | |
46 | | |
47 | Simple query mode is suitable for the same scenarios as `unsafeSimpleQuery`. | |
48 | | |
49 | ```dart | |
50 | int result = await session.db.unsafeSimpleExecute( | |
51 | r'DELETE FROM mytable WHERE id = 1; DELETE FROM othertable;' | |
52 | ); | |
53 | ``` | |
54 | | |
55 | ## Query parameters | |
56 | | |
57 | 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: | |
58 | | |
59 | * **Named parameters:** Use `@` to denote named parameters in your query and pass a `Map` of parameter names and values. | |
60 | * **Positional parameters:** Use `$1`, `$2`, etc., to denote positional parameters and pass a `List` of parameter values in the correct order. | |
61 | | |
62 | ```dart | |
63 | // Named parameters | |
64 | var result = await db.unsafeQuery( | |
65 | r'SELECT id FROM apparel WHERE color = @color AND size = @size', | |
66 | QueryParameters.named({ | |
67 | 'color': 'green', | |
68 | 'size': 'XL', | |
69 | })); | |
70 | | |
71 | // Positional parameters | |
72 | var result = await db.unsafeQuery( | |
73 | r'SELECT id FROM apparel WHERE color = $1 AND size = $2', | |
74 | QueryParameters.positional(['green', 'XL']), | |
75 | ); | |
76 | ``` | |
77 | | |
78 | :::danger | |
79 | 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. | |
80 | ::: | |
81 | | |
82 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/06-database/11-migrations.md: | |
-------------------------------------------------------------------------------- | |
1 | # Migrations | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | 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. | |
8 | | |
9 | ## Opt out of migrations | |
10 | | |
11 | 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. | |
12 | | |
13 | ```yaml | |
14 | class: Example | |
15 | table: example | |
16 | managedMigration: false | |
17 | fields: | |
18 | name: String | |
19 | ``` | |
20 | | |
21 | 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: | |
22 | | |
23 | - 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. | |
24 | - 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. | |
25 | | |
26 | ## Creating a migration | |
27 | | |
28 | To create a migration navigate to your project's `server` package directory and run the `create-migration` command. | |
29 | | |
30 | ```bash | |
31 | $ serverpod create-migration | |
32 | ``` | |
33 | | |
34 | 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. | |
35 | | |
36 | If no previous migration exists it will create a migration assuming there is no initial state. | |
37 | | |
38 | 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. | |
39 | | |
40 | ### Force create migration | |
41 | | |
42 | The migration command aborts and displays an error under two conditions: | |
43 | | |
44 | 1. When no changes are identified between the database schema in the latest migration and the schema required by the project. | |
45 | 2. When there is a risk of data loss. | |
46 | | |
47 | To override these safeguards and force the creation of a migration, use the `--force` flag. | |
48 | | |
49 | ```bash | |
50 | $ serverpod create-migration --force | |
51 | ``` | |
52 | | |
53 | ### Tag migration | |
54 | | |
55 | 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. | |
56 | | |
57 | ```bash | |
58 | $ serverpod create-migration --tag "v1-0-0" | |
59 | ``` | |
60 | | |
61 | This would create a migration named `<timestamp>-v1-0-0`: | |
62 | | |
63 | ```text | |
64 | ├── migrations | |
65 | │ └── 20231205080937028-v1-0-0 | |
66 | ``` | |
67 | | |
68 | ### Add data in a migration | |
69 | | |
70 | 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. | |
71 | | |
72 | The developer is responsible for ensuring that any added SQL statements are compatible with the database schema and rolling forward from the previous migrations. | |
73 | | |
74 | ### Migrations directory structure | |
75 | | |
76 | The `migrations` directory contains a folder for each migration that is created, looking like this for a project with two migrations: | |
77 | | |
78 | ```text | |
79 | ├── migrations | |
80 | │ ├── 20231205080937028 | |
81 | │ ├── 20231205081959122 | |
82 | │ └── migration_registry.txt | |
83 | ``` | |
84 | | |
85 | 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. | |
86 | | |
87 | For each migration, five files are created: | |
88 | | |
89 | - **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. | |
90 | - **definition.sql** - Contains SQL statements to create the complete database schema. This file is applied when initializing a new database. | |
91 | - **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. | |
92 | - **migration.json** - Contains the actions that are part of the migration. This file is parsed by the Serverpod CLI. | |
93 | - **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. | |
94 | | |
95 | ## Apply migrations | |
96 | | |
97 | 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. | |
98 | | |
99 | ```bash | |
100 | $ dart run bin/main.dart --apply-migrations | |
101 | ``` | |
102 | | |
103 | 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. | |
104 | | |
105 | ```bash | |
106 | $ dart run bin/main.dart --role maintenance --apply-migrations | |
107 | ``` | |
108 | | |
109 | This is useful if migrations are applied as part of an automated process. | |
110 | | |
111 | If migrations are applied at the same time as repair migration, the repair migration is applied first. | |
112 | | |
113 | ## Creating a repair migration | |
114 | | |
115 | 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. | |
116 | | |
117 | By default, the command connects to and pulls a live database schema from a running development server. | |
118 | | |
119 | To create a repair migration, navigate to your project's `server` package directory and run the `create-repair-migration` command. | |
120 | | |
121 | ```bash | |
122 | $ serverpod create-repair-migration | |
123 | ``` | |
124 | | |
125 | This creates a repair migration in the `repair-migration` directory targeting the project's latest migration. | |
126 | | |
127 | 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. | |
128 | | |
129 | :::warning | |
130 | 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. | |
131 | ::: | |
132 | | |
133 | 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. | |
134 | | |
135 | ### Migration database source | |
136 | | |
137 | 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. | |
138 | | |
139 | ```bash | |
140 | $ serverpod create-repair-migration --mode production | |
141 | ``` | |
142 | | |
143 | The command connects and pulls the live database schema from a running server. | |
144 | | |
145 | ### Targeting a specific migration | |
146 | | |
147 | Repair migrations can also target a specific migration version by specifying the migration name with the `--version` option. | |
148 | | |
149 | ```bash | |
150 | $ serverpod create-repair-migration --version 20230821135718-v1-0-0 | |
151 | ``` | |
152 | | |
153 | This makes it possible to revert your database schema back to any older migration version. | |
154 | | |
155 | ### Force create repair migration | |
156 | | |
157 | The repair migration command aborts and displays an error under two conditions: | |
158 | | |
159 | 1. When no changes are identified between the database schema in the latest migration and the schema required by the project. | |
160 | 2. When there is a risk of data loss. | |
161 | | |
162 | To override these safeguards and force the creation of a repair migration, use the `--force` flag. | |
163 | | |
164 | ```bash | |
165 | $ serverpod create-repair-migration --force | |
166 | ``` | |
167 | | |
168 | ### Tag repair migration | |
169 | | |
170 | Repair migrations can be tagged just like regular migrations. Tags are appended to the migration name and can be added with the `--tag` option. | |
171 | | |
172 | ```bash | |
173 | $ serverpod create-repair-migration --tag "reset-migrations" | |
174 | ``` | |
175 | | |
176 | This would create a repair migration named `<timestamp>-reset-migrations` in the `repair` directory: | |
177 | | |
178 | ```text | |
179 | ├── repair | |
180 | │ └── 20230821135718-v1-0-0.sql | |
181 | ``` | |
182 | | |
183 | ### Repair migrations directory structure | |
184 | | |
185 | 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. | |
186 | | |
187 | ```text | |
188 | ├── repair | |
189 | │ └── 20230821135718-v1-0-0.sql | |
190 | ``` | |
191 | | |
192 | ## Applying a repair migration | |
193 | | |
194 | 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. | |
195 | | |
196 | ```bash | |
197 | $ dart run bin/main.dart --apply-repair-migration | |
198 | ``` | |
199 | | |
200 | 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. | |
201 | | |
202 | ```bash | |
203 | $ dart run bin/main.dart --role maintenance --apply-repair-migration | |
204 | ``` | |
205 | | |
206 | If a repair migration is applied at the same time as migrations, the repair migration is applied first. | |
207 | | |
208 | ## Rolling back migrations | |
209 | | |
210 | 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. | |
211 | | |
212 | Note that data is not rolled back, only the database schema. | |
213 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/07-configuration.md: | |
-------------------------------------------------------------------------------- | |
1 | # Configurations | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Configuration options | |
6 | | |
7 | 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. | |
8 | | |
9 | ### Configuration options for the server | |
10 | | |
11 | These can be separately declared for each run mode in the corresponding yaml file (`development.yaml`,`staging.yaml`, `production.yaml` and `testing.yaml`) or as environment variables. | |
12 | | |
13 | | Environment variable | Config file | Default | Description | | |
14 | | ---------------------------------------- | ----------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- | | |
15 | | SERVERPOD_API_SERVER_PORT | apiServer.port | 8080 | The port number for the API server | | |
16 | | SERVERPOD_API_SERVER_PUBLIC_HOST | apiServer.publicHost | localhost | The public host address of the API server | | |
17 | | SERVERPOD_API_SERVER_PUBLIC_PORT | apiServer.publicPort | 8080 | The public port number for the API server | | |
18 | | SERVERPOD_API_SERVER_PUBLIC_SCHEME | apiServer.publicScheme | http | The public scheme (http/https) for the API server | | |
19 | | SERVERPOD_INSIGHTS_SERVER_PORT | insightsServer.port | - | The port number for the Insights server | | |
20 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_HOST | insightsServer.publicHost | - | The public host address of the Insights server | | |
21 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_PORT | insightsServer.publicPort | - | The public port number for the Insights server | | |
22 | | SERVERPOD_INSIGHTS_SERVER_PUBLIC_SCHEME | insightsServer.publicScheme | - | The public scheme (http/https) for the Insights server | | |
23 | | SERVERPOD_WEB_SERVER_PORT | webServer.port | - | The port number for the Web server | | |
24 | | SERVERPOD_WEB_SERVER_PUBLIC_HOST | webServer.publicHost | - | The public host address of the Web server | | |
25 | | SERVERPOD_WEB_SERVER_PUBLIC_PORT | webServer.publicPort | - | The public port number for the Web server | | |
26 | | SERVERPOD_WEB_SERVER_PUBLIC_SCHEME | webServer.publicScheme | - | The public scheme (http/https) for the Web server | | |
27 | | SERVERPOD_DATABASE_HOST | database.host | - | The host address of the database | | |
28 | | SERVERPOD_DATABASE_PORT | database.port | - | The port number for the database connection | | |
29 | | SERVERPOD_DATABASE_NAME | database.name | - | The name of the database | | |
30 | | SERVERPOD_DATABASE_USER | database.user | - | The user name for database authentication | | |
31 | | SERVERPOD_DATABASE_SEARCH_PATHS | database.searchPaths | - | The search paths used for all database connections | | |
32 | | SERVERPOD_DATABASE_REQUIRE_SSL | database.requireSsl | false | Indicates if SSL is required for the database | | |
33 | | SERVERPOD_DATABASE_IS_UNIX_SOCKET | database.isUnixSocket | false | Specifies if the database connection is a Unix socket | | |
34 | | SERVERPOD_REDIS_HOST | redis.host | - | The host address of the Redis server | | |
35 | | SERVERPOD_REDIS_PORT | redis.port | - | The port number for the Redis server | | |
36 | | SERVERPOD_REDIS_USER | redis.user | - | The user name for Redis authentication | | |
37 | | SERVERPOD_REDIS_ENABLED | redis.enabled | false | Indicates if Redis is enabled | | |
38 | | SERVERPOD_MAX_REQUEST_SIZE | maxRequestSize | 524288 | The maximum size of requests allowed in bytes | | |
39 | | 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`. | | |
40 | | 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`. | | |
41 | | |
42 | | Environment variable | Command line option | Default | Description | | |
43 | | --------------------- | ------------------- | ------- | ----------------------------------------- | | |
44 | | SERVERPOD_SERVER_ID | `--server-id` | default | Configures the id of the server instance. | | |
45 | | |
46 | ### Secrets | |
47 | | |
48 | Secrets are declared in the `passwords.yaml` file. The password 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. | |
49 | | |
50 | | Environment variable | Passwords file | Default | Description | | |
51 | | --------------------------- | -------------- | ------- | ----------------------------------------------------------------- | | |
52 | | SERVERPOD_DATABASE_PASSWORD | database | - | The password for the database | | |
53 | | SERVERPOD_SERVICE_SECRET | serviceSecret | - | The token used to connect with insights must be at least 20 chars | | |
54 | | SERVERPOD_REDIS_PASSWORD | redis | - | The password for the Redis server | | |
55 | | |
56 | #### Secrets for first party packages | |
57 | | |
58 | - [serverpod_cloud_storage_gcp](https://pub.dev/packages/serverpod_cloud_storage_gcp): Google Cloud Storage | |
59 | - [serverpod_cloud_storage_s3](https://pub.dev/packages/serverpod_cloud_storage_s3): Amazon S3 | |
60 | | |
61 | | Environment variable | Passwords file | Default | Description | | |
62 | | ---------------------------- | --------------- | ------- | ------------------------------------------------------------------------- | | |
63 | | SERVERPOD_HMAC_ACCESS_KEY_ID | HMACAccessKeyId | - | The access key ID for HMAC authentication for serverpod_cloud_storage_gcp | | |
64 | | SERVERPOD_HMAC_SECRET_KEY | HMACSecretKey | - | The secret key for HMAC authentication for serverpod_cloud_storage_gcp | | |
65 | | SERVERPOD_AWS_ACCESS_KEY_ID | AWSAccessKeyId | - | The access key ID for AWS authentication for serverpod_cloud_storage_s3 | | |
66 | | SERVERPOD_AWS_SECRET_KEY | AWSSecretKey | - | The secret key for AWS authentication for serverpod_cloud_storage_s3 | | |
67 | | |
68 | ### Config file example | |
69 | | |
70 | 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. | |
71 | | |
72 | ```yaml | |
73 | apiServer: | |
74 | port: 8080 | |
75 | publicHost: localhost | |
76 | publicPort: 8080 | |
77 | publicScheme: http | |
78 | | |
79 | insightsServer: | |
80 | port: 8081 | |
81 | publicHost: localhost | |
82 | publicPort: 8081 | |
83 | publicScheme: http | |
84 | | |
85 | webServer: | |
86 | port: 8082 | |
87 | publicHost: localhost | |
88 | publicPort: 8082 | |
89 | publicScheme: http | |
90 | | |
91 | database: | |
92 | host: localhost | |
93 | port: 8090 | |
94 | name: database_name | |
95 | user: postgres | |
96 | | |
97 | redis: | |
98 | enabled: false | |
99 | host: localhost | |
100 | port: 8091 | |
101 | | |
102 | maxRequestSize: 524288 | |
103 | | |
104 | sessionLogs: | |
105 | persistentEnabled: true | |
106 | consoleEnabled: true | |
107 | ``` | |
108 | | |
109 | ### Passwords file example | |
110 | | |
111 | 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. | |
112 | | |
113 | ```yaml | |
114 | shared: | |
115 | myCustomSharedSecret: 'secret_key' | |
116 | | |
117 | development: | |
118 | database: 'development_password' | |
119 | redis: 'development_password' | |
120 | serviceSecret: 'development_service_secret' | |
121 | | |
122 | production: | |
123 | database: 'production_password' | |
124 | redis: 'production_password' | |
125 | serviceSecret: 'production_service_secret' | |
126 | ``` | |
127 | | |
128 | ### Dart config object example | |
129 | | |
130 | 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. | |
131 | | |
132 | ```dart | |
133 | Serverpod( | |
134 | args, | |
135 | Protocol(), | |
136 | Endpoints(), | |
137 | config: ServerpodConfig( | |
138 | apiServer: ServerConfig( | |
139 | port: 8080, | |
140 | publicHost: 'localhost', | |
141 | publicPort: 8080, | |
142 | publicScheme: 'http', | |
143 | ), | |
144 | insightsServer: ServerConfig( | |
145 | port: 8081, | |
146 | publicHost: 'localhost', | |
147 | publicPort: 8081, | |
148 | publicScheme: 'http', | |
149 | ), | |
150 | webServer: ServerConfig( | |
151 | port: 8082, | |
152 | publicHost: 'localhost', | |
153 | publicPort: 8082, | |
154 | publicScheme: 'http', | |
155 | ), | |
156 | ), | |
157 | ); | |
158 | ``` | |
159 | | |
160 | ### Default | |
161 | | |
162 | If no yaml config files exist, no environment variables are configured and no dart config file is supplied this default configuration will be used. | |
163 | | |
164 | ```dart | |
165 | ServerpodConfig( | |
166 | apiServer: ServerConfig( | |
167 | port: 8080, | |
168 | publicHost: 'localhost', | |
169 | publicPort: 8080, | |
170 | publicScheme: 'http', | |
171 | ), | |
172 | ); | |
173 | ``` | |
174 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/08-caching.md: | |
-------------------------------------------------------------------------------- | |
1 | # Caching | |
2 | | |
3 | 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. | |
4 | | |
5 | Caches can be accessed through the `Session` object. This is an example of an endpoint method for requesting data about a user: | |
6 | | |
7 | ```dart | |
8 | Future<UserData> getUserData(Session session, int userId) async { | |
9 | // Define a unique key for the UserData object | |
10 | var cacheKey = 'UserData-$userId'; | |
11 | | |
12 | // Try to retrieve the object from the cache | |
13 | var userData = await session.caches.local.get<UserData>(cacheKey); | |
14 | | |
15 | // If the object wasn't found in the cache, load it from the database and | |
16 | // save it in the cache. Make it valid for 5 minutes. | |
17 | if (userData == null) { | |
18 | userData = UserData.db.findById(session, userId); | |
19 | await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5)); | |
20 | } | |
21 | | |
22 | // Return the user data to the client | |
23 | return userData; | |
24 | } | |
25 | ``` | |
26 | | |
27 | 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. | |
28 | | |
29 | 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. | |
30 | | |
31 | ### Cache miss handler | |
32 | | |
33 | 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. | |
34 | | |
35 | The above example rewritten using the `CacheMissHandler`: | |
36 | | |
37 | ```dart | |
38 | Future<UserData> getUserData(Session session, int userId) async { | |
39 | // Define a unique key for the UserData object | |
40 | var cacheKey = 'UserData-$userId'; | |
41 | | |
42 | // Try to retrieve the object from the cache | |
43 | var userData = await session.caches.local.get( | |
44 | cacheKey, | |
45 | // If the object wasn't found in the cache, load it from the database and | |
46 | // save it in the cache. Make it valid for 5 minutes. | |
47 | CacheMissHandler( | |
48 | () async => UserData.db.findById(session, userId), | |
49 | lifetime: Duration(minutes: 5), | |
50 | ), | |
51 | ); | |
52 | | |
53 | // Return the user data to the client | |
54 | return userData; | |
55 | } | |
56 | ``` | |
57 | | |
58 | If the `CacheMissHandler` returns `null`, no object will be stored in the cache. | |
59 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/09-logging.md: | |
-------------------------------------------------------------------------------- | |
1 | # Logging | |
2 | | |
3 | 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. | |
4 | | |
5 | ```dart | |
6 | session.log('This is working well'); | |
7 | ``` | |
8 | | |
9 | You can also pass exceptions and stack traces to the `log` method or set the logging level. | |
10 | | |
11 | ```dart | |
12 | session.log( | |
13 | 'Oops, something went wrong', | |
14 | level: LogLevel.warning, | |
15 | exception: e, | |
16 | stackTrace: stackTrace, | |
17 | ); | |
18 | ``` | |
19 | | |
20 | 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. | |
21 | | |
22 | ### Controlling Session Logs with Environment Variables or Configuration Files | |
23 | | |
24 | 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. | |
25 | | |
26 | #### Environment Variables | |
27 | | |
28 | - `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED`: Controls whether session logs are written to the database. | |
29 | - `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED`: Controls whether session logs are output to the console. | |
30 | | |
31 | #### Configuration File Example | |
32 | | |
33 | You can also configure logging behavior directly in the configuration file: | |
34 | | |
35 | ```yaml | |
36 | sessionLogs: | |
37 | persistentEnabled: true # Logs are stored in the database | |
38 | consoleEnabled: true # Logs are output to the console | |
39 | ``` | |
40 | | |
41 | ### Default Behavior for Session Logs | |
42 | | |
43 | By default, session logging behavior depends on whether the project has database support: | |
44 | | |
45 | - **When a database is present** | |
46 | | |
47 | - `persistentEnabled` is set to `true`, meaning logs are stored in the database. | |
48 | - `consoleEnabled` is set to `false` by default, meaning logs are not printed to the console unless explicitly enabled. | |
49 | | |
50 | - **When no database is present** | |
51 | | |
52 | - `persistentEnabled` is set to `false` since persistent logging requires a database. | |
53 | - `consoleEnabled` is set to `true`, meaning logs are printed to the console by default. | |
54 | | |
55 | ### Important: Persistent Logging Requires a Database | |
56 | | |
57 | 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. | |
58 | | |
59 | :::info | |
60 | | |
61 | You can use the companion app **[Serverpod Insights](../tools/insights)** to read, search, and configure the logs. | |
62 | | |
63 | ::: | |
64 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/10-modules.md: | |
-------------------------------------------------------------------------------- | |
1 | # Modules | |
2 | | |
3 | 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. | |
4 | | |
5 | Examples of modules are the `serverpod_auth` module and the `serverpod_chat` module, which both are maintained by the Serverpod team. | |
6 | | |
7 | ## Adding a module to your project | |
8 | | |
9 | ### Server setup | |
10 | | |
11 | To add a module to your project, you must include the server and client/Flutter packages in your project's `pubspec.yaml` files. | |
12 | | |
13 | For example, to add the `serverpod_auth` module to your project, you need to add `serverpod_auth_server` to your server's `pubspec.yaml`: | |
14 | | |
15 | ```yaml | |
16 | dependencies: | |
17 | serverpod_auth_server: ^1.x.x | |
18 | ``` | |
19 | | |
20 | :::info | |
21 | | |
22 | 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. | |
23 | | |
24 | ::: | |
25 | | |
26 | 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. | |
27 | | |
28 | ```yaml | |
29 | modules: | |
30 | serverpod_auth: | |
31 | nickname: auth | |
32 | ``` | |
33 | | |
34 | 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. | |
35 | | |
36 | ```bash | |
37 | $ dart pub get | |
38 | $ serverpod generate | |
39 | ``` | |
40 | | |
41 | 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. | |
42 | | |
43 | ```bash | |
44 | $ serverpod create-migration | |
45 | $ dart bin/main.dart --apply-migrations | |
46 | ``` | |
47 | | |
48 | ### Client setup | |
49 | | |
50 | In your client's `pubspec.yaml`, you will need to add the generated client code from the module. | |
51 | | |
52 | ```yaml | |
53 | dependencies: | |
54 | serverpod_auth_client: ^1.x.x | |
55 | ``` | |
56 | | |
57 | ### Flutter app setup | |
58 | | |
59 | In your Flutter app, add the corresponding dart or Flutter package(s) to your `pubspec.yaml`. | |
60 | | |
61 | ```yaml | |
62 | dependencies: | |
63 | serverpod_auth_shared_flutter: ^1.x.x | |
64 | serverpod_auth_google_flutter: ^1.x.x | |
65 | serverpod_auth_apple_flutter: ^1.x.x | |
66 | ``` | |
67 | | |
68 | ## Referencing a module | |
69 | | |
70 | 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. | |
71 | | |
72 | ```yaml | |
73 | class: MyClass | |
74 | fields: | |
75 | userInfo: module:auth:UserInfo | |
76 | ``` | |
77 | | |
78 | ## Creating custom modules | |
79 | | |
80 | 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. | |
81 | | |
82 | ```bash | |
83 | $ serverpod create --template module my_module | |
84 | ``` | |
85 | | |
86 | 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. | |
87 | | |
88 | ```bash | |
89 | $ flutter create --template package my_module_flutter | |
90 | ``` | |
91 | | |
92 | In your Flutter package, you most likely want to import the client libraries created by `serverpod create`. | |
93 | | |
94 | :::info | |
95 | | |
96 | 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_`. | |
97 | | |
98 | ::: | |
99 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/01-setup.md: | |
-------------------------------------------------------------------------------- | |
1 | # Setup | |
2 | | |
3 | 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. | |
4 | | |
5 | Future versions of the authentication module will include more options. If you write another authentication module, please consider [contributing](/contribute) your code. | |
6 | | |
7 |  | |
8 | | |
9 | ## Installing the auth module | |
10 | | |
11 | 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. | |
12 | | |
13 | ## Server setup | |
14 | | |
15 | Add the module as a dependency to the server project's `pubspec.yaml`. | |
16 | | |
17 | ```sh | |
18 | $ dart pub add serverpod_auth_server | |
19 | ``` | |
20 | | |
21 | Add the authentication handler to the Serverpod instance. | |
22 | | |
23 | ```dart | |
24 | import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; | |
25 | | |
26 | void run(List<String> args) async { | |
27 | var pod = Serverpod( | |
28 | args, | |
29 | Protocol(), | |
30 | Endpoints(), | |
31 | authenticationHandler: auth.authenticationHandler, // Add this line | |
32 | ); | |
33 | | |
34 | ... | |
35 | } | |
36 | ``` | |
37 | 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. | |
38 | | |
39 | ```yaml | |
40 | modules: | |
41 | serverpod_auth: | |
42 | nickname: auth | |
43 | ``` | |
44 | | |
45 | 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. | |
46 | | |
47 | ```bash | |
48 | $ serverpod generate | |
49 | ``` | |
50 | | |
51 | ### Initialize the auth database | |
52 | | |
53 | 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. | |
54 | | |
55 | ```bash | |
56 | $ serverpod create-migration | |
57 | ``` | |
58 | | |
59 | Start your database container from the server project. | |
60 | | |
61 | ```bash | |
62 | $ docker-compose up --build --detach | |
63 | ``` | |
64 | | |
65 | Then apply the migration by starting the server with the `apply-migrations` flag. | |
66 | | |
67 | ```bash | |
68 | $ dart run bin/main.dart --role maintenance --apply-migrations | |
69 | ``` | |
70 | | |
71 | The full migration instructions can be found in the [migration guide](../database/migrations). | |
72 | | |
73 | ### Configure Authentication | |
74 | 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: | |
75 | | |
76 | | |
77 | ```dart | |
78 | import 'package:serverpod_auth_server/module.dart' as auth; | |
79 | | |
80 | void run(List<String> args) async { | |
81 | | |
82 | auth.AuthConfig.set(auth.AuthConfig( | |
83 | minPasswordLength: 12, | |
84 | )); | |
85 | | |
86 | // Start the Serverpod server. | |
87 | await pod.start(); | |
88 | } | |
89 | | |
90 | ``` | |
91 | | |
92 | | **Property** | **Description** | **Default** | | |
93 | |:-------------|:----------------|:-----------:| | |
94 | | **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 | | |
95 | | **emailSignInFailureResetTime** | The reset period for email sign in attempts. Defaults to 5 minutes. | 5min | | |
96 | | **enableUserImages** | True if user images are enabled. | true | | |
97 | | **extraSaltyHash** | True if the server should use the accounts email address as part of the salt when storing password hashes (strongly recommended). | true | | |
98 | | **firebaseServiceAccountKeyJson** | Firebase service account key JSON file. Generate and download from the Firebase console. | - | | |
99 | | **importUserImagesFromGoogleSignIn** | True if user images should be imported when signing in with Google. | true | | |
100 | | **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` | | |
101 | | **maxAllowedEmailSignInAttempts** | Max allowed failed email sign in attempts within the reset period. | 5 | | |
102 | | **maxPasswordLength** | The maximum length of passwords when signing up with email. | 128 | | |
103 | | **minPasswordLength** | The minimum length of passwords when signing up with email. | 8 | | |
104 | | **onUserCreated** | Called after a user has been created. Listen to this callback if you need to do additional setup. | - | | |
105 | | **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. | - | | |
106 | | **onUserWillBeCreated** | Called when a user is about to be created, gives a chance to abort the creation by returning false. | - | | |
107 | | **passwordResetExpirationTime** | The time for password resets to be valid. | 24h | | |
108 | | **sendPasswordResetEmail** | Called when a user should be sent a reset code by email. | - | | |
109 | | **sendValidationEmail** | Called when a user should be sent a validation code on account setup. | - | | |
110 | | **userCanEditFullName** | True if users can edit their full name. | false | | |
111 | | **userCanEditUserImage** | True if users can update their profile images. | true | | |
112 | | **userCanEditUserName** | True if users can edit their user names. | true | | |
113 | | **userCanSeeFullName** | True if users can view their full name. | true | | |
114 | | **userCanSeeUserName** | True if users can view their user name. | true | | |
115 | | **userImageFormat** | The format used to store user images. | jpg | | |
116 | | **userImageGenerator** | Generator used to produce default user images. | - | | |
117 | | **userImageQuality** | The quality setting for images if JPG format is used. | 70 | | |
118 | | **userImageSize** | The size of user images. | 256 | | |
119 | | **userInfoCacheLifetime** | The duration which user infos are cached locally in the server. | 1min | | |
120 | | **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 | | |
121 | | |
122 | ## Client setup | |
123 | | |
124 | Add the auth client in your client project's `pubspec.yaml`. | |
125 | | |
126 | ```yaml | |
127 | dependencies: | |
128 | ... | |
129 | serverpod_auth_client: ^1.x.x | |
130 | ``` | |
131 | | |
132 | ## App setup | |
133 | | |
134 | First, add dependencies to your app's `pubspec.yaml` file for the methods of signing in that you want to support. | |
135 | | |
136 | ```yaml | |
137 | dependencies: | |
138 | flutter: | |
139 | sdk: flutter | |
140 | serverpod_flutter: ^1.x.x | |
141 | auth_example_client: | |
142 | path: ../auth_example_client | |
143 | | |
144 | serverpod_auth_shared_flutter: ^1.x.x | |
145 | ``` | |
146 | | |
147 | 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. | |
148 | | |
149 | ```dart | |
150 | late SessionManager sessionManager; | |
151 | late Client client; | |
152 | | |
153 | void main() async { | |
154 | // Need to call this as we are using Flutter bindings before runApp is called. | |
155 | WidgetsFlutterBinding.ensureInitialized(); | |
156 | | |
157 | // The android emulator does not have access to the localhost of the machine. | |
158 | // const ipAddress = '10.0.2.2'; // Android emulator ip for the host | |
159 | | |
160 | // On a real device replace the ipAddress with the IP address of your computer. | |
161 | const ipAddress = 'localhost'; | |
162 | | |
163 | // Sets up a singleton client object that can be used to talk to the server from | |
164 | // anywhere in our app. The client is generated from your server code. | |
165 | // The client is set up to connect to a Serverpod running on a local server on | |
166 | // the default port. You will need to modify this to connect to staging or | |
167 | // production servers. | |
168 | client = Client( | |
169 | 'http://$ipAddress:8080/', | |
170 | authenticationKeyManager: FlutterAuthenticationKeyManager(), | |
171 | )..connectivityMonitor = FlutterConnectivityMonitor(); | |
172 | | |
173 | // The session manager keeps track of the signed-in state of the user. You | |
174 | // can query it to see if the user is currently signed in and get information | |
175 | // about the user. | |
176 | sessionManager = SessionManager( | |
177 | caller: client.modules.auth, | |
178 | ); | |
179 | await sessionManager.initialize(); | |
180 | | |
181 | runApp(MyApp()); | |
182 | } | |
183 | ``` | |
184 | | |
185 | The `SessionManager` has useful methods for viewing and monitoring the user's current state. | |
186 | | |
187 | #### Check authentication state | |
188 | To check if the user is signed in: | |
189 | | |
190 | ```dart | |
191 | sessionManager.isSignedIn; | |
192 | ``` | |
193 | Returns `true` if the user is signed in, or `false` otherwise. | |
194 | | |
195 | #### Access current user | |
196 | To retrieve information about the current user: | |
197 | | |
198 | ```dart | |
199 | sessionManager.signedInUser; | |
200 | ``` | |
201 | Returns a `UserInfo` object if the user is currently signed in, or `null` if the user is not. | |
202 | | |
203 | #### Register authentication | |
204 | To register a signed in user in the session manager: | |
205 | | |
206 | ```dart | |
207 | await sessionManager.registerSignedInUser( | |
208 | userInfo, | |
209 | keyId, | |
210 | authKey, | |
211 | ); | |
212 | ``` | |
213 | 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. | |
214 | | |
215 | #### Monitor authentication changes | |
216 | To add a listener that tracks changes in the user's authentication state, useful for updating the UI: | |
217 | | |
218 | ```dart | |
219 | @override | |
220 | void initState() { | |
221 | super.initState(); | |
222 | | |
223 | // Rebuild the page if authentication state changes. | |
224 | sessionManager.addListener(() { | |
225 | setState(() {}); | |
226 | }); | |
227 | } | |
228 | ``` | |
229 | The listener is triggered whenever the user's sign-in state changes. | |
230 | | |
231 | #### Sign out current device | |
232 | To sign the user out on from the current device: | |
233 | | |
234 | ```dart | |
235 | await sessionManager.signOutDevice(); | |
236 | ``` | |
237 | Returns `true` if the sign-out is successful, or `false` if it fails. | |
238 | | |
239 | #### Sign out all devices | |
240 | To sign the user out across all devices: | |
241 | | |
242 | ```dart | |
243 | await sessionManager.signOutAllDevices(); | |
244 | ``` | |
245 | Returns `true` if the user is successfully signed out from all devices, or `false` if it fails. | |
246 | | |
247 | | |
248 | :::info | |
249 | | |
250 | 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. | |
251 | | |
252 | ```dart | |
253 | await sessionManager.signOut(); // Deprecated | |
254 | ``` | |
255 | ::: | |
256 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/02-basics.md: | |
-------------------------------------------------------------------------------- | |
1 | # The basics | |
2 | | |
3 | 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. | |
4 | | |
5 | 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` asynchronous getter of the `Session` object. | |
6 | | |
7 | ```dart | |
8 | Future<void> myMethod(Session session) async { | |
9 | final authenticationInfo = await session.authenticated; | |
10 | final userId = authenticationInfo?.userId; | |
11 | ... | |
12 | } | |
13 | ``` | |
14 | | |
15 | You can also use the Session object to check if a user is authenticated: | |
16 | | |
17 | ```dart | |
18 | Future<void> myMethod(Session session) async { | |
19 | var isSignedIn = await session.isUserSignedIn; | |
20 | ... | |
21 | } | |
22 | ``` | |
23 | | |
24 | ## Requiring authentication on endpoints | |
25 | | |
26 | 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. | |
27 | | |
28 | ```dart | |
29 | class MyEndpoint extends Endpoint { | |
30 | @override | |
31 | bool get requireLogin => true; | |
32 | | |
33 | Future<void> myMethod(Session session) async { | |
34 | ... | |
35 | } | |
36 | ... | |
37 | } | |
38 | ``` | |
39 | | |
40 | ## Authorization on endpoints | |
41 | | |
42 | 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. | |
43 | | |
44 | ```dart | |
45 | class MyEndpoint extends Endpoint { | |
46 | @override | |
47 | bool get requireLogin => true; | |
48 | | |
49 | @override | |
50 | Set<Scope> get requiredScopes => {Scope.admin}; | |
51 | | |
52 | Future<void> myMethod(Session session) async { | |
53 | ... | |
54 | } | |
55 | ... | |
56 | } | |
57 | ``` | |
58 | | |
59 | ### Managing scopes | |
60 | | |
61 | 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. | |
62 | | |
63 | ```dart | |
64 | await Users.updateUserScopes(session, userId, {Scope.admin}); | |
65 | ``` | |
66 | | |
67 | ### Custom scopes | |
68 | | |
69 | You may need more granular access control for specific endpoints. To create custom scopes, extend the Scope class, as shown below: | |
70 | | |
71 | ```dart | |
72 | class CustomScope extends Scope { | |
73 | const CustomScope(String name) : super(name); | |
74 | | |
75 | static const userRead = CustomScope('userRead'); | |
76 | static const userWrite = CustomScope('userWrite'); | |
77 | } | |
78 | ``` | |
79 | | |
80 | Then use the custom scopes like this: | |
81 | | |
82 | ```dart | |
83 | class MyEndpoint extends Endpoint { | |
84 | @override | |
85 | bool get requireLogin => true; | |
86 | | |
87 | @override | |
88 | Set<Scope> get requiredScopes => {CustomScope.userRead, CustomScope.userWrite}; | |
89 | | |
90 | Future<void> myMethod(Session session) async { | |
91 | ... | |
92 | } | |
93 | ... | |
94 | } | |
95 | ``` | |
96 | | |
97 | :::caution | |
98 | 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. | |
99 | ::: | |
100 | | |
101 | ## User authentication | |
102 | | |
103 | The `StatusEndpoint` class includes methods for handling user sign-outs, whether from a single device or all devices. | |
104 | | |
105 | :::info | |
106 | | |
107 | In addition to the `StatusEndpoint` methods, Serverpod provides more comprehensive tools for managing user authentication and sign-out processes across multiple devices. | |
108 | | |
109 | For more detailed information on managing and revoking authentication keys, please refer to the [Revoking authentication keys](providers/custom-providers#revoking-authentication-keys) section. | |
110 | | |
111 | ::: | |
112 | | |
113 | #### Sign out device | |
114 | | |
115 | To sign out a single device: | |
116 | | |
117 | ```dart | |
118 | await client.modules.auth.status.signOutDevice(); | |
119 | ``` | |
120 | | |
121 | This status endpoint method obtains the authentication key from session's authentication information, then revokes that key. | |
122 | | |
123 | #### Sign out all devices | |
124 | | |
125 | To sign the user out across all devices: | |
126 | | |
127 | ```dart | |
128 | await client.modules.auth.status.signOutAllDevices(); | |
129 | ``` | |
130 | | |
131 | This status endpoint retrieves the user ID from session's authentication information, then revokes all authentication keys related to that user. | |
132 | | |
133 | :::info | |
134 | <span id="deprecated-signout-endpoint"></span> | |
135 | The `signOut` status endpoint is deprecated. Use `signOutDevice` or `signOutAllDevices` instead. | |
136 | | |
137 | ```dart | |
138 | await client.modules.auth.status.signOut(); // Deprecated | |
139 | ``` | |
140 | | |
141 | 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. | |
142 | ::: | |
143 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/03-working-with-users.md: | |
-------------------------------------------------------------------------------- | |
1 | # Working with users | |
2 | | |
3 | 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. | |
4 | | |
5 | ```dart | |
6 | var userId = (await session.authenticated)?.userId; | |
7 | ``` | |
8 | | |
9 | 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. | |
10 | | |
11 | ```dart | |
12 | var userInfo = await Users.findUserByUserId(session, userId!); | |
13 | ``` | |
14 | | |
15 | The `UserInfo` is automatically populated when the user signs in. Different data may be available depending on which method was used for authentication. | |
16 | | |
17 | :::tip | |
18 | | |
19 | 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/serverpod_auth_server/Users-class.html). | |
20 | | |
21 | ::: | |
22 | | |
23 | ## Displaying or editing user images | |
24 | | |
25 | The module has built-in methods for handling a user's basic settings, including uploading new profile pictures. | |
26 | | |
27 |  | |
28 | | |
29 | To display a user's profile picture, use the `CircularUserImage` widget and pass a `UserInfo` retrieved from the `SessionManager`. | |
30 | | |
31 | 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. | |
32 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/01-email.md: | |
-------------------------------------------------------------------------------- | |
1 | # Email | |
2 | | |
3 | 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. | |
4 | | |
5 | 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). | |
6 | | |
7 | :::caution | |
8 | You need to install the auth module before you continue, see [Setup](../setup). | |
9 | ::: | |
10 | | |
11 | ## Server-side configuration | |
12 | | |
13 | In your main `server.dart` file, import the `serverpod_auth_server` module, and set up the authentication configuration: | |
14 | | |
15 | ```dart | |
16 | import 'package:serverpod_auth_server/module.dart' as auth; | |
17 | | |
18 | auth.AuthConfig.set(auth.AuthConfig( | |
19 | sendValidationEmail: (session, email, validationCode) async { | |
20 | // Send the validation email to the user. | |
21 | // Return `true` if the email was successfully sent, otherwise `false`. | |
22 | return true; | |
23 | }, | |
24 | sendPasswordResetEmail: (session, userInfo, validationCode) async { | |
25 | // Send the password reset email to the user. | |
26 | // Return `true` if the email was successfully sent, otherwise `false`. | |
27 | return true; | |
28 | }, | |
29 | )); | |
30 | | |
31 | // Start the Serverpod server. | |
32 | await pod.start(); | |
33 | ``` | |
34 | | |
35 | :::info | |
36 | | |
37 | 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). | |
38 | | |
39 | ::: | |
40 | | |
41 | ## Client-side configuration | |
42 | | |
43 | Add the dependencies to your `pubspec.yaml` in your **client** project. | |
44 | | |
45 | ```yaml | |
46 | dependencies: | |
47 | ... | |
48 | serverpod_auth_client: ^1.x.x | |
49 | ``` | |
50 | | |
51 | Add the dependencies to your `pubspec.yaml` in your **Flutter** project. | |
52 | | |
53 | ```yaml | |
54 | dependencies: | |
55 | ... | |
56 | serverpod_auth_email_flutter: ^1.x.x | |
57 | serverpod_auth_shared_flutter: ^1.x.x | |
58 | ``` | |
59 | | |
60 | ### Prebuilt sign in button | |
61 | | |
62 | 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. | |
63 | | |
64 | ```dart | |
65 | SignInWithEmailButton( | |
66 | caller: client.modules.auth, | |
67 | onSignedIn: () { | |
68 | // Optional callback when user successfully signs in | |
69 | }, | |
70 | ), | |
71 | ``` | |
72 | | |
73 |  | |
74 | | |
75 | ### Modal example | |
76 | | |
77 | The triggered modal will look like this: | |
78 | | |
79 |  | |
80 | | |
81 | ## Custom UI with EmailAuthController | |
82 | | |
83 | 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. | |
84 | | |
85 | ```dart | |
86 | import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart'; | |
87 | | |
88 | final authController = EmailAuthController(client.modules.auth); | |
89 | ``` | |
90 | | |
91 | 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. | |
92 | | |
93 | ```dart | |
94 | await authController.createAccountRequest(userName, email, password); | |
95 | ``` | |
96 | | |
97 | 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. | |
98 | | |
99 | ```dart | |
100 | await authController.validateAccount(email, verificationCode); | |
101 | ``` | |
102 | | |
103 | To let users log in if they already have an account you can use the `signIn` method. | |
104 | | |
105 | ```dart | |
106 | await authController.signIn(email, password); | |
107 | ``` | |
108 | | |
109 | 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. | |
110 | | |
111 | ```dart | |
112 | await authController.initiatePasswordReset(email); | |
113 | ``` | |
114 | | |
115 | Let the user type in the verification code along with the new password and send it to the backend with the `resetPassword` method. | |
116 | | |
117 | ```dart | |
118 | await authController.resetPassword(email, verificationCode, password); | |
119 | ``` | |
120 | | |
121 | 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. | |
122 | | |
123 | ## Password storage security | |
124 | | |
125 | Serverpod provides some additional configurable options to provide extra layers of security for stored password hashes. | |
126 | | |
127 | :::info | |
128 | 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. | |
129 | ::: | |
130 | | |
131 | ### Peppering | |
132 | | |
133 | 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. | |
134 | | |
135 | The [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes. | |
136 | | |
137 | To configure a pepper, set the `emailPasswordPepper` property in the `config/passwords.yaml` file. | |
138 | | |
139 | ```yaml | |
140 | development: | |
141 | emailPasswordPepper: 'your-pepper' | |
142 | ``` | |
143 | | |
144 | It is essential to keep the pepper secret and never expose it to the client. | |
145 | | |
146 | :::warning | |
147 | | |
148 | If the pepper is changed, all passwords in the database will need to be re-hashed with the new pepper. | |
149 | | |
150 | ::: | |
151 | | |
152 | ### Secure random | |
153 | | |
154 | 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. | |
155 | | |
156 | 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. | |
157 | | |
158 | ```dart | |
159 | auth.AuthConfig.set(auth.AuthConfig( | |
160 | allowUnsecureRandom: false, | |
161 | )); | |
162 | ``` | |
163 | | |
164 | ## Custom password hash generator | |
165 | | |
166 | 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`. | |
167 | | |
168 | ```dart | |
169 | AuthConfig( | |
170 | passwordHashValidator: ( | |
171 | password, | |
172 | email, | |
173 | hash, { | |
174 | onError, | |
175 | onValidationFailure, | |
176 | }, | |
177 | ) { | |
178 | // Custom hash validator. | |
179 | }, | |
180 | passwordHashGenerator: (password) { | |
181 | // Custom hash generator. | |
182 | }, | |
183 | ) | |
184 | | |
185 | ``` | |
186 | | |
187 | It could be useful if you already have stored passwords that should be preserved or migrated. | |
188 | | |
189 | :::warning | |
190 | | |
191 | Using a custom hash generator will permanently disrupt compatibility with the default hash generator. | |
192 | | |
193 | ::: | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/02-google.md: | |
-------------------------------------------------------------------------------- | |
1 | # Google | |
2 | | |
3 | 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. | |
4 | | |
5 | 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). | |
6 | | |
7 | :::note | |
8 | Right now, we have official support for iOS, Android, and Web for Google Sign In. | |
9 | ::: | |
10 | | |
11 | :::caution | |
12 | You need to install the auth module before you continue, see [Setup](../setup). | |
13 | ::: | |
14 | | |
15 | ## Create your credentials | |
16 | | |
17 | 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/). | |
18 | | |
19 | ### Enable Peoples API | |
20 | | |
21 | To be allowed to access user data and use the authentication method in Serverpod we have to enable the Peoples API in our project. | |
22 | | |
23 | [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_. | |
24 | | |
25 | ### Setup OAuth consent screen | |
26 | | |
27 | 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_. | |
28 | | |
29 | 1. Fill in all the required information, for production use you need a domain that adds under `Authorized` domains. | |
30 | | |
31 | 2. Add the scopes `.../auth/userinfo.email` and `.../auth/userinfo.profile`. | |
32 | | |
33 | 3. Add your email to the test users so that you can test your integration in development mode. | |
34 | | |
35 |  | |
36 | | |
37 | ## Server-side configuration | |
38 | | |
39 | 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. | |
40 | | |
41 | 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. | |
42 | | |
43 | :::warning | |
44 | | |
45 | The `google_client_secret.json` contains a private key and should not be version controlled. | |
46 | | |
47 | ::: | |
48 | | |
49 |  | |
50 | | |
51 | ## Client-side configuration | |
52 | | |
53 | 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. | |
54 | | |
55 | :::info | |
56 | 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. | |
57 | ::: | |
58 | | |
59 | ### iOS | |
60 | | |
61 | 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**_. | |
62 | | |
63 | 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. | |
64 | | |
65 | Open the `GoogleService-Info.plist` in your editor and add the SERVER_CLIENT_ID if it does not exist: | |
66 | | |
67 | ```xml | |
68 | <dict> | |
69 | ... | |
70 | <key>SERVER_CLIENT_ID</key> | |
71 | <string>your_server_client_id</string> | |
72 | </dict> | |
73 | ``` | |
74 | | |
75 | Replace `your_server_client_id` with the client id from the JSON file you put inside the config folder in the server. | |
76 | | |
77 | #### Add the URL scheme | |
78 | | |
79 | 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. | |
80 | | |
81 | Open the `info.plist` file in your editor and add the following to register the URL Scheme. | |
82 | | |
83 | ```xml | |
84 | <dict> | |
85 | ... | |
86 | <key>CFBundleURLTypes</key> | |
87 | <array> | |
88 | <dict> | |
89 | <key>CFBundleTypeRole</key> | |
90 | <string>Editor</string> | |
91 | <key>CFBundleURLSchemes</key> | |
92 | <array> | |
93 | <string>your_reversed_client_id</string> | |
94 | </array> | |
95 | </dict> | |
96 | </array> | |
97 | </dict> | |
98 | ``` | |
99 | | |
100 | Replace `your_reversed_client_id` with your reversed client ID. | |
101 | | |
102 | :::info | |
103 | | |
104 | 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/)). | |
105 | | |
106 | ::: | |
107 | | |
108 | ### Android | |
109 | | |
110 | 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**_. | |
111 | | |
112 | 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. | |
113 | | |
114 | Put the file inside the `android/app/` directory and rename it to `google-services.json`. | |
115 | | |
116 | :::info | |
117 | 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)). | |
118 | | |
119 | ```bash | |
120 | $ keytool -list -v -keystore /path/to/keystore | |
121 | ``` | |
122 | | |
123 | ::: | |
124 | | |
125 | ### Web | |
126 | | |
127 | There is no need to create any client credentials for the web we will simply pass the `serverClientId` to the sign-in button. | |
128 | However, we have to modify the server credentials inside the google cloud console. | |
129 | | |
130 | 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. | |
131 | | |
132 | :::info | |
133 | | |
134 | Force flutter to run on a specific port by running. | |
135 | | |
136 | ```bash | |
137 | $ flutter run -d chrome --web-port=49660 | |
138 | ``` | |
139 | | |
140 | ::: | |
141 | | |
142 | 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. | |
143 | | |
144 | For example, using the path `/googlesignin`. | |
145 | | |
146 | For development inside `Authorized redirect URIs` add `http://localhost:8082/googlesignin`, in production use `https://example.com/googlesignin`. | |
147 | | |
148 |  | |
149 | | |
150 | #### Serve the redirect page | |
151 | | |
152 | Register the Google Sign In route inside `server.dart`. | |
153 | | |
154 | ```dart | |
155 | import 'package:serverpod_auth_server/module.dart' as auth | |
156 | | |
157 | | |
158 | void run(List<String> args) async { | |
159 | ... | |
160 | pod.webServer.addRoute(auth.RouteGoogleSignIn(), '/googlesignin'); | |
161 | ... | |
162 | } | |
163 | ``` | |
164 | | |
165 | This page is needed for the web app to receive the authentication code given by Google. | |
166 | | |
167 | ### Flutter implementation | |
168 | | |
169 |  | |
170 | | |
171 | Add the `SignInWithGoogleButton` to your widget. | |
172 | | |
173 | ```dart | |
174 | import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; | |
175 | | |
176 | | |
177 | SignInWithGoogleButton( | |
178 | caller: client.modules.auth, | |
179 | serverClientId: _googleServerClientId, // needs to be supplied for the web integration | |
180 | redirectUri: Uri.parse('http://localhost:8082/googlesignin'), | |
181 | ) | |
182 | ``` | |
183 | | |
184 | As an alternative to adding the JSON files in your client projects, you can supply the client and server ID on iOS and Android. | |
185 | | |
186 | ```dart | |
187 | import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart'; | |
188 | | |
189 | | |
190 | SignInWithGoogleButton( | |
191 | caller: client.modules.auth, | |
192 | clientId: _googleClientId, // Client ID of the client (null on web) | |
193 | serverClientId: _googleServerClientId, // Client ID from the server (required on web) | |
194 | redirectUri: Uri.parse('http://localhost:8082/googlesignin'), | |
195 | ) | |
196 | ``` | |
197 | | |
198 | ## Calling Google APIs | |
199 | | |
200 | 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: | |
201 | | |
202 | - Add the required scopes to the OAuth consent screen. | |
203 | - Request access to the scopes when signing in. Do this by setting the `additionalScopes` parameter of the `signInWithGoogle` method or the `SignInWithGoogleButton` widget. | |
204 | | |
205 | A full list of available scopes can be found [here](https://developers.google.com/identity/protocols/oauth2/scopes). | |
206 | | |
207 | :::info | |
208 | | |
209 | Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. | |
210 | | |
211 | ::: | |
212 | | |
213 | 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. | |
214 | | |
215 | For instance, to access the Youtube APIs, add the scope to your `SignInWithGoogleButton` in your app: | |
216 | | |
217 | ```dart | |
218 | SignInWithGoogleButton( | |
219 | ... | |
220 | additionalScopes: const ['https://www.googleapis.com/auth/youtube'], | |
221 | ) | |
222 | ``` | |
223 | | |
224 | 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. | |
225 | | |
226 | ```dart | |
227 | import 'package:serverpod_auth_server/module.dart'; | |
228 | import 'package:googleapis/youtube/v3.dart'; | |
229 | | |
230 | | |
231 | final googleClient = await GoogleAuth.authClientForUser(session, userId); | |
232 | | |
233 | if (googleClient != null) { | |
234 | var youTubeApi = YouTubeApi(googleClient); | |
235 | | |
236 | var favorites = await youTubeApi.playlistItems.list( | |
237 | ['snippet'], | |
238 | playlistId: 'LL', // Liked List | |
239 | ); | |
240 | | |
241 | } else { | |
242 | // The user hasn't signed in with Google. | |
243 | } | |
244 | ``` | |
245 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/03-apple.md: | |
-------------------------------------------------------------------------------- | |
1 | # Apple | |
2 | | |
3 | 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. | |
4 | | |
5 | 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). | |
6 | | |
7 | :::note | |
8 | Right now, we have official support for iOS and MacOS for Sign in with Apple. | |
9 | ::: | |
10 | | |
11 | :::caution | |
12 | You need to install the auth module before you continue, see [Setup](../setup). | |
13 | ::: | |
14 | | |
15 | ## Server-side configuration | |
16 | | |
17 | No extra steps outside installing the auth module are required. | |
18 | | |
19 | ## Client-side configuration | |
20 | | |
21 | Add the dependency to your `pubspec.yaml` in your flutter project. | |
22 | | |
23 | ```yaml | |
24 | dependencies: | |
25 | ... | |
26 | serverpod_auth_apple_flutter: ^1.x.x | |
27 | ``` | |
28 | | |
29 | ### Config | |
30 | | |
31 | 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. | |
32 | | |
33 |  | |
34 | | |
35 |  | |
36 | | |
37 | ### Sign in button | |
38 | | |
39 | `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. | |
40 | | |
41 | ```dart | |
42 | import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart'; | |
43 | | |
44 | SignInWithAppleButton( | |
45 | caller: client.modules.auth, | |
46 | ); | |
47 | ``` | |
48 | | |
49 | The SignInWithAppleButton widget takes a caller parameter that you pass in the authentication module from your Serverpod client, in this case, client.modules.auth. | |
50 | | |
51 |  | |
52 | | |
53 | ## Extra | |
54 | | |
55 | 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. | |
56 | | |
57 | _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._ | |
58 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/05-firebase.md: | |
-------------------------------------------------------------------------------- | |
1 | # Firebase | |
2 | | |
3 | 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. | |
4 | | |
5 | :::warning | |
6 | | |
7 | 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. | |
8 | | |
9 | ::: | |
10 | | |
11 | ## Server-side configuration | |
12 | | |
13 | 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`. | |
14 | | |
15 |  | |
16 | | |
17 | 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. | |
18 | | |
19 | :::danger | |
20 | The firebase_service_account_key.json file gives admin access to your Firebase project, never store it in version control. | |
21 | ::: | |
22 | | |
23 | ## Client-side configuration | |
24 | | |
25 | 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. | |
26 | | |
27 | ## Firebase config | |
28 | | |
29 | The short version: | |
30 | | |
31 | ```bash | |
32 | $ flutter pub add firebase_core firebase_auth firebase_ui_auth | |
33 | $ flutterfire configure | |
34 | ``` | |
35 | | |
36 | In the Firebase console, configure the different social sign-ins you plan to use, under `Authentication > Sign-in method`. | |
37 | | |
38 |  | |
39 | | |
40 | In your `main.dart` in your flutter project add: | |
41 | | |
42 | ```dart | |
43 | import 'package:firebase_ui_auth/firebase_ui_auth.dart' as firebase; | |
44 | import 'package:firebase_core/firebase_core.dart'; | |
45 | import 'firebase_options.dart'; | |
46 | | |
47 | ... | |
48 | void main() async { | |
49 | ... | |
50 | await Firebase.initializeApp( | |
51 | options: DefaultFirebaseOptions.currentPlatform, | |
52 | ); | |
53 | | |
54 | firebase.FirebaseUIAuth.configureProviders([ | |
55 | firebase.PhoneAuthProvider(), | |
56 | ]); | |
57 | | |
58 | ... | |
59 | runApp(const MyApp()); | |
60 | } | |
61 | ``` | |
62 | | |
63 | ## Trigger the auth UI with Serverpod | |
64 | | |
65 | Add the [serverpod_auth_firebase_flutter](https://pub.dev/packages/serverpod_auth_firebase_flutter) package. | |
66 | | |
67 | ```bash | |
68 | $ flutter pub add serverpod_auth_firebase_flutter | |
69 | ``` | |
70 | | |
71 | The `SignInWithFirebaseButton` is a convenient button that triggers the sign-in flow and can be used like this: | |
72 | | |
73 | ```dart | |
74 | SignInWithFirebaseButton( | |
75 | caller: client.modules.auth, | |
76 | authProviders: [ | |
77 | firebase.PhoneAuthProvider(), | |
78 | ], | |
79 | onFailure: () => print('Failed to sign in with Firebase.'), | |
80 | onSignedIn: () => print('Signed in with Firebase.'), | |
81 | ) | |
82 | ``` | |
83 | | |
84 | 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. | |
85 | | |
86 | You can also trigger the Firebase auth UI by calling the method `signInWithFirebase` like so: | |
87 | | |
88 | ```dart | |
89 | await signInWithFirebase( | |
90 | context: context, | |
91 | caller: client.modules.auth, | |
92 | authProviders: [ | |
93 | firebase.PhoneAuthProvider(), | |
94 | ], | |
95 | ); | |
96 | ``` | |
97 | | |
98 | 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. | |
99 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/04-providers/06-custom-providers.md: | |
-------------------------------------------------------------------------------- | |
1 | # Custom providers | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Server setup | |
6 | | |
7 | 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. | |
8 | | |
9 | ### Connect user | |
10 | | |
11 | The authentication module provides methods to find or create users. This ensures that all authentication tokens from the same user are connected. | |
12 | | |
13 | Users can be identified either by their email through the `Users.findUserByEmail(...)` method or by a unique identifier through the `Users.findUserByIdentifier(...)` method. | |
14 | | |
15 | If no user is found, a new user can be created through the `Users.createUser(...)` method. | |
16 | | |
17 | ```dart | |
18 | UserInfo? userInfo; | |
19 | userInfo = await Users.findUserByEmail(session, email); | |
20 | userInfo ??= await Users.findUserByIdentifier(session, userIdentifier); | |
21 | if (userInfo == null) { | |
22 | userInfo = UserInfo( | |
23 | userIdentifier: userIdentifier, | |
24 | userName: name, | |
25 | email: email, | |
26 | blocked: false, | |
27 | created: DateTime.now().toUtc(), | |
28 | scopeNames: [], | |
29 | ); | |
30 | userInfo = await Users.createUser(session, userInfo, _authMethod); | |
31 | } | |
32 | ``` | |
33 | | |
34 | 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. | |
35 | | |
36 | :::note | |
37 | 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. | |
38 | ::: | |
39 | | |
40 | ### Custom identification methods | |
41 | | |
42 | 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. | |
43 | | |
44 | ```dart | |
45 | var userInfo = await UserInfo.db.findFirstRow( | |
46 | session, | |
47 | where: (t) => t.fullName.equals(name), | |
48 | ); | |
49 | ``` | |
50 | | |
51 | The example above shows how to find a user by name using the `UserInfo` model. | |
52 | | |
53 | ### Create auth token | |
54 | | |
55 | When a user has been found or created, an auth token that is connected to the user should be created. | |
56 | | |
57 | To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible as a static method, e.g. `UserAuthentication.signInUser`. | |
58 | | |
59 | 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. | |
60 | | |
61 | ```dart | |
62 | var authToken = await UserAuthentication.signInUser(userInfo.id, 'myAuthMethod', scopes: { | |
63 | Scope('delete'), | |
64 | Scope('create'), | |
65 | }); | |
66 | ``` | |
67 | | |
68 | 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`. | |
69 | | |
70 | :::info | |
71 | 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. | |
72 | ::: | |
73 | | |
74 | ### Send auth token to client | |
75 | | |
76 | 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. | |
77 | | |
78 | ```dart | |
79 | class MyAuthenticationEndpoint extends Endpoint { | |
80 | Future<AuthenticationResponse> login( | |
81 | Session session, | |
82 | String username, | |
83 | String password, | |
84 | ) async { | |
85 | // Authenticates a user with email and password. | |
86 | if (!authenticateUser(session, username, password)) { | |
87 | return AuthenticationResponse(success: false); | |
88 | } | |
89 | | |
90 | // Finds or creates a user in the database using the User methods. | |
91 | var userInfo = findOrCreateUser(session, username); | |
92 | | |
93 | // Creates an authentication key for the user. | |
94 | var authToken = await UserAuthentication.signInUser( | |
95 | session, | |
96 | userInfo.id!, | |
97 | 'myAuth', | |
98 | scopes: {}, | |
99 | ); | |
100 | | |
101 | // Returns the authentication response. | |
102 | return AuthenticationResponse( | |
103 | success: true, | |
104 | keyId: authToken.id, | |
105 | key: authToken.key, | |
106 | userInfo: userInfo, | |
107 | ); | |
108 | } | |
109 | } | |
110 | ``` | |
111 | | |
112 | The example above shows how to create an `AuthenticationResponse` with the auth token and user information. | |
113 | | |
114 | ### Revoking authentication keys | |
115 | | |
116 | 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: | |
117 | | |
118 | - Closing all affected method streaming connections to maintain connection integrity. | |
119 | - Synchronizing authentication state across all connected servers. | |
120 | - Updating the session's authentication state with `session.updateAuthenticated(null)` if the affected user is currently authenticated. | |
121 | | |
122 | #### Revoking specific keys | |
123 | | |
124 | To revoke specific authentication keys, use the `revokeAuthKey` method: | |
125 | | |
126 | ```dart | |
127 | await UserAuthentication.revokeAuthKey( | |
128 | session, | |
129 | authKeyId: 'auth-key-id-here', | |
130 | ); | |
131 | ``` | |
132 | | |
133 | ##### Fetching and revoking an authentication key using AuthenticationInfo | |
134 | | |
135 | To revoke a specific authentication key for the current session, you can directly access the session's authentication information and call the `revokeAuthKey` method: | |
136 | | |
137 | ```dart | |
138 | // Fetch the authentication information for the current session | |
139 | var authId = (await session.authenticated)?.authId; | |
140 | | |
141 | // Revoke the authentication key if the session is authenticated and has an authId | |
142 | if (authId != null) { | |
143 | await UserAuthentication.revokeAuthKey( | |
144 | session, | |
145 | authKeyId: authId, | |
146 | ); | |
147 | } | |
148 | ``` | |
149 | | |
150 | ##### Fetching and revoking a specific authentication key for a user | |
151 | | |
152 | 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: | |
153 | | |
154 | ```dart | |
155 | // Fetch all authentication keys for the user | |
156 | var authKeys = await AuthKey.db.find( | |
157 | session, | |
158 | where: (t) => t.userId.equals(userId), | |
159 | ); | |
160 | | |
161 | // Revoke a specific key (for example, the last one) | |
162 | if (authKeys.isNotEmpty) { | |
163 | var authKeyId = authKeys.last.id.toString(); // Convert the ID to string | |
164 | await UserAuthentication.revokeAuthKey( | |
165 | session, | |
166 | authKeyId: authKeyId, | |
167 | ); | |
168 | } | |
169 | ``` | |
170 | | |
171 | ##### Removing specific tokens (direct deletion) | |
172 | | |
173 | ```dart | |
174 | await AuthKey.db.deleteWhere( | |
175 | session, | |
176 | where: (t) => t.userId.equals(userId) & t.method.equals('username'), | |
177 | ); | |
178 | ``` | |
179 | | |
180 | :::warning | |
181 | | |
182 | 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. | |
183 | | |
184 | ::: | |
185 | | |
186 | #### Signing out all devices | |
187 | | |
188 | The `signOutUser` method signs a user out from all devices: | |
189 | | |
190 | ```dart | |
191 | await UserAuthentication.signOutUser( | |
192 | session, | |
193 | userId: 123, // Optional: If omitted, the currently authenticated user will be signed out | |
194 | ); | |
195 | ``` | |
196 | This method deletes all authentication keys associated with the user. | |
197 | | |
198 | ##### Signing out a specific user | |
199 | | |
200 | In this example, a specific `userId` is provided to sign out that user from all their devices: | |
201 | | |
202 | ```dart | |
203 | // Sign out the user with ID 123 from all devices | |
204 | await UserAuthentication.signOutUser( | |
205 | session, | |
206 | userId: 123, | |
207 | ); | |
208 | ``` | |
209 | | |
210 | ##### Signing out the currently authenticated user | |
211 | | |
212 | If no `userId` is provided, `signOutUser` will automatically sign out the user who is currently authenticated in the session: | |
213 | | |
214 | ```dart | |
215 | // Sign out the currently authenticated user | |
216 | await UserAuthentication.signOutUser( | |
217 | session, // No userId provided, signs out the current user | |
218 | ); | |
219 | ``` | |
220 | | |
221 | #### Creating a logout endpoint | |
222 | | |
223 | To sign out a user on all devices using an endpoint, the `signOutUser` method in the `UserAuthentication` class can be used: | |
224 | | |
225 | ```dart | |
226 | class AuthenticatedEndpoint extends Endpoint { | |
227 | @override | |
228 | bool get requireLogin => true; | |
229 | | |
230 | Future<void> logout(Session session) async { | |
231 | await UserAuthentication.signOutUser(session); | |
232 | } | |
233 | } | |
234 | ``` | |
235 | | |
236 | ## Client setup | |
237 | | |
238 | 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`. | |
239 | | |
240 | 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. | |
241 | | |
242 | 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`. | |
243 | | |
244 | ```dart | |
245 | var serverResponse = await caller.myAuthentication.login(username, password); | |
246 | | |
247 | if (serverResponse.success) { | |
248 | // Store the user info in the session manager. | |
249 | SessionManager sessionManager = await SessionManager.instance; | |
250 | await sessionManager.registerSignedInUser( | |
251 | serverResponse.userInfo!, | |
252 | serverResponse.keyId!, | |
253 | serverResponse.key!, | |
254 | ); | |
255 | } | |
256 | ``` | |
257 | | |
258 | The example above shows how to register a signed-in user in the session manager. | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/11-authentication/05-custom-overrides.md: | |
-------------------------------------------------------------------------------- | |
1 | # Custom overrides | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Server setup | |
6 | | |
7 | When running a custom auth integration it is up to you to build the authentication model and issuing auth tokens. | |
8 | | |
9 | ### Token validation | |
10 | | |
11 | 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`. | |
12 | | |
13 | ```dart | |
14 | // Initialize Serverpod and connect it with your generated code. | |
15 | final pod = Serverpod( | |
16 | args, | |
17 | Protocol(), | |
18 | Endpoints(), | |
19 | authenticationHandler: (Session session, String token) async { | |
20 | /// Custom validation handler | |
21 | if (token != 'valid') return null; | |
22 | | |
23 | return AuthenticationInfo(1, <Scope>{}); | |
24 | }, | |
25 | ); | |
26 | ``` | |
27 | | |
28 | 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`. | |
29 | | |
30 | :::note | |
31 | 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. | |
32 | ::: | |
33 | | |
34 | :::info | |
35 | 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. | |
36 | ::: | |
37 | | |
38 | #### Scopes | |
39 | | |
40 | 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. | |
41 | | |
42 | 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. | |
43 | | |
44 | ```dart | |
45 | List<String> scopes = extractScopes(token); | |
46 | Set<Scope> userScopes = scopes.map((scope) => Scope(scope)).toSet(); | |
47 | ``` | |
48 | | |
49 | ### Handling revoked authentication | |
50 | | |
51 | 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. | |
52 | | |
53 | ```dart | |
54 | var userId = 1; | |
55 | var revokedScopes = ['write']; | |
56 | var message = RevokedAuthenticationScope( | |
57 | scopes: revokedScopes, | |
58 | ); | |
59 | | |
60 | await session.messages.authenticationRevoked( | |
61 | userId, | |
62 | message, | |
63 | ); | |
64 | ``` | |
65 | | |
66 | ##### Parameters | |
67 | | |
68 | - `userId` - The user id belonging to the `AuthenticationInfo` object to be revoked. | |
69 | - `message` - The revoked authentication event message. See below for the different type of messages. | |
70 | | |
71 | #### Revoked authentication messages | |
72 | There are three types of `RevokedAuthentication` messages that are used to specify the extent of the authentication revocation: | |
73 | | |
74 | | Message type | Description | | |
75 | |-----------|-------------| | |
76 | | `RevokedAuthenticationUser` | All authentication is revoked for a user. | | |
77 | | `RevokedAuthenticationAuthId` | A single authentication id is revoked for the user. This should match the `authId` field in the `AuthenticationInfo` object. | | |
78 | | `RevokedAuthenticationScope` | List of scopes that have been revoked for a user. | | |
79 | | |
80 | Each message type provides a tailored approach to revoke authentication based on different needs. | |
81 | | |
82 | ### Send token to client | |
83 | | |
84 | You are responsible for implementing the endpoints to authenticate/authorize the user. But as an example such an endpoint could look like the following. | |
85 | | |
86 | ```dart | |
87 | class UserEndpoint extends Endpoint { | |
88 | Future<LoginResponse> login( | |
89 | Session session, | |
90 | String username, | |
91 | String password, | |
92 | ) async { | |
93 | var identifier = authenticateUser(session, username, password); | |
94 | if (identifier == null) return null; | |
95 | | |
96 | return issueMyToken(identifier, scopes: {}); | |
97 | } | |
98 | } | |
99 | ``` | |
100 | | |
101 | In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client. | |
102 | | |
103 | ## Client setup | |
104 | | |
105 | 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. | |
106 | | |
107 | ### Configure key manager | |
108 | | |
109 | 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. | |
110 | | |
111 | ```dart | |
112 | class SimpleAuthKeyManager extends AuthenticationKeyManager { | |
113 | String? _key; | |
114 | | |
115 | @override | |
116 | Future<String?> get() async { | |
117 | return _key; | |
118 | } | |
119 | | |
120 | @override | |
121 | Future<void> put(String key) async { | |
122 | _key = key; | |
123 | } | |
124 | | |
125 | @override | |
126 | Future<void> remove() async { | |
127 | _key = null; | |
128 | } | |
129 | } | |
130 | | |
131 | | |
132 | var client = Client('http://$localhost:8080/', | |
133 | authenticationKeyManager: SimpleAuthKeyManager()) | |
134 | ..connectivityMonitor = FlutterConnectivityMonitor(); | |
135 | ``` | |
136 | | |
137 | In the above example, the `SimpleAuthKeyManager` is configured as the client's authentication key manager. The `SimpleAuthKeyManager` stores the token in memory. | |
138 | | |
139 | :::info | |
140 | | |
141 | 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. | |
142 | | |
143 | ::: | |
144 | | |
145 | The key manager is then available through the client's `authenticationKeyManager` field. | |
146 | | |
147 | ```dart | |
148 | var keyManager = client.authenticationKeyManager; | |
149 | ``` | |
150 | | |
151 | ### Store token | |
152 | | |
153 | 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. | |
154 | | |
155 | ```dart | |
156 | await client.authenticationKeyManager?.put(token); | |
157 | ``` | |
158 | | |
159 | In the above example, the `token` is placed in the key manager. It will now be included in communication with the server. | |
160 | | |
161 | ### Remove token | |
162 | | |
163 | To remove the token from the key manager, call the `remove` method. | |
164 | | |
165 | ```dart | |
166 | await client.authenticationKeyManager?.remove(); | |
167 | ``` | |
168 | | |
169 | The above example removes any token from the key manager. | |
170 | | |
171 | ### Retrieve token | |
172 | | |
173 | To retrieve the token from the key manager, call the `get` method. | |
174 | | |
175 | ```dart | |
176 | var token = await client.authenticationKeyManager?.get(); | |
177 | ``` | |
178 | | |
179 | The above example retrieves the token from the key manager and stores it in the `token` variable. | |
180 | | |
181 | ## Authentication schemes | |
182 | | |
183 | 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. | |
184 | | |
185 | 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. | |
186 | | |
187 | In other words the default transport implementation is "invisible" to user code. | |
188 | | |
189 | ### Implementing your own authentication scheme | |
190 | | |
191 | 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. | |
192 | | |
193 | 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. | |
194 | | |
195 | You will also need to implement the `AuthenticationHandler` accordingly, in order to process that header value server-side. | |
196 | | |
197 | The header value must be compliant with the HTTP header format defined in RFC 9110 HTTP Semantics, 11.6.2. Authorization. | |
198 | See: | |
199 | - [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) | |
200 | - [RFC 9110, 11.6.2. Authorization](https://httpwg.org/specs/rfc9110.html#field.authorization) | |
201 | | |
202 | An approach to adding OAuth handling might make changes to the above code akin to the following. | |
203 | | |
204 | Client side: | |
205 | | |
206 | ```dart | |
207 | class MyOAuthKeyManager extends AuthenticationKeyManager { | |
208 | String? _key; | |
209 | | |
210 | @override | |
211 | Future<String?> get() async { | |
212 | return _key; | |
213 | } | |
214 | | |
215 | @override | |
216 | Future<void> put(String key) async { | |
217 | _key = key; | |
218 | } | |
219 | | |
220 | @override | |
221 | Future<void> remove() async { | |
222 | _key = null; | |
223 | } | |
224 | | |
225 | @override | |
226 | Future<String?> toHeaderValue(String? key) async { | |
227 | if (key == null) return null; | |
228 | return 'Bearer ${myBearerTokenObtainer(key)}'; | |
229 | } | |
230 | } | |
231 | | |
232 | | |
233 | var client = Client('http://$localhost:8080/', | |
234 | authenticationKeyManager: SimpleAuthKeyManager()) | |
235 | ..connectivityMonitor = FlutterConnectivityMonitor(); | |
236 | ``` | |
237 | | |
238 | Server side: | |
239 | | |
240 | ```dart | |
241 | // Initialize Serverpod and connect it with your generated code. | |
242 | final pod = Serverpod( | |
243 | args, | |
244 | Protocol(), | |
245 | Endpoints(), | |
246 | authenticationHandler: (Session session, String token) async { | |
247 | /// Bearer token validation handler | |
248 | var (uid, scopes) = myBearerTokenValidator(token) | |
249 | if (uid == null) return null; | |
250 | | |
251 | return AuthenticationInfo(uid, scopes); | |
252 | }, | |
253 | ); | |
254 | ``` | |
255 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/12-file-uploads.md: | |
-------------------------------------------------------------------------------- | |
1 | # Uploading files | |
2 | | |
3 | 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. | |
4 | | |
5 | ## How to upload a file | |
6 | | |
7 | 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. | |
8 | | |
9 | ### Server-side code | |
10 | | |
11 | 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. | |
12 | | |
13 | ```dart | |
14 | Future<String?> getUploadDescription(Session session, String path) async { | |
15 | return await session.storage.createDirectFileUploadDescription( | |
16 | storageId: 'public', | |
17 | path: path, | |
18 | ); | |
19 | } | |
20 | ``` | |
21 | | |
22 | 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. | |
23 | | |
24 | ```dart | |
25 | Future<bool> verifyUpload(Session session, String path) async { | |
26 | return await session.storage.verifyDirectFileUpload( | |
27 | storageId: 'public', | |
28 | path: path, | |
29 | ); | |
30 | } | |
31 | ``` | |
32 | | |
33 | ### Client-side code | |
34 | | |
35 | 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. | |
36 | | |
37 | ```dart | |
38 | var uploadDescription = await client.myEndpoint.getUploadDescription('myfile'); | |
39 | if (uploadDescription != null) { | |
40 | var uploader = FileUploader(uploadDescription); | |
41 | await uploader.upload(myStream); | |
42 | var success = await client.myEndpoint.verifyUpload('myfile'); | |
43 | } | |
44 | ``` | |
45 | | |
46 | :::info | |
47 | | |
48 | 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.: | |
49 | | |
50 | ```dart | |
51 | 'profile/$userId/images/avatar.png' | |
52 | ``` | |
53 | | |
54 | ::: | |
55 | | |
56 | ## Accessing stored files | |
57 | | |
58 | 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. | |
59 | | |
60 | To check if a file exists, use the `fileExists` method. | |
61 | | |
62 | ```dart | |
63 | var exists = await session.storage.fileExists( | |
64 | storageId: 'public', | |
65 | path: 'my/file/path', | |
66 | ); | |
67 | ``` | |
68 | | |
69 | If the file is in a public storage, you can access it through its URL. | |
70 | | |
71 | ```dart | |
72 | var url = await session.storage.getPublicUrl( | |
73 | storageId: 'public', | |
74 | path: 'my/file/path', | |
75 | ); | |
76 | ``` | |
77 | | |
78 | You can also directly retrieve or store a file from your server. | |
79 | | |
80 | ```dart | |
81 | var myByteData = await session.storage.retrieveFile( | |
82 | storageId: 'public', | |
83 | path: 'my/file/path', | |
84 | ); | |
85 | ``` | |
86 | | |
87 | ## Add a configuration for GCP | |
88 | | |
89 | 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: | |
90 | | |
91 | 1. Create a service account with the _Storage Admin_ role. | |
92 | 2. Under _Cloud Storage_ > _Settings_ > _Interoperability_, create a new HMAC key for your newly created service account. | |
93 | 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. You can also pass them in as environment variables. The environment variable names are `SERVERPOD_HMAC_ACCESS_KEY_ID` and `SERVERPOD_HMAC_SECRET_KEY`. | |
94 | 4. When creating a new bucket, set the _Access control_ to _Fine-grained_ and disable the _Prevent public access_ option. | |
95 | | |
96 | You may also want to add the bucket as a backend for your load balancer to give it a custom domain name. | |
97 | | |
98 | 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. | |
99 | | |
100 | ```bash | |
101 | $ dart pub add serverpod_cloud_storage_gcp | |
102 | ``` | |
103 | | |
104 | ```dart | |
105 | import 'package:serverpod_cloud_storage_gcp/serverpod_cloud_storage_gcp.dart' | |
106 | as gcp; | |
107 | ``` | |
108 | | |
109 | 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. | |
110 | | |
111 | ```dart | |
112 | pod.addCloudStorage(gcp.GoogleCloudStorage( | |
113 | serverpod: pod, | |
114 | storageId: 'public', | |
115 | public: true, | |
116 | region: 'auto', | |
117 | bucket: 'my-bucket-name', | |
118 | publicHost: 'storage.myapp.com', | |
119 | )); | |
120 | ``` | |
121 | | |
122 | ## Add a configuration for AWS S3 | |
123 | | |
124 | 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 (`AWSAccessKeyId` and `AWSAccessKey`) or pass them in as environment variables (`SERVERPOD_AWS_ACCESS_KEY_ID` and `SERVERPOD_AWS_ACCESS_KEY`). | |
125 | | |
126 | 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. | |
127 | | |
128 | ```bash | |
129 | $ dart pub add serverpod_cloud_storage_s3 | |
130 | ``` | |
131 | | |
132 | ```dart | |
133 | import 'package:serverpod_cloud_storage_s3/serverpod_cloud_storage_s3.dart' | |
134 | as s3; | |
135 | ``` | |
136 | | |
137 | 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. | |
138 | | |
139 | ```dart | |
140 | pod.addCloudStorage(s3.S3CloudStorage( | |
141 | serverpod: pod, | |
142 | storageId: 'public', | |
143 | public: true, | |
144 | region: 'us-west-2', | |
145 | bucket: 'my-bucket-name', | |
146 | publicHost: 'storage.myapp.com', | |
147 | )); | |
148 | ``` | |
149 | | |
150 | 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. | |
151 | | |
152 | ```yaml | |
153 | shared: | |
154 | AWSAccessKeyId: 'XXXXXXXXXXXXXX' | |
155 | AWSSecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' | |
156 | ``` | |
157 | | |
158 | :::info | |
159 | | |
160 | 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. | |
161 | | |
162 | ::: | |
163 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/13-health-checks.md: | |
-------------------------------------------------------------------------------- | |
1 | # Health checks | |
2 | | |
3 | 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. | |
4 | | |
5 | ## Adding custom metrics | |
6 | | |
7 | 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. | |
8 | | |
9 | ```dart | |
10 | // Create your custom health metric handler. | |
11 | Future<List<ServerHealthMetric>> myHealthCheckHandler( | |
12 | Serverpod pod, DateTime timestamp) async { | |
13 | // Actually perform some checks. | |
14 | | |
15 | // Return a list of health metrics for the given timestamp. | |
16 | return [ | |
17 | ServerHealthMetric( | |
18 | name: 'MyMetric', | |
19 | serverId: pod.serverId, | |
20 | timestamp: timestamp, | |
21 | isHealthy: true, | |
22 | value: 1.0, | |
23 | ), | |
24 | ]; | |
25 | } | |
26 | ``` | |
27 | | |
28 | Register your handler when you create your Serverpod object. | |
29 | | |
30 | ```dart | |
31 | final pod = Serverpod( | |
32 | args, | |
33 | Protocol(), | |
34 | Endpoints(), | |
35 | healthCheckHandler: myHealthCheckHandler, | |
36 | ); | |
37 | ``` | |
38 | | |
39 | 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. | |
40 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/14-scheduling.md: | |
-------------------------------------------------------------------------------- | |
1 | # Scheduling | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | 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)). | |
8 | | |
9 | :::info | |
10 | The future call feature is not enabled when running Serverpod in serverless mode. | |
11 | ::: | |
12 | | |
13 | ```dart | |
14 | import 'package:serverpod/serverpod.dart'; | |
15 | | |
16 | class ExampleFutureCall extends FutureCall<MyModelEntity> { | |
17 | @override | |
18 | Future<void> invoke(Session session, MyModelEntity? object) async { | |
19 | // Do something interesting in the future here. | |
20 | } | |
21 | } | |
22 | ``` | |
23 | | |
24 | 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. | |
25 | | |
26 | ```dart | |
27 | void run(List<String> args) async { | |
28 | final pod = Serverpod( | |
29 | args, | |
30 | Protocol(), | |
31 | Endpoints(), | |
32 | ); | |
33 | | |
34 | ... | |
35 | | |
36 | pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall'); | |
37 | | |
38 | ... | |
39 | } | |
40 | ``` | |
41 | | |
42 | 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. | |
43 | | |
44 | Invoke the future call 1 hour from now by calling `futureCallWithDelay`. | |
45 | | |
46 | ```dart | |
47 | await session.serverpod.futureCallWithDelay( | |
48 | 'exampleFutureCall', | |
49 | data, | |
50 | const Duration(hours: 1), | |
51 | ); | |
52 | ``` | |
53 | | |
54 | Invoke the future call at a specific time and/or date in the future by calling `futureCallAtTime`. | |
55 | | |
56 | ```dart | |
57 | await session.serverpod.futureCallAtTime( | |
58 | 'exampleFutureCall', | |
59 | data, | |
60 | DateTime(2025, 1, 1), | |
61 | ); | |
62 | ``` | |
63 | | |
64 | :::note | |
65 | `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. | |
66 | ::: | |
67 | | |
68 | 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. | |
69 | | |
70 | ```dart | |
71 | await session.serverpod.futureCallWithDelay( | |
72 | 'exampleFutureCall', | |
73 | data, | |
74 | const Duration(hours: 1), | |
75 | identifier: 'an-identifying-string', | |
76 | ); | |
77 | ``` | |
78 | | |
79 | This identifier can then be used to cancel all future calls registered with said identifier. | |
80 | | |
81 | ```dart | |
82 | await session.serverpod.cancelFutureCall('an-identifying-string'); | |
83 | ``` | |
84 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/15-streams.md: | |
-------------------------------------------------------------------------------- | |
1 | # Streams | |
2 | | |
3 | 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. | |
4 | | |
5 | 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. | |
6 | | |
7 | :::tip | |
8 | | |
9 | 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. | |
10 | | |
11 | ::: | |
12 | | |
13 | ## Streaming Methods | |
14 | | |
15 | 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. | |
16 | | |
17 | ### Defining a streaming method | |
18 | | |
19 | Streaming methods are defined by using the `Stream` type as either the return value or a parameter. | |
20 | | |
21 | Following is an example of a streaming method that echoes back any message: | |
22 | | |
23 | ```dart | |
24 | class ExampleEndpoint extends Endpoint { | |
25 | Stream echoStream(Session session, Stream stream) async* { | |
26 | await for (var message in stream) { | |
27 | yield message; | |
28 | } | |
29 | } | |
30 | } | |
31 | ``` | |
32 | | |
33 | The generic for the `Stream` can also be defined, e.g., `Stream<String>`. This definition is then included in the client, enabling static type validation. | |
34 | | |
35 | The streaming method above can then be called from the client like this: | |
36 | | |
37 | ```dart | |
38 | var inStream = StreamController(); | |
39 | var outStream = client.example.echoStream(inStream.stream); | |
40 | outStream.listen((message) { | |
41 | print('Received message: $message'); | |
42 | }); | |
43 | | |
44 | inStream.add('Hello'); | |
45 | inStream.add(42); | |
46 | | |
47 | // This will print | |
48 | // Received message: Hello | |
49 | // Received message: 42 | |
50 | ``` | |
51 | | |
52 | In the example above, the `echoStream` method passes back any message sent through the `outStream`. | |
53 | | |
54 | :::tip | |
55 | | |
56 | 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. | |
57 | | |
58 | ::: | |
59 | | |
60 | ### Lifecycle of a streaming method | |
61 | | |
62 | 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. | |
63 | | |
64 | If the web socket connection is lost, all streaming methods are closed on the server and the client. | |
65 | | |
66 | 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. | |
67 | | |
68 | When the streaming method returns a `Future`, the method is kept alive until the method returns. | |
69 | | |
70 | 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. | |
71 | | |
72 | All streams in parameters are closed when the method call is over. | |
73 | | |
74 | ### Authentication | |
75 | | |
76 | Authentication is seamlessly integrated into streaming method calls. When a client initiates a streaming method, the server automatically authenticates the session. | |
77 | | |
78 | 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. | |
79 | | |
80 | For more details on handling revoked authentication, refer to the section on [handling revoked authentication](authentication/custom-overrides#Handling-revoked-authentication). | |
81 | | |
82 | ### Error handling | |
83 | | |
84 | 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. | |
85 | | |
86 | This is supported in both directions; stream parameters can pass exceptions to the server, and return streams can pass exceptions to the client. | |
87 | | |
88 | ```dart | |
89 | class ExampleEndpoint extends Endpoint { | |
90 | Stream echoStream(Session session, Stream stream) async* { | |
91 | stream.listen((message) { | |
92 | // Do nothing | |
93 | }, onError: (error) { | |
94 | print('Server received error: $error'); | |
95 | throw SerializableException('Error from server'); | |
96 | }); | |
97 | } | |
98 | } | |
99 | ``` | |
100 | | |
101 | ```dart | |
102 | var inStream = StreamController(); | |
103 | var outStream = client.example.echoStream(inStream.stream); | |
104 | outStream.listen((message) { | |
105 | // Do nothing | |
106 | }, onError: (error) { | |
107 | print('Client received error: $error'); | |
108 | }); | |
109 | | |
110 | inStream.addError(SerializableException('Error from client')); | |
111 | | |
112 | // This will print | |
113 | // Server received error: Error from client | |
114 | // Client received error: Error from server | |
115 | ``` | |
116 | | |
117 | 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. | |
118 | | |
119 | Read more about serializable exceptions here: [Serializable exceptions](exceptions). | |
120 | | |
121 | ## Streaming Endpoints | |
122 | | |
123 | 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. | |
124 | | |
125 | ### Handling streams server-side | |
126 | | |
127 | The Endpoint class has three methods you override to work with streams. | |
128 | | |
129 | - `streamOpened` is called when a user connects to a stream on the Endpoint. | |
130 | - `streamClosed` is called when a user disconnects from a stream on the Endpoint. | |
131 | - `handleStreamMessage` is called when a serialized message is received from a client. | |
132 | | |
133 | To send a message to a client, call the `sendStreamMessage` method. You will need to include the session associated with the user. | |
134 | | |
135 | #### The user object | |
136 | | |
137 | It's often handy to associate a state together with a streaming session. Typically, you do this when a stream is opened. | |
138 | | |
139 | ```dart | |
140 | Future<void> streamOpened(StreamingSession session) async { | |
141 | setUserObject(session, MyUserObject()); | |
142 | } | |
143 | ``` | |
144 | | |
145 | You can access the user object at any time by calling the `getUserObject` method. The user object is automatically discarded when a session ends. | |
146 | | |
147 | ### Handling streams in your app | |
148 | | |
149 | 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. | |
150 | | |
151 | ```dart | |
152 | await client.openStreamingConnection(); | |
153 | | |
154 | ``` | |
155 | | |
156 | You can monitor the state of the connection by adding a listener to the client. | |
157 | Once connected to your server's web socket, you can pass and receive serialized objects. | |
158 | | |
159 | Listen to its web socket stream to receive updates from an endpoint on the server. | |
160 | | |
161 | ```dart | |
162 | await for (var message in client.myEndpoint.stream) { | |
163 | _handleMessage(message); | |
164 | } | |
165 | ``` | |
166 | | |
167 | You send messages to the server's endpoint by calling `sendStreamMessage`. | |
168 | | |
169 | ```dart | |
170 | client.myEndpoint.sendStreamMessage(MyMessage(text: 'Hello')); | |
171 | ``` | |
172 | | |
173 | :::info | |
174 | | |
175 | Authentication is handled automatically. If you have signed in, your web socket connection will be authenticated. | |
176 | | |
177 | ::: | |
178 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/16-server-events.md: | |
-------------------------------------------------------------------------------- | |
1 | # Server events | |
2 | | |
3 | Serverpod framework comes with a built-in event messaging system. This enables efficient message exchange within and across servers, making it ideal for scenarios where shared state is needed, such as coordinating streams or managing data across a server cluster. | |
4 | | |
5 | The event message system is accessed on the `Session` object through the field `messages`. | |
6 | | |
7 | ## Quick Reference | |
8 | | |
9 | Here is a quick reference to the key messaging methods: | |
10 | | |
11 | | Method | Description | | |
12 | |--------|---------| | |
13 | | `postMessage` | Send a message to a channel. | | |
14 | | `addListener` | Add a listener to a channel. | | |
15 | | `removeListener` | Remove a listener from a channel. | | |
16 | | `createStream` | Create a stream that listens to a channel. | | |
17 | | `revokeAuthentication` | Revoke authentication tokens. | | |
18 | | |
19 | ## Sending messages | |
20 | | |
21 | To send a message, you can use the `postMessage` method. The message is published to the specified channel and needs to be a Serverpod model. | |
22 | | |
23 | ```dart | |
24 | var message = UserUpdate(); // Model that represents changes to user data. | |
25 | session.messages.postMessage('user_updates', message); | |
26 | ``` | |
27 | | |
28 | In the example above, the message published on the `user_updates` channel. Any subscriber to this channel in the server will receive the message. | |
29 | | |
30 | ### Global messages | |
31 | | |
32 | Serverpod uses Redis to pass messages between servers. To send a message to another server, enable Redis and then set the `global` parameter to `true` when posting a message. | |
33 | | |
34 | ```dart | |
35 | var message = UserUpdate(); // Model that represents changes to user data. | |
36 | session.messages.postMessage('user_updates', message, global: true); | |
37 | ``` | |
38 | | |
39 | In the example above, the message is published to the `user_updates` channel and will be received by all servers connected to the same Redis instance. | |
40 | | |
41 | :::warning | |
42 | | |
43 | If Redis is not enabled, sending global messages will throw an exception. | |
44 | | |
45 | ::: | |
46 | | |
47 | ## Receiving messages | |
48 | | |
49 | Receiving messages is just as simple as sending them! Serverpod provides two ways to handle incoming messages: by creating a stream that subscribes to a channel or by adding a listener to a channel. | |
50 | | |
51 | ### Creating a stream | |
52 | | |
53 | To create a stream that subscribes to a channel, use the `createStream` method. The stream will emit a value whenever a message is posted to the channel. | |
54 | | |
55 | ```dart | |
56 | var stream = session.messages.createStream('user_updates'); | |
57 | stream.listen((message) { | |
58 | print('Received message: $message'); | |
59 | }) | |
60 | ``` | |
61 | | |
62 | In the above example, a stream is created that listens to the `user_updates` channel and processes incoming requests. | |
63 | | |
64 | #### Stream lifecycle | |
65 | | |
66 | The stream is automatically closed when the session is closed. If you want to close the stream manually, you simply cancel the stream subscription. | |
67 | | |
68 | ```dart | |
69 | var stream = session.messages.createStream('user_updates'); | |
70 | var subscription = stream.listen((message) { | |
71 | print('Received message: $message'); | |
72 | }); | |
73 | | |
74 | subscription.cancel(); | |
75 | ``` | |
76 | | |
77 | In the above example, the stream is first created and then canceled. | |
78 | | |
79 | ### Adding a listener | |
80 | | |
81 | To add a listener to a channel, use the `addListener` method. The listener will be called whenever a message is posted to the channel. | |
82 | | |
83 | ```dart | |
84 | session.messages.addListener('user_updates', (message) { | |
85 | print('Received message: $message'); | |
86 | }); | |
87 | ``` | |
88 | | |
89 | In the above example, the listener will be called whenever a message is posted to the `user_updates` channel. The listener will be called regardless if a message is published globally by another server or internally by the same server. | |
90 | | |
91 | #### Listener lifecycle | |
92 | | |
93 | The listener is automatically removed when the session is closed. To manually remove a listener, use the `removeListener` method. | |
94 | | |
95 | ```dart | |
96 | var myListenerCallback = (message) { | |
97 | print('Received message: $message'); | |
98 | }; | |
99 | // Register the listener | |
100 | session.messages.addListener('user_updates', myListenerCallback); | |
101 | | |
102 | // Remove the listener | |
103 | session.messages.removeListener('user_updates', myListenerCallback); | |
104 | ``` | |
105 | | |
106 | In the above example, the listener is first added and then removed from the `user_updates` channel. | |
107 | | |
108 | ## Revoke authentication | |
109 | | |
110 | The messaging interface also exposes a method for revoking authentication. For more details on handling revoked authentication, refer to the section on [handling revoked authentication](authentication/custom-overrides#Handling-revoked-authentication). | |
111 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/17-backward-compatibility.md: | |
-------------------------------------------------------------------------------- | |
1 | # Backward compatibility | |
2 | | |
3 | 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. | |
4 | | |
5 | Following a simple set of rules, your server will stay compatible with older app versions: | |
6 | | |
7 | 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. | |
8 | 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. | |
9 | 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. | |
10 | | |
11 | ## Managing breaking changes with endpoint inheritance | |
12 | | |
13 | An [endpoint sub-class](/concepts/working-with-endpoints) can be useful when you have to make a breaking change to an entire endpoint but need to keep supporting existing clients. Doing so allows you to share most of its implementation with the old endpoint. | |
14 | | |
15 | Imagine you had a "team" management endpoint where before a user could join if they had an e-mail address ending in the expected domain, but now it should be opened up for anyone to join if they can provide an "invite code". Additionally, the return type (serialized classes) should be updated across the entire endpoint, which would not be allowed on the existing one. | |
16 | | |
17 | Transitioning from the current to the new endpoint structure might look like this: | |
18 | | |
19 | ```dart | |
20 | @Deprecated('Use TeamV2Endpoint instead') | |
21 | class TeamEndpoint extends Endpoint { | |
22 | Future<TeamInfo> join(Session session) async { | |
23 | // … | |
24 | } | |
25 | | |
26 | // many more methods, like `leave`, etc. | |
27 | } | |
28 | | |
29 | class TeamV2Endpoint extends TeamEndpoint { | |
30 | @override | |
31 | @ignoreEndpoint | |
32 | Future<TeamInfo> join(Session session) async { | |
33 | throw UnimplementedError(); | |
34 | } | |
35 | | |
36 | Future<NewTeamInfo> joinWithCode(Session session, String invitationCode) async { | |
37 | // … | |
38 | } | |
39 | } | |
40 | ``` | |
41 | | |
42 | In the above example, we created a new `TeamV2` endpoint, which hides the `join` method and instead exposes a `joinWithCode` method with the added parameter and the new return type. Additionally all the other inherited (and untouched) methods from the parent class are exposed. | |
43 | | |
44 | While we may have liked to re-use the `join` method name, Dart inheritance rules do not allow doing so. Otherwise, we would have to write the endpoint from scratch, meaning without inheritance, and re-implement all methods we would like to keep. | |
45 | | |
46 | In your client, you could then move all usages from `client.team` to `client.teamV2` and eventually (after all clients have upgraded) remove the old endpoint on the server. That means either marking the old endpoint with `@ignoreEndpoint` on the class or deleting it and moving the re-used method implementations you want to keep to the new V2 endpoint class. | |
47 | | |
48 | An alternative pattern to consider would be to move all the business logic for an endpoint into a helper class and then call into that from the endpoint. In case you want to create a V2 version later, you might be able to reuse most of the underlying business logic through that helper class, and don't have to subclass the old endpoint. This has the added benefit of the endpoint class clearly listing all exposed methods, and you don't have to wonder what you inherit from the base class. | |
49 | | |
50 | Either approach has pros and cons, and it depends on the concrete circumstances to pick the most useful one. Both give you all the tools you need to extend and update your API while gracefully moving clients along and giving them time to update. | |
51 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/18-webserver.md: | |
-------------------------------------------------------------------------------- | |
1 | # Web server | |
2 | | |
3 | 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. | |
4 | | |
5 | :::caution | |
6 | | |
7 | 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. | |
8 | | |
9 | ::: | |
10 | | |
11 | 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. | |
12 | | |
13 | ## Creating new routes and widgets | |
14 | | |
15 | 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. | |
16 | | |
17 | 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. | |
18 | | |
19 | ```dart | |
20 | // Add a single page. | |
21 | pod.webServer.addRoute(MyRoute(), '/my/page/address'); | |
22 | | |
23 | // Match all paths that start with /item/ | |
24 | pod.webServer.addRoute(AnotherRoute(), '/item/*'); | |
25 | ``` | |
26 | | |
27 | Typically, you want to create custom routes for your pages. Do this by overriding the WidgetRoute class and implementing the build method. | |
28 | | |
29 | ```dart | |
30 | class MyRoute extends WidgetRoute { | |
31 | @override | |
32 | Future<Widget> build(Session session, HttpRequest request) async { | |
33 | return MyPageWidget(title: 'Home page'); | |
34 | } | |
35 | } | |
36 | ``` | |
37 | | |
38 | 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. | |
39 | | |
40 | ```dart | |
41 | class MyPageWidget extends Widget { | |
42 | MyPageWidget({String title}) : super(name: 'my_page') { | |
43 | values = { | |
44 | 'title': title, | |
45 | }; | |
46 | } | |
47 | } | |
48 | ``` | |
49 | | |
50 | :::info | |
51 | | |
52 | 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. | |
53 | | |
54 | ::: | |
55 | | |
56 | ## Special widgets and routes | |
57 | | |
58 | 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. | |
59 | | |
60 | - `WidgetList` concatenates a list of other widgets into a single widget. | |
61 | - `WidgetJson` renders a JSON document from a serializable structure of maps, lists, and basic values. | |
62 | - `WidgetRedirect` creates a redirect to another URL. | |
63 | | |
64 | To serve a static directory, use the `RouteStaticDirectory` class. Serverpod will set the correct content types for most file types automatically. | |
65 | | |
66 | :::caution | |
67 | | |
68 | 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. | |
69 | | |
70 | ::: | |
71 | | |
72 | ## Database access and logging | |
73 | | |
74 | 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. | |
75 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/19-testing/01-get-started.md: | |
-------------------------------------------------------------------------------- | |
1 | # Get started | |
2 | | |
3 | Serverpod provides simple but feature rich test tools to make testing your backend a breeze. | |
4 | | |
5 | :::info | |
6 | | |
7 | For Serverpod Mini projects, everything related to the database in this guide can be ignored. | |
8 | | |
9 | ::: | |
10 | | |
11 | <details> | |
12 | <summary> Have an existing project? Follow these steps first!</summary> | |
13 | <p> | |
14 | For existing non-Mini projects, a few extra things need to be done: | |
15 | 1. Add the `server_test_tools_path` key with the value `test/integration/test_tools` to `config/generator.yaml`: | |
16 | | |
17 | ```yaml | |
18 | server_test_tools_path: test/integration/test_tools | |
19 | ``` | |
20 | | |
21 | 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). | |
22 | | |
23 | 2. New projects now come with a test postgres and redis instance 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: | |
24 | | |
25 | ```yaml | |
26 | # Add to the existing services | |
27 | postgres_test: | |
28 | image: postgres:16.3 | |
29 | ports: | |
30 | - '9090:5432' | |
31 | environment: | |
32 | POSTGRES_USER: postgres | |
33 | POSTGRES_DB: <projectname>_test | |
34 | POSTGRES_PASSWORD: "<insert database test password>" | |
35 | volumes: | |
36 | - <projectname>_test_data:/var/lib/postgresql/data | |
37 | redis_test: | |
38 | image: redis:6.2.6 | |
39 | ports: | |
40 | - '9091:6379' | |
41 | command: redis-server --requirepass 'REDIS_TEST_PASSWORD' | |
42 | environment: | |
43 | - REDIS_REPLICATION_MODE=master | |
44 | volumes: | |
45 | # ... | |
46 | <projectname>_test_data: | |
47 | ``` | |
48 | | |
49 | <details> | |
50 | <summary>Or copy the complete file here.</summary> | |
51 | <p> | |
52 | | |
53 | ```yaml | |
54 | services: | |
55 | # Development services | |
56 | postgres: | |
57 | image: postgres:16.3 | |
58 | ports: | |
59 | - '8090:5432' | |
60 | environment: | |
61 | POSTGRES_USER: postgres | |
62 | POSTGRES_DB: <projectname> | |
63 | POSTGRES_PASSWORD: "<insert database development password>" | |
64 | volumes: | |
65 | - <projectname>_data:/var/lib/postgresql/data | |
66 | redis: | |
67 | image: redis:6.2.6 | |
68 | ports: | |
69 | - '8091:6379' | |
70 | command: redis-server --requirepass "<insert redis development password>" | |
71 | environment: | |
72 | - REDIS_REPLICATION_MODE=master | |
73 | | |
74 | # Test services | |
75 | postgres_test: | |
76 | image: postgres:16.3 | |
77 | ports: | |
78 | - '9090:5432' | |
79 | environment: | |
80 | POSTGRES_USER: postgres | |
81 | POSTGRES_DB: <projectname>_test | |
82 | POSTGRES_PASSWORD: "<insert database test password>" | |
83 | volumes: | |
84 | - <projectname>_test_data:/var/lib/postgresql/data | |
85 | redis_test: | |
86 | image: redis:6.2.6 | |
87 | ports: | |
88 | - '9091:6379' | |
89 | command: redis-server --requirepass "<insert redis test password>" | |
90 | environment: | |
91 | - REDIS_REPLICATION_MODE=master | |
92 | | |
93 | volumes: | |
94 | <projectname>_data: | |
95 | <projectname>_test_data: | |
96 | ``` | |
97 | | |
98 | </p> | |
99 | </details> | |
100 | 3. Create a `test.yaml` file and add it to the `config` directory: | |
101 | | |
102 | ```yaml | |
103 | # This is the configuration file for your test environment. | |
104 | # All ports are set to zero in this file which makes the server find the next available port. | |
105 | # This is needed to enable running tests concurrently. To set up your server, you will | |
106 | # need to add the name of the database you are connecting to and the user name. | |
107 | # The password for the database is stored in the config/passwords.yaml. | |
108 | | |
109 | # Configuration for the main API test server. | |
110 | apiServer: | |
111 | port: 0 | |
112 | publicHost: localhost | |
113 | publicPort: 0 | |
114 | publicScheme: http | |
115 | | |
116 | # Configuration for the Insights test server. | |
117 | insightsServer: | |
118 | port: 0 | |
119 | publicHost: localhost | |
120 | publicPort: 0 | |
121 | publicScheme: http | |
122 | | |
123 | # Configuration for the web test server. | |
124 | webServer: | |
125 | port: 0 | |
126 | publicHost: localhost | |
127 | publicPort: 0 | |
128 | publicScheme: http | |
129 | | |
130 | # This is the database setup for your test server. | |
131 | database: | |
132 | host: localhost | |
133 | port: 9090 | |
134 | name: <projectname>_test | |
135 | user: postgres | |
136 | | |
137 | # This is the setup for your Redis test instance. | |
138 | redis: | |
139 | enabled: false | |
140 | host: localhost | |
141 | port: 9091 | |
142 | ``` | |
143 | | |
144 | 4. Add this entry to `config/passwords.yaml` | |
145 | | |
146 | ```yaml | |
147 | test: | |
148 | database: '<insert database test password>' | |
149 | redis: '<insert redis test password>' | |
150 | ``` | |
151 | | |
152 | 5. Add a `dart_test.yaml` file to the `server` directory (next to `pubspec.yaml`) with the following contents: | |
153 | | |
154 | ```yaml | |
155 | tags: | |
156 | integration: {} | |
157 | | |
158 | ``` | |
159 | | |
160 | 6. Finally, add the `test` and `serverpod_test` packages as dev dependencies in `pubspec.yaml`: | |
161 | | |
162 | ```yaml | |
163 | dev_dependencies: | |
164 | serverpod_test: <serverpod version> # Should be same version as the `serverpod` package | |
165 | test: ^1.24.2 | |
166 | ``` | |
167 | | |
168 | That's it, the project setup should be ready to start using the test tools! | |
169 | </p> | |
170 | </details> | |
171 | | |
172 | Go to the server directory and generate the test tools: | |
173 | | |
174 | ```bash | |
175 | serverpod generate | |
176 | ``` | |
177 | | |
178 | 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). | |
179 | | |
180 | The generated file exports a `withServerpod` helper that enables you to call your endpoints directly like regular functions: | |
181 | | |
182 | ```dart | |
183 | import 'package:test/test.dart'; | |
184 | | |
185 | // Import the generated file, it contains everything you need. | |
186 | import 'test_tools/serverpod_test_tools.dart'; | |
187 | | |
188 | void main() { | |
189 | withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { | |
190 | test('when calling `hello` then should return greeting', () async { | |
191 | final greeting = await endpoints.example.hello(sessionBuilder, 'Michael'); | |
192 | expect(greeting, 'Hello Michael'); | |
193 | }); | |
194 | }); | |
195 | } | |
196 | ``` | |
197 | | |
198 | A few things to note from the above example: | |
199 | | |
200 | - The test tools should be imported from the generated test tools file and not the `serverpod_test` package. | |
201 | - The `withServerpod` callback takes two parameters: `sessionBuilder` and `endpoints`. | |
202 | - `sessionBuilder` is used to build a `session` object that represents the server state during an endpoint call and is used to set up scenarios. | |
203 | - `endpoints` contains all your Serverpod endpoints and lets you call them. | |
204 | | |
205 | :::tip | |
206 | | |
207 | 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. | |
208 | | |
209 | ::: | |
210 | | |
211 | Before the test can be run the Postgres and Redis also have to be started: | |
212 | | |
213 | ```bash | |
214 | docker-compose up --build --detach | |
215 | ``` | |
216 | Now the test is ready to be run: | |
217 | | |
218 | ```bash | |
219 | dart test | |
220 | ``` | |
221 | | |
222 | Happy testing! | |
223 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/19-testing/02-the-basics.md: | |
-------------------------------------------------------------------------------- | |
1 | # The basics | |
2 | | |
3 | ## Set up a test scenario | |
4 | | |
5 | 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: | |
6 | | |
7 | |Property|Description|Default| | |
8 | |:---|:---|:---:| | |
9 | |`authentication`|See section [Setting authenticated state](#setting-authenticated-state).|`AuthenticationOverride.unauthenticated()`| | |
10 | |`enableLogging`|Whether logging is turned on for the session.|`false`| | |
11 | | |
12 | 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). | |
13 | | |
14 | 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: | |
15 | | |
16 | ```dart | |
17 | Session session = sessionBuilder.build(); | |
18 | ``` | |
19 | | |
20 | Given the properties set on the session builder through the `copyWith` method, this returns a Serverpod `Session` that has the corresponding state. | |
21 | | |
22 | ### Setting authenticated state | |
23 | | |
24 | To control the authenticated state of the session, the `AuthenticationOverride` class can be used. | |
25 | | |
26 | To create an unauthenticated override (this is the default value for new sessions), call `AuthenticationOverride unauthenticated()`: | |
27 | | |
28 | ```dart | |
29 | static AuthenticationOverride unauthenticated(); | |
30 | ``` | |
31 | | |
32 | To create an authenticated override, call `AuthenticationOverride.authenticationInfo(...)`: | |
33 | | |
34 | ```dart | |
35 | static AuthenticationOverride authenticationInfo( | |
36 | int userId, | |
37 | Set<Scope> scopes, { | |
38 | String? authId, | |
39 | }) | |
40 | ``` | |
41 | | |
42 | Pass these to `sessionBuilder.copyWith` to simulate different scenarios. Below follows an example for each case: | |
43 | | |
44 | ```dart | |
45 | withServerpod('Given AuthenticatedExample endpoint', (sessionBuilder, endpoints) { | |
46 | // Corresponds to an actual user id | |
47 | const int userId = 1234; | |
48 | | |
49 | group('when authenticated', () { | |
50 | var authenticatedSessionBuilder = sessionBuilder.copyWith( | |
51 | authentication: | |
52 | AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), | |
53 | ); | |
54 | | |
55 | test('then calling `hello` should return greeting', () async { | |
56 | final greeting = await endpoints.authenticatedExample | |
57 | .hello(authenticatedSessionBuilder, 'Michael'); | |
58 | expect(greeting, 'Hello, Michael!'); | |
59 | }); | |
60 | }); | |
61 | | |
62 | group('when unauthenticated', () { | |
63 | var unauthenticatedSessionBuilder = sessionBuilder.copyWith( | |
64 | authentication: AuthenticationOverride.unauthenticated(), | |
65 | ); | |
66 | | |
67 | test( | |
68 | 'then calling `hello` should throw `ServerpodUnauthenticatedException`', | |
69 | () async { | |
70 | final future = endpoints.authenticatedExample | |
71 | .hello(unauthenticatedSessionBuilder, 'Michael'); | |
72 | await expectLater( | |
73 | future, throwsA(isA<ServerpodUnauthenticatedException>())); | |
74 | }); | |
75 | }); | |
76 | }); | |
77 | ``` | |
78 | | |
79 | ### Seeding the database | |
80 | | |
81 | To seed the database before tests, `build` a `session` and pass it to the database call just as in production code. | |
82 | | |
83 | :::info | |
84 | | |
85 | 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. | |
86 | | |
87 | ::: | |
88 | | |
89 | ```dart | |
90 | withServerpod('Given Products endpoint', (sessionBuilder, endpoints) { | |
91 | var session = sessionBuilder.build(); | |
92 | | |
93 | setUp(() async { | |
94 | await Product.db.insert(session, [ | |
95 | Product(name: 'Apple', price: 10), | |
96 | Product(name: 'Banana', price: 10) | |
97 | ]); | |
98 | }); | |
99 | | |
100 | test('then calling `all` should return all products', () async { | |
101 | final products = await endpoints.products.all(sessionBuilder); | |
102 | expect(products, hasLength(2)); | |
103 | expect(products.map((p) => p.name), contains(['Apple', 'Banana'])); | |
104 | }); | |
105 | }); | |
106 | ``` | |
107 | | |
108 | ## Environment | |
109 | | |
110 | By default `withServerpod` uses the `test` run mode and the database settings will be read from `config/test.yaml`. | |
111 | | |
112 | It is possible to override the default run mode by setting the `runMode` setting: | |
113 | | |
114 | ```dart | |
115 | withServerpod( | |
116 | 'Given Products endpoint', | |
117 | (sessionBuilder, endpoints) { | |
118 | /* test code */ | |
119 | }, | |
120 | runMode: ServerpodRunMode.development, | |
121 | ); | |
122 | ``` | |
123 | | |
124 | ## Configuration | |
125 | | |
126 | The following optional configuration options are available to pass as a second argument to `withServerpod`: | |
127 | | |
128 | |Property|Description|Default| | |
129 | |:-----|:-----|:---:| | |
130 | |`applyMigrations`|Whether pending migrations should be applied when starting Serverpod.|`true`| | |
131 | |`enableSessionLogging`|Whether session logging should be enabled.|`false`| | |
132 | |`rollbackDatabase`|Options for when to rollback the database during the test lifecycle (or disable it). See detailed description [here](#rollback-database-configuration).|`RollbackDatabase.afterEach`| | |
133 | |`runMode`|The run mode that Serverpod should be running in.|`ServerpodRunmode.test`| | |
134 | |`serverpodLoggingMode`|The logging mode used when creating Serverpod.|`ServerpodLoggingMode.normal`| | |
135 | |`serverpodStartTimeout`|The timeout to use when starting Serverpod, which connects to the database among other things. Defaults to `Duration(seconds: 30)`.|`Duration(seconds: 30)`| | |
136 | |`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']`| | |
137 | | |
138 | ### `rollbackDatabase` {#rollback-database-configuration} | |
139 | | |
140 | 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: | |
141 | | |
142 | ```dart | |
143 | /// Options for when to rollback the database during the test lifecycle. | |
144 | enum RollbackDatabase { | |
145 | /// After each test. This is the default. | |
146 | afterEach, | |
147 | | |
148 | /// After all tests. | |
149 | afterAll, | |
150 | | |
151 | /// Disable rolling back the database. | |
152 | disabled, | |
153 | } | |
154 | ``` | |
155 | | |
156 | There are a few reasons to change the default setting: | |
157 | | |
158 | 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. | |
159 | | |
160 | 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`. | |
161 | | |
162 | ```dart | |
163 | Future<void> concurrentTransactionCalls( | |
164 | Session session, | |
165 | ) async { | |
166 | await Future.wait([ | |
167 | session.db.transaction((tx) => /*...*/), | |
168 | // Will throw `InvalidConfigurationException` if `rollbackDatabase` | |
169 | // is not set to `RollbackDatabase.disabled` in `withServerpod` | |
170 | session.db.transaction((tx) => /*...*/), | |
171 | ]); | |
172 | } | |
173 | ``` | |
174 | | |
175 | When setting `rollbackDatabase.disabled` to be able to test `concurrentTransactionCalls`, remember that the database has to be manually cleaned up to not leak data: | |
176 | | |
177 | ```dart | |
178 | withServerpod( | |
179 | 'Given ProductsEndpoint when calling concurrentTransactionCalls', | |
180 | (sessionBuilder, endpoints) { | |
181 | tearDownAll(() async { | |
182 | var session = sessionBuilder.build(); | |
183 | // If something was saved to the database in the endpoint, | |
184 | // for example a `Product`, then it has to be cleaned up! | |
185 | await Product.db.deleteWhere( | |
186 | session, | |
187 | where: (_) => Constant.bool(true), | |
188 | ); | |
189 | }); | |
190 | | |
191 | test('then should execute and commit all transactions', () async { | |
192 | var result = | |
193 | await endpoints.products.concurrentTransactionCalls(sessionBuilder); | |
194 | // ... | |
195 | }); | |
196 | }, | |
197 | rollbackDatabase: RollbackDatabase.disabled, | |
198 | ); | |
199 | ``` | |
200 | | |
201 | 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: | |
202 | | |
203 | ```bash | |
204 | dart test -t integration --concurrency=1 | |
205 | ``` | |
206 | | |
207 | For the other cases this is not an issue, as each `withServerpod` has its own transaction and will therefore be isolated. | |
208 | | |
209 | 3. **Database exceptions that are quelled**: There is a specific edge case where the test tools behavior deviates from production behavior. See example below: | |
210 | | |
211 | ```dart | |
212 | var transactionFuture = session.db.transaction((tx) async { | |
213 | var data = UniqueData(number: 1, email: 'test@test.com'); | |
214 | try { | |
215 | await UniqueData.db.insertRow(session, data, transaction: tx); | |
216 | await UniqueData.db.insertRow(session, data, transaction: tx); | |
217 | } on DatabaseException catch (_) { | |
218 | // Ignore the database exception | |
219 | } | |
220 | }); | |
221 | | |
222 | // ATTENTION: This will throw an exception in production | |
223 | // but not in the test tools. | |
224 | await transactionFuture; | |
225 | ``` | |
226 | | |
227 | 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. | |
228 | | |
229 | ## Test exceptions | |
230 | | |
231 | The following exceptions are exported from the generated test tools file and can be thrown by the test tools in various scenarios, see below. | |
232 | | |
233 | |Exception|Description| | |
234 | |:-----|:-----| | |
235 | |`ServerpodUnauthenticatedException`|Thrown during an endpoint method call when the user was not authenticated.| | |
236 | |`ServerpodInsufficientAccessException`|Thrown during an endpoint method call when the authentication key provided did not have sufficient access.| | |
237 | |`ConnectionClosedException`|Thrown during an endpoint method call if a stream connection was closed with an error. For example, if the user authentication was revoked.| | |
238 | |`InvalidConfigurationException`|Thrown when an invalid configuration state is found.| | |
239 | | |
240 | ## Test helpers | |
241 | | |
242 | ### `flushEventQueue` | |
243 | | |
244 | Test helper to flush the event queue. | |
245 | Useful for waiting for async events to complete before continuing the test. | |
246 | | |
247 | ```dart | |
248 | Future<void> flushEventQueue(); | |
249 | ``` | |
250 | | |
251 | For example, if depending on a generator function to execute up to its `yield`, then the | |
252 | event queue can be flushed to ensure the generator has executed up to that point: | |
253 | | |
254 | ```dart | |
255 | var stream = endpoints.someEndoint.generatorFunction(session); | |
256 | await flushEventQueue(); | |
257 | ``` | |
258 | | |
259 | See also [this complete example](advanced-examples#multiple-users-interacting-with-a-shared-stream). | |
260 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/19-testing/03-advanced-examples.md: | |
-------------------------------------------------------------------------------- | |
1 | # Advanced examples | |
2 | | |
3 | ## Run unit and integration tests separately | |
4 | | |
5 | To run unit and integration tests separately, the `"integration"` tag can be used as a filter. See the following examples: | |
6 | | |
7 | ```bash | |
8 | # All tests (unit and integration) | |
9 | dart test | |
10 | | |
11 | # Only integration tests: add --tags (-t) flag | |
12 | dart test -t integration | |
13 | | |
14 | # Only unit tests: add --exclude-tags (-x) flag | |
15 | dart test -x integration | |
16 | ``` | |
17 | | |
18 | To change the name of this tag, see the [`testGroupTagsOverride`](the-basics#configuration) configuration option. | |
19 | | |
20 | ## Test business logic that depends on `Session` | |
21 | | |
22 | 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: | |
23 | | |
24 | ```dart | |
25 | withServerpod('Given decreasing product quantity when quantity is zero', ( | |
26 | sessionBuilder, | |
27 | _, | |
28 | ) { | |
29 | var session = sessionBuilder.build(); | |
30 | | |
31 | setUp(() async { | |
32 | await Product.db.insertRow(session, [ | |
33 | Product( | |
34 | id: 123, | |
35 | name: 'Apple', | |
36 | quantity: 0, | |
37 | ), | |
38 | ]); | |
39 | }); | |
40 | | |
41 | test('then should throw `InvalidOperationException`', | |
42 | () async { | |
43 | var future = ProductsBusinessLogic.updateQuantity( | |
44 | session, | |
45 | id: 123, | |
46 | decrease: 1, | |
47 | ); | |
48 | | |
49 | await expectLater(future, throwsA(isA<InvalidOperationException>())); | |
50 | }); | |
51 | }); | |
52 | ``` | |
53 | | |
54 | ## Multiple users interacting with a shared stream | |
55 | | |
56 | 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. | |
57 | | |
58 | Given the following simplified endpoint: | |
59 | | |
60 | ```dart | |
61 | class CommunicationExampleEndpoint { | |
62 | static const sharedStreamName = 'shared-stream'; | |
63 | Future<void> postNumberToSharedStream(Session session, int number) async { | |
64 | await session.messages | |
65 | .postMessage(sharedStreamName, SimpleData(num: number)); | |
66 | } | |
67 | | |
68 | Stream<int> listenForNumbersOnSharedStream(Session session) async* { | |
69 | var sharedStream = | |
70 | session.messages.createStream<SimpleData>(sharedStreamName); | |
71 | | |
72 | await for (var message in sharedStream) { | |
73 | yield message.num; | |
74 | } | |
75 | } | |
76 | } | |
77 | ``` | |
78 | | |
79 | 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. | |
80 | | |
81 | ```dart | |
82 | withServerpod('Given CommunicationExampleEndpoint', (sessionBuilder, endpoints) { | |
83 | const int userId1 = 1; | |
84 | const int userId2 = 2; | |
85 | | |
86 | test( | |
87 | 'when calling postNumberToSharedStream and listenForNumbersOnSharedStream ' | |
88 | 'with different sessions then number should be echoed', | |
89 | () async { | |
90 | var userSession1 = sessionBuilder.copyWith( | |
91 | authentication: AuthenticationOverride.authenticationInfo( | |
92 | userId1, | |
93 | {}, | |
94 | ), | |
95 | ); | |
96 | var userSession2 = sessionBuilder.copyWith( | |
97 | authentication: AuthenticationOverride.authenticationInfo( | |
98 | userId2, | |
99 | {}, | |
100 | ), | |
101 | ); | |
102 | | |
103 | var stream = | |
104 | endpoints.testTools.listenForNumbersOnSharedStream(userSession1); | |
105 | // Wait for `listenForNumbersOnSharedStream` to execute up to its | |
106 | // `yield` statement before continuing | |
107 | await flushEventQueue(); | |
108 | | |
109 | await endpoints.testTools.postNumberToSharedStream(userSession2, 111); | |
110 | await endpoints.testTools.postNumberToSharedStream(userSession2, 222); | |
111 | | |
112 | await expectLater(stream.take(2), emitsInOrder([111, 222])); | |
113 | }); | |
114 | }); | |
115 | ``` | |
116 | | |
117 | ## Optimising number of database connections | |
118 | | |
119 | 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. | |
120 | | |
121 | 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. | |
122 | | |
123 | 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. | |
124 | | |
125 | If the max connection limit is hit, there are two options: | |
126 | | |
127 | - Raise the max connections limit on the database. | |
128 | - Build out the session in `setUp`/`setUpAll` instead of the top level scope: | |
129 | | |
130 | ```dart | |
131 | withServerpod('Given example test', (sessionBuilder, endpoints) { | |
132 | // Instead of this | |
133 | var session = sessionBuilder.build(); | |
134 | | |
135 | | |
136 | // Do this to postpone connecting to the database until the test group is running | |
137 | late Session session; | |
138 | setUpAll(() { | |
139 | session = sessionBuilder.build(); | |
140 | }); | |
141 | // ... | |
142 | }); | |
143 | ``` | |
144 | | |
145 | :::info | |
146 | | |
147 | 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. | |
148 | | |
149 | ::: | |
150 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/19-testing/04-best-practises.md: | |
-------------------------------------------------------------------------------- | |
1 | --- | |
2 | # Don't display do's and don'ts in the table of contents | |
3 | toc_max_heading_level: 2 | |
4 | --- | |
5 | | |
6 | # Best practises | |
7 | | |
8 | ## Imports | |
9 | | |
10 | 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. | |
11 | | |
12 | ### Don't | |
13 | | |
14 | ```dart | |
15 | import 'serverpod_test_tools.dart'; | |
16 | // Don't import `serverpod_test` directly. | |
17 | import 'package:serverpod_test/serverpod_test.dart'; ❌ | |
18 | ``` | |
19 | | |
20 | ### Do | |
21 | | |
22 | ```dart | |
23 | // Only import the generated test tools file. | |
24 | // It re-exports all helpers and types that are needed. | |
25 | import 'serverpod_test_tools.dart'; ✅ | |
26 | ``` | |
27 | | |
28 | ### Database clean up | |
29 | | |
30 | 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). | |
31 | | |
32 | ### Don't | |
33 | | |
34 | ```dart | |
35 | withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { | |
36 | var session = sessionBuilder.build(); | |
37 | | |
38 | setUp(() async { | |
39 | await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); | |
40 | }); | |
41 | | |
42 | tearDown(() async { | |
43 | await Product.db.deleteWhere( ❌ // Unnecessary clean up | |
44 | session, | |
45 | where: (_) => Constant.bool(true), | |
46 | ); | |
47 | }); | |
48 | | |
49 | // ... | |
50 | }); | |
51 | ``` | |
52 | | |
53 | ### Do | |
54 | | |
55 | ```dart | |
56 | withServerpod('Given ProductsEndpoint', (sessionBuilder, endpoints) { | |
57 | var session = sessionBuilder.build(); | |
58 | | |
59 | setUp(() async { | |
60 | await Product.db.insertRow(session, Product(name: 'Apple', price: 10)); | |
61 | }); | |
62 | | |
63 | ✅ // Clean up can be omitted since the transaction is rolled back after each by default | |
64 | | |
65 | // ... | |
66 | }); | |
67 | ``` | |
68 | | |
69 | ## Calling endpoints | |
70 | | |
71 | 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. | |
72 | | |
73 | ### Don't | |
74 | | |
75 | ```dart | |
76 | void main() { | |
77 | // ❌ Don't instantiate endpoints directly | |
78 | var exampleEndpoint = ExampleEndpoint(); | |
79 | | |
80 | withServerpod('Given Example endpoint', ( | |
81 | sessionBuilder, | |
82 | _ /* not using the provided endpoints */, | |
83 | ) { | |
84 | var session = sessionBuilder.build(); | |
85 | | |
86 | test('when calling `hello` then should return greeting', () async { | |
87 | // ❌ Don't call and endpoint method directly on the endpoint class. | |
88 | final greeting = await exampleEndpoint.hello(session, 'Michael'); | |
89 | expect(greeting, 'Hello, Michael!'); | |
90 | }); | |
91 | }); | |
92 | } | |
93 | ``` | |
94 | | |
95 | ### Do | |
96 | | |
97 | ```dart | |
98 | void main() { | |
99 | withServerpod('Given Example endpoint', (sessionBuilder, endpoints) { | |
100 | var session = sessionBuilder.build(); | |
101 | | |
102 | test('when calling `hello` then should return greeting', () async { | |
103 | // ✅ Use the provided `endpoints` to call the endpoint that should be tested. | |
104 | final greeting = | |
105 | await endpoints.example.hello(session, 'Michael'); | |
106 | expect(greeting, 'Hello, Michael!'); | |
107 | }); | |
108 | }); | |
109 | } | |
110 | ``` | |
111 | | |
112 | ## Unit and integration tests | |
113 | | |
114 | It is significantly easier to navigate a project if the different types of tests are clearly separated. | |
115 | | |
116 | ### Don't | |
117 | | |
118 | ❌ Mix different types of tests together. | |
119 | | |
120 | ### Do | |
121 | | |
122 | ✅ Have a clear structure for the different types of test. Serverpod recommends the following two folders in the `server`: | |
123 | | |
124 | - `test/unit`: Unit tests. | |
125 | - `test/integration`: Tests for endpoints or business logic modules using the `withServerpod` helper. | |
126 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/20-security-configuration.md: | |
-------------------------------------------------------------------------------- | |
1 | # Security Configuration | |
2 | | |
3 | :::info | |
4 | | |
5 | In a **production environment**, TLS termination is **normally handled by a load balancer** or **reverse proxy** (e.g., Nginx, AWS ALB, or Cloudflare). | |
6 | However, Serverpod also supports setting up **TLS/SSL directly on the server**, allowing you to provide your own certificates if needed. | |
7 | | |
8 | ::: | |
9 | | |
10 | Serverpod supports **TLS/SSL security configurations** through the **Dart configuration object**. | |
11 | To enable SSL/TLS, you must pass a **`SecurityContextConfig`** to the `Serverpod` constructor. | |
12 | | |
13 | ## Server Security Configuration | |
14 | | |
15 | To enable SSL/TLS in Serverpod, configure the `SecurityContextConfig` and pass it to the `Serverpod` instance. | |
16 | | |
17 | ### Dart Configuration Example | |
18 | | |
19 | ```dart | |
20 | final securityContext = SecurityContext() | |
21 | ..useCertificateChain('path/to/server_cert.pem') | |
22 | ..usePrivateKey('path/to/server_key.pem', password: 'password'); | |
23 | | |
24 | Serverpod( | |
25 | args, | |
26 | Protocol(), | |
27 | Endpoints(), | |
28 | securityContextConfig: SecurityContextConfig( | |
29 | apiServer: securityContext, | |
30 | webServer: securityContext, | |
31 | insightsServer: securityContext, | |
32 | ), | |
33 | ); | |
34 | ``` | |
35 | | |
36 | ## Client Security Configuration | |
37 | | |
38 | When connecting to a **Serverpod server over HTTPS**, the client must be configured to trust the server's certificate. | |
39 | | |
40 | ### Dart Configuration Example | |
41 | | |
42 | To enable SSL/TLS when using the Serverpod client, pass a **`SecurityContext`** to the `Client` constructor. | |
43 | | |
44 | ```dart | |
45 | final securityContext = SecurityContext() | |
46 | ..setTrustedCertificates('path/to/server_cert.pem'); | |
47 | | |
48 | | |
49 | final client = Client( | |
50 | 'https://yourserver.com', | |
51 | securityContext: securityContext, | |
52 | ... | |
53 | ); | |
54 | ``` | |
55 | | |
-------------------------------------------------------------------------------- | |
/versioned_docs/version-2.6.0/06-concepts/21-experimental.md: | |
-------------------------------------------------------------------------------- | |
1 | # Experimental features | |
2 | | |
3 | :::warning | |
4 | Be cautious when using experimental features in production environments, as their stability is uncertain and they may receive breaking changes in upcoming releases. | |
5 | ::: | |
6 | | |
7 | "Experimental Features" are cutting-edge additions to Serverpod that are currently under development or testing or whose API is not yet stable. | |
8 | These features allow developers to explore new functionalities and provide feedback, helping shape the future of Serverpod. | |
9 | However, they may not be fully stable or complete and are subject to change. | |
10 | | |
11 | Experimental features are disabled by default, i.e. they are not active unless the developer opts-in. | |
12 | | |
13 | ## Experimental internal APIs | |
14 | | |
15 | Experimental internal APIs are placed under the `experimental` sub-API of the `Serverpod` class. | |
16 | When an experimental feature matures it is moved from `experimental` to `Serverpod` proper. | |
17 | If possible, the experimental API will remain for some time as `@deprecated`, and then removed. | |
18 | | |
19 | ## Command-line enabled features | |
20 | | |
21 | Some of the experimental features are enabled by including the `--experimental-features` flag when running the serverpod command: | |
22 | | |
23 | ```bash | |
24 | $ serverpod generate --experimental-features=all | |
25 | ``` | |
26 | | |
27 | The current options you can pass are: | |
28 | | |
29 | | **Feature** | Description | | |
30 | | :--------------- | :----------------------------------------------------------------------------------------- | | |
31 | | **all** | Enables all available experimental features. | | |
32 | | **inheritance** | Allows using the `extends` keyword in your model files to create class hierarchies. | | |
33 | | **changeIdType** | Allows declaring the `id` field in table model files to change the type of the `id` field. | | |
34 | | |
35 | ## Inheritance | |
36 | | |
37 | :::warning | |
38 | 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. | |
39 | ::: | |
40 | | |
41 | 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. | |
42 | | |
43 | ### Extending a Class | |
44 | | |
45 | To inherit from a class, use the `extends` keyword in your model files, as shown below: | |
46 | | |
47 | ```yaml | |
48 | class: ParentClass | |
49 | fields: | |
50 | name: String | |
51 | ``` | |
52 | | |
53 | ```yaml | |
54 | class: ChildClass | |
55 | extends: ParentClass | |
56 | fields: | |
57 | int: age | |
58 | ``` | |
59 | | |
60 | This will generate a class with both `name` and `age` field. | |
61 | | |
62 | ```dart | |
63 | class ChildClass extends ParentClass { | |
64 | String name | |
65 | int age | |
66 | } | |
67 | ``` | |
68 | | |
69 | ### Sealed Classes | |
70 | | |
71 | 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. | |
72 | | |
73 | ```yaml | |
74 | class: ParentClass | |
75 | sealed: true | |
76 | fields: | |
77 | name: String | |
78 | ``` | |
79 | | |
80 | ```yaml | |
81 | class: ChildClass | |
82 | extends: ParentClass | |
83 | fields: | |
84 | age: int | |
85 | ``` | |
86 | | |
87 | This will generate the following classes: | |
88 | | |
89 | ```dart | |
90 | sealed class ParentClass { | |
91 | String name; | |
92 | } | |
93 | | |
94 | class ChildClass extends ParentClass { | |
95 | String name; | |
96 | int age; | |
97 | } | |
98 | ``` | |
99 | | |
100 | :::info | |
101 | All files in a sealed hierarchy need to be located in the same directory. | |
102 | ::: | |
103 | | |
104 | ## Change ID type | |
105 | | |
106 | Changing the type of the `id` field allows you to customize the identifier type for your database tables. This is done by declaring the `id` field on table models with one of the supported types. If the field is omitted, the id field will still be created with type `int`, as have always been. | |
107 | | |
108 | The following types are supported for the `id` field: | |
109 | | |
110 | | **Type** | Default | Default Persist options | Default Model options | Description | | |
111 | | :------------ | :------ | :---------------------- | :-------------------- | :--------------------- | | |
112 | | **int** | serial | serial (optional) | - | 64-bit serial integer. | | |
113 | | **UuidValue** | random | random | random | UUID v4 value. | | |
114 | | |
115 | ### Declaring a Custom ID Type | |
116 | | |
117 | To declare a custom type for the `id` field in a table model file, use the following syntax: | |
118 | | |
119 | ```yaml | |
120 | class: UuidIdTable | |
121 | table: uuid_id_table | |
122 | fields: | |
123 | id: UuidValue?, defaultPersist=random | |
124 | ``` | |
125 | | |
126 | ```yaml | |
127 | class: IntIdTable | |
128 | table: int_id_table | |
129 | fields: | |
130 | id: int?, defaultPersist=serial // The default keyword for 'int' is optional. | |
131 | ``` | |
132 | | |
133 | #### Default Uuid model value | |
134 | | |
135 | For UUIDs, it is possible to configure the `defaultModel` value. This will ensure that UUIDs are generated as soon as the object is created, rather than when it is persisted to the database. This is useful for creating objects offline or using them before they are sent to the server. | |
136 | | |
137 | ```yaml | |
138 | class: UuidIdTable | |
139 | table: uuid_id_table | |
140 | fields: | |
141 | id: UuidValue, defaultModel=random | |
142 | ``` | |
143 | | |
144 | When using `defaultModel=random`, the UUID will be generated when the object is created. Since an id is always assigned the `id` field can be non-nullable. | |
145 | | |
146 | ## Exception monitoring | |
147 | | |
148 | Serverpod allows you to monitor exceptions in a central and flexible way by using the new diagnostic event handlers. | |
149 | These work both for exceptions thrown in application code and from the framework (e.g. server startup or shutdown errors). | |
150 | | |
151 | This can be used to get all exceptions reported in realtime to services for monitoring and diagnostics, | |
152 | such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), and [Datadog](https://www.datadoghq.com/). | |
153 | | |
154 | It is easy to implement handlers and define custom filters within them. | |
155 | Any number of handlers can be added. | |
156 | They are run asynchronously and should not affect the behavior or response times of the server. | |
157 | | |
158 | These event handlers are for diagnostics only, | |
159 | they do not allow any behavior-changing action such as suppressing exceptions or converting them to another exception type. | |
160 | | |
161 | ### Setup | |
162 | | |
163 | This feature is enabled by providing one ore more `DiagnosticEventHandler` implementations | |
164 | to the Serverpod constructor's `experimentalFeatures` specification. | |
165 | | |
166 | Example: | |
167 | | |
168 | ```dart | |
169 | var serverpod = Serverpod( | |
170 | ... | |
171 | experimentalFeatures: ExperimentalFeatures( | |
172 | diagnosticEventHandlers: [ | |
173 | AsEventHandler((event, {required space, required context}) { | |
174 | print('$event Origin is $space\n Context is ${context.toJson()}'); | |
175 | }), | |
176 | ], | |
177 | ), | |
178 | ); | |
179 | ``` | |
180 | | |
181 | ### Submitting diagnostic events | |
182 | | |
183 | The API for submitting diagnostic events from user code, e.g. from endpoint methods, web calls, and future calls, | |
184 | is the new method `submitDiagnosticEvent` under the `experimental` member of the Serverpod class. | |
185 | | |
186 | ```dart | |
187 | void submitDiagnosticEvent( | |
188 | DiagnosticEvent event, { | |
189 | required Session session, | |
190 | }) | |
191 | ``` | |
192 | | |
193 | Usage example: | |
194 | | |
195 | ```dart | |
196 | class DiagnosticEventTestEndpoint extends Endpoint { | |
197 | Future<String> submitExceptionEvent(Session session) async { | |
198 | try { | |
199 | throw Exception('An exception is thrown'); | |
200 | } catch (e, stackTrace) { | |
201 | session.serverpod.experimental.submitDiagnosticEvent( | |
202 | ExceptionEvent(e, stackTrace), | |
203 | session: session, | |
204 | ); | |
205 | } | |
206 | return 'success'; | |
207 | } | |
208 | } | |
209 | ``` | |
210 | | |
211 | ### Guidelines for handlers | |
212 | | |
213 | A `DiagnosticEvent` represents an event that occurs in the server. | |
214 | `DiagnosticEventHandler` implementations can react to these events | |
215 | in order to gain insights into the behavior of the server. | |
216 | | |
217 | As the name suggests the handlers should perform diagnostics only, | |
218 | and not have any responsibilities that the regular functioning | |
219 | of the server depends on. | |
220 | | |
221 | The registered handlers are typically run concurrently, | |
222 | can not depend on each other, and asynchronously - | |
223 | they are not awaited by the operation they are triggered from. | |
224 | | |
225 | If a handler throws an exception it will be logged to stderr | |
226 | and otherwise ignored. | |
227 | | |
228 | ### Test support | |
229 | | |
230 | This feature also includes support via the Serverpod test framework. | |
231 | This means that the `withServerpod` construct can be used together with diagnostic event handlers to test that the events are submitted and propagated as intended. | |
232 | | |
233 | Example: | |
234 | | |
235 | ```dart | |
236 | void main() { | |
237 | var exceptionHandler = TestExceptionHandler(); | |
238 | | |
239 | withServerpod('Given withServerpod with a diagnostic event handler', | |
240 | experimentalFeatures: ExperimentalFeatures( | |
241 | diagnosticEventHandlers: [exceptionHandler], | |
242 | ), (sessionBuilder, endpoints) { | |
243 | test( | |
244 | 'when calling an endpoint method that submits an exception event ' | |
245 | 'then the diagnostic event handler gets called', () async { | |
246 | final result = await endpoints.diagnosticEventTest | |
247 | .submitExceptionEvent(sessionBuilder); | |
248 | expect(result, 'success'); | |
249 | | |
250 | final record = await exceptionHandler.events.first.timeout(Duration(seconds: 1)); | |
251 | expect(record.event.exception, isA<Exception>()); | |
252 | expect(record.space, equals(OriginSpace.application)); | |
253 | expect(record.context, isA<DiagnosticEventContext>()); | |
254 | expect( | |
255 | record.context.toJson(), | |
256 | allOf([ | |
257 | containsPair('serverId', 'default'), | |
258 | containsPair('serverRunMode', 'test'), | |
259 | containsPair('serverName', 'Server default'), | |
260 | ])); | |
261 | }); | |
262 | }); | |
263 | } | |
264 | ``` | |
265 | | |
-------------------------------------------------------------------------------- | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment