From cb92ade043c5c0dcc654b8e130b21f3de56ab683 Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Thu, 16 Jul 2026 00:37:43 -0300 Subject: [PATCH 1/4] docs: Document database interceptors --- .../02-database/12-raw-access.md | 2 + .../02-database/17-database-interceptors.md | 89 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md diff --git a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md index 93392bb7..0a47aa52 100644 --- a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md +++ b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md @@ -81,3 +81,5 @@ var result = await db.unsafeQuery( :::danger Always sanitize your input when using raw query methods. For the `unsafeQuery` and `unsafeExecute` methods, use query parameters to prevent SQL injection. Avoid using `unsafeSimpleQuery` and `unsafeSimpleExecute` unless the simple query protocol is strictly required. ::: + +For cross-cutting tracing, metrics, tenant scoping, or policy enforcement across database operations, see [Intercept database access](./database-interceptors). diff --git a/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md b/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md new file mode 100644 index 00000000..d1f243a8 --- /dev/null +++ b/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md @@ -0,0 +1,89 @@ +--- +description: Replace Session.db once per session with a custom database layer for tracing, policy enforcement, tenant scoping, metrics, or tests. +--- + +# Intercept database access + +Use a database interceptor when every database operation in a session needs the same cross-cutting behavior. The interceptor receives the new `Session` and Serverpod's framework-provided `Database`, then returns the database instance exposed as `session.db`. + +Configure it when you construct `Serverpod`: + +```dart +var databaseSessionCount = 0; + +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + databaseInterceptor: (session, inner) { + databaseSessionCount++; + session.log('Created a database layer for this session.'); + return inner; + }, +); +``` + +This example observes database session creation and keeps the framework database unchanged. Serverpod calls the interceptor once when it creates each session. Repeated reads of `session.db` return the same database instance for that session. + +## Return a custom database layer + +Return an application-defined `Database` implementation to intercept its operations: + +```dart +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + databaseInterceptor: (session, inner) { + return PolicyDatabase( + session: session, + inner: inner, + ); + }, +); +``` + +In this example, `PolicyDatabase` is your implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`. `Database` does not expose a public constructor for subclassing, so implement its interface and generate the required overrides with your IDE. Keep the supplied `inner` database and delegate every operation that your layer does not change. The returned object becomes `Session.db`, so generated calls such as `User.db.find(session)` also pass through it. + +Common uses include: + +- Recording query traces, timings, and metrics. +- Applying tenant scope to every supported query. +- Enforcing read and write policies. +- Blocking unsafe operations or writes in protected environments. +- Recording or replacing database behavior in tests. + +Forward transaction objects, runtime parameters, ordering, pagination, row locks, and return options without changing their meaning. A policy layer should also account for raw database methods and transactions so callers cannot bypass it through another `Database` method. + +Do not retain the session or its inner database after the session closes. + +:::warning + +Custom database layers depend on the `Database` API from `serverpod_database`. This API may receive breaking changes in a minor Serverpod release. Review and test the complete implementation whenever you upgrade Serverpod. + +::: + +## Use an interceptor in integration tests + +Generated `withServerpod` test helpers accept the same `databaseInterceptor` callback. This lets a test count session creation or return a recording database layer without changing server startup code: + +```dart +var interceptedSessions = 0; + +withServerpod( + 'database policy', + databaseInterceptor: (session, inner) { + interceptedSessions++; + return inner; + }, + (sessionBuilder, _) { + test('creates the database layer for a test session', () { + var beforeBuild = interceptedSessions; + sessionBuilder.build(); + expect(interceptedSessions, beforeBuild + 1); + }); + }, +); +``` + +The callback is also invoked once for every test session created by the helper. From 68fe606e1df4c1119e25c3fbe87cc9056dc7455c Mon Sep 17 00:00:00 2001 From: Marcelo Soares Date: Tue, 21 Jul 2026 15:36:47 -0300 Subject: [PATCH 2/4] docs: Rework the database interceptor page for the concepts section Give the page a noun-shaped title like its sibling database pages, drop the counter from the introductory example, link the related database pages instead of listing concepts in prose, correct the argument order in the withServerpod example, and add databaseInterceptor to the test helper configuration table. --- .../02-database/12-raw-access.md | 2 +- .../02-database/17-database-interceptors.md | 54 ++++++++----------- docs/06-concepts/08-testing/02-the-basics.md | 1 + 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md index 0a47aa52..b45c0cdd 100644 --- a/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md +++ b/docs/06-concepts/03-data-and-the-database/02-database/12-raw-access.md @@ -82,4 +82,4 @@ var result = await db.unsafeQuery( 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. ::: -For cross-cutting tracing, metrics, tenant scoping, or policy enforcement across database operations, see [Intercept database access](./database-interceptors). +To apply tracing, metrics, tenant scoping, or policy enforcement across every database operation of a session, including the raw methods above, see [Database interceptors](./database-interceptors). diff --git a/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md b/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md index d1f243a8..ca8d2473 100644 --- a/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md +++ b/docs/06-concepts/03-data-and-the-database/02-database/17-database-interceptors.md @@ -1,33 +1,30 @@ --- -description: Replace Session.db once per session with a custom database layer for tracing, policy enforcement, tenant scoping, metrics, or tests. +description: A database interceptor replaces Session.db once per session with a custom database layer for tracing, policy enforcement, tenant scoping, and tests. --- -# Intercept database access +# Database interceptors -Use a database interceptor when every database operation in a session needs the same cross-cutting behavior. The interceptor receives the new `Session` and Serverpod's framework-provided `Database`, then returns the database instance exposed as `session.db`. +A database interceptor lets you wrap every database operation of a session in the same behavior, such as query tracing, tenant scoping, or a guard that rejects writes in a protected environment. Serverpod calls the interceptor once when it creates a session, passing the session and the framework database, and whatever the interceptor returns becomes `session.db` for the rest of that session. -Configure it when you construct `Serverpod`: +Register the interceptor when you construct `Serverpod`: ```dart -var databaseSessionCount = 0; - var pod = Serverpod( args, Protocol(), Endpoints(), databaseInterceptor: (session, inner) { - databaseSessionCount++; session.log('Created a database layer for this session.'); return inner; }, ); ``` -This example observes database session creation and keeps the framework database unchanged. Serverpod calls the interceptor once when it creates each session. Repeated reads of `session.db` return the same database instance for that session. +Returning `inner` leaves the framework database in place, so this example only observes session creation. Repeated reads of `session.db` return the same instance for the lifetime of the session. ## Return a custom database layer -Return an application-defined `Database` implementation to intercept its operations: +To intercept the operations themselves, return your own implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`: ```dart var pod = Serverpod( @@ -43,47 +40,38 @@ var pod = Serverpod( ); ``` -In this example, `PolicyDatabase` is your implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`. `Database` does not expose a public constructor for subclassing, so implement its interface and generate the required overrides with your IDE. Keep the supplied `inner` database and delegate every operation that your layer does not change. The returned object becomes `Session.db`, so generated calls such as `User.db.find(session)` also pass through it. +The `Database` class has no public constructor to subclass, so implement the interface and let your IDE generate the overrides. Hold on to the supplied `inner` database and delegate every operation the layer does not change. The object you return becomes `Session.db`, so generated calls such as `User.db.find(session)` pass through it as well. -Common uses include: +Typical uses are: - Recording query traces, timings, and metrics. -- Applying tenant scope to every supported query. +- Applying a tenant scope to every supported query. - Enforcing read and write policies. - Blocking unsafe operations or writes in protected environments. - Recording or replacing database behavior in tests. -Forward transaction objects, runtime parameters, ordering, pagination, row locks, and return options without changing their meaning. A policy layer should also account for raw database methods and transactions so callers cannot bypass it through another `Database` method. - -Do not retain the session or its inner database after the session closes. +Pass transaction objects, [runtime parameters](./runtime-parameters), ordering, pagination, [row locks](./row-locking), and return options through without changing their meaning. A layer that enforces a policy also needs to cover [raw access](./raw-access) and transactions, otherwise a caller can reach the database around it. Do not hold on to the session or its inner database after the session closes. :::warning - -Custom database layers depend on the `Database` API from `serverpod_database`. This API may receive breaking changes in a minor Serverpod release. Review and test the complete implementation whenever you upgrade Serverpod. - +Custom database layers build on the `Database` API from `serverpod_database`, which can change in a breaking way in a minor Serverpod release. Review and test your implementation whenever you upgrade Serverpod. ::: -## Use an interceptor in integration tests +## Use an interceptor in tests -Generated `withServerpod` test helpers accept the same `databaseInterceptor` callback. This lets a test count session creation or return a recording database layer without changing server startup code: +The generated [`withServerpod`](../../testing/the-basics) test helper takes the same `databaseInterceptor` callback, so a test can install a recording or restricting database layer without touching the server startup code. The callback runs once for every session the helper creates. ```dart -var interceptedSessions = 0; +var recorder = QueryRecorder(); withServerpod( - 'database policy', - databaseInterceptor: (session, inner) { - interceptedSessions++; - return inner; - }, - (sessionBuilder, _) { - test('creates the database layer for a test session', () { - var beforeBuild = interceptedSessions; - sessionBuilder.build(); - expect(interceptedSessions, beforeBuild + 1); + 'Given Companies endpoint', + (sessionBuilder, endpoints) { + test('then calling `all` records the query', () async { + await endpoints.companies.all(sessionBuilder); + expect(recorder.queries, isNotEmpty); }); }, + databaseInterceptor: (session, inner) => + RecordingDatabase(inner: inner, recorder: recorder), ); ``` - -The callback is also invoked once for every test session created by the helper. diff --git a/docs/06-concepts/08-testing/02-the-basics.md b/docs/06-concepts/08-testing/02-the-basics.md index cef7a338..295045df 100644 --- a/docs/06-concepts/08-testing/02-the-basics.md +++ b/docs/06-concepts/08-testing/02-the-basics.md @@ -135,6 +135,7 @@ The following optional configuration options are available to pass as named argu |:-----|:-----|:---:| |`applyMigrations`|Whether pending migrations should be applied when starting Serverpod.|`true`| |`configOverride`|A function that overrides values in the loaded Serverpod config before the test server starts.|`null`| +|`databaseInterceptor`|A callback that returns the database used as `Session.db` for each test session. See [Database interceptors](../data-and-the-database/database/database-interceptors).|`null`| |`enableSessionLogging`|Whether session logging should be enabled.|`false`| |`experimentalFeatures`|Optionally specify experimental features used by Serverpod.|`null`| |`rollbackDatabase`|Options for when to rollback the database during the test lifecycle (or disable it). See detailed description [here](#rollback-database-configuration).|`RollbackDatabase.afterEach`| From dbb44f4e9b1c72d46e1549cb0ca9de35e706b37f Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Thu, 23 Jul 2026 12:18:55 +0100 Subject: [PATCH 3/4] docs: Fit the database interceptors page into the restructured section --- .../02-database/18-database-interceptors.md | 25 ++----------------- docs/06-concepts/08-testing/02-the-basics.md | 1 - docs/06-concepts/cli/_generated/cloud.md | 1 + 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md b/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md index ca8d2473..a88e2389 100644 --- a/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md +++ b/docs/06-concepts/03-data-and-the-database/02-database/18-database-interceptors.md @@ -1,5 +1,5 @@ --- -description: A database interceptor replaces Session.db once per session with a custom database layer for tracing, policy enforcement, tenant scoping, and tests. +description: A database interceptor replaces session.db once per session with a custom database layer for tracing, policy enforcement, and tenant scoping. --- # Database interceptors @@ -40,7 +40,7 @@ var pod = Serverpod( ); ``` -The `Database` class has no public constructor to subclass, so implement the interface and let your IDE generate the overrides. Hold on to the supplied `inner` database and delegate every operation the layer does not change. The object you return becomes `Session.db`, so generated calls such as `User.db.find(session)` pass through it as well. +The `Database` class has no public constructor to subclass, so implement the interface and let your IDE generate the overrides. Hold on to the supplied `inner` database and delegate every operation the layer does not change. The object you return becomes `session.db`, so generated calls such as `User.db.find(session)` pass through it as well. Typical uses are: @@ -48,30 +48,9 @@ Typical uses are: - Applying a tenant scope to every supported query. - Enforcing read and write policies. - Blocking unsafe operations or writes in protected environments. -- Recording or replacing database behavior in tests. Pass transaction objects, [runtime parameters](./runtime-parameters), ordering, pagination, [row locks](./row-locking), and return options through without changing their meaning. A layer that enforces a policy also needs to cover [raw access](./raw-access) and transactions, otherwise a caller can reach the database around it. Do not hold on to the session or its inner database after the session closes. :::warning Custom database layers build on the `Database` API from `serverpod_database`, which can change in a breaking way in a minor Serverpod release. Review and test your implementation whenever you upgrade Serverpod. ::: - -## Use an interceptor in tests - -The generated [`withServerpod`](../../testing/the-basics) test helper takes the same `databaseInterceptor` callback, so a test can install a recording or restricting database layer without touching the server startup code. The callback runs once for every session the helper creates. - -```dart -var recorder = QueryRecorder(); - -withServerpod( - 'Given Companies endpoint', - (sessionBuilder, endpoints) { - test('then calling `all` records the query', () async { - await endpoints.companies.all(sessionBuilder); - expect(recorder.queries, isNotEmpty); - }); - }, - databaseInterceptor: (session, inner) => - RecordingDatabase(inner: inner, recorder: recorder), -); -``` diff --git a/docs/06-concepts/08-testing/02-the-basics.md b/docs/06-concepts/08-testing/02-the-basics.md index 295045df..cef7a338 100644 --- a/docs/06-concepts/08-testing/02-the-basics.md +++ b/docs/06-concepts/08-testing/02-the-basics.md @@ -135,7 +135,6 @@ The following optional configuration options are available to pass as named argu |:-----|:-----|:---:| |`applyMigrations`|Whether pending migrations should be applied when starting Serverpod.|`true`| |`configOverride`|A function that overrides values in the loaded Serverpod config before the test server starts.|`null`| -|`databaseInterceptor`|A callback that returns the database used as `Session.db` for each test session. See [Database interceptors](../data-and-the-database/database/database-interceptors).|`null`| |`enableSessionLogging`|Whether session logging should be enabled.|`false`| |`experimentalFeatures`|Optionally specify experimental features used by Serverpod.|`null`| |`rollbackDatabase`|Options for when to rollback the database during the test lifecycle (or disable it). See detailed description [here](#rollback-database-configuration).|`RollbackDatabase.afterEach`| diff --git a/docs/06-concepts/cli/_generated/cloud.md b/docs/06-concepts/cli/_generated/cloud.md index e69de29b..8b137891 100644 --- a/docs/06-concepts/cli/_generated/cloud.md +++ b/docs/06-concepts/cli/_generated/cloud.md @@ -0,0 +1 @@ + From 7ff31ebf0a3ee00d93ece9e31620c251b8fb8fcb Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Thu, 23 Jul 2026 12:25:10 +0100 Subject: [PATCH 4/4] docs: Restore generated CLI reference file --- docs/06-concepts/cli/_generated/cloud.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/06-concepts/cli/_generated/cloud.md b/docs/06-concepts/cli/_generated/cloud.md index 8b137891..e69de29b 100644 --- a/docs/06-concepts/cli/_generated/cloud.md +++ b/docs/06-concepts/cli/_generated/cloud.md @@ -1 +0,0 @@ -