Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::

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).
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
description: A database interceptor replaces Session.db once per session with a custom database layer for tracing, policy enforcement, tenant scoping, and tests.
---

# Database interceptors

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.

Register the interceptor when you construct `Serverpod`:

```dart
var pod = Serverpod(
args,
Protocol(),
Endpoints(),
databaseInterceptor: (session, inner) {
session.log('Created a database layer for this session.');
return inner;
},
);
```

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

To intercept the operations themselves, return your own implementation of the `Database` interface exported by `package:serverpod/serverpod.dart`:

```dart
var pod = Serverpod(
args,
Protocol(),
Endpoints(),
databaseInterceptor: (session, inner) {
return PolicyDatabase(
session: session,
inner: inner,
);
},
);
```

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:

- Recording query traces, timings, and metrics.
- 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),
);
```
1 change: 1 addition & 0 deletions docs/06-concepts/08-testing/02-the-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`|
Expand Down
Loading