@nxgt/mongo-kit 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -268,6 +268,21 @@ Each is a `@ts-expect-error` case in this package's type tests.
268
268
  - **`kit.db` throws on a kit with several databases**, where its type is
269
269
  already `never`: the message names the databases to read instead.
270
270
 
271
+ ## Documentation
272
+
273
+ - [Guide index](docs/README.md) — every page, and when to read it.
274
+ - [Configuration](docs/guide/configuration.md) — the databases, the
275
+ collections, and the options each is built with.
276
+ - [The `db` scope](docs/guide/db-scope.md) — the collections on the driver's
277
+ `Db`, and the kit in a request.
278
+ - [The actor, sessions and transactions](docs/guide/actor-and-transactions.md)
279
+ — `as`, `withSession` and `transaction`.
280
+ - [Syncing](docs/guide/sync.md) — the deployment step, and `dryRun`.
281
+ - [`discoverCollections`](docs/guide/discover-collections.md) — definitions
282
+ from a glob, for scripts.
283
+ - [Troubleshooting](docs/troubleshooting.md) — the errors, by their message.
284
+ - [Roadmap](docs/roadmap.md) — what is next, and what is not planned.
285
+
271
286
  ## License
272
287
 
273
288
  MIT
package/docs/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # `@nxgt/mongo-kit` documentation
2
+
3
+ The [README](../README.md) is the short version: what the package is, and one
4
+ example per area. These pages are the long one.
5
+
6
+ | Page | Read it when |
7
+ | --- | --- |
8
+ | [Configuration](guide/configuration.md) | you are describing the databases and the collections of an application, one database or several |
9
+ | [The `db` scope](guide/db-scope.md) | you are reading `kit.db.users`, reaching for something only the driver's `Db` has, or wondering when a collection is built |
10
+ | [The actor, sessions and transactions](guide/actor-and-transactions.md) | a write has to be stamped with who made it, or several writes have to commit together |
11
+ | [Syncing](guide/sync.md) | the collections, their validators and their indexes have to exist on the server |
12
+ | [`discoverCollections`](guide/discover-collections.md) | a script has to find the definitions of a repository without importing each one |
13
+ | [Troubleshooting](troubleshooting.md) | something threw, and you have the message |
14
+ | [Roadmap](roadmap.md) | you want to know what is coming, and what will not |
@@ -0,0 +1,196 @@
1
+ # The actor, sessions and transactions
2
+
3
+ Who a write is stamped as, and which writes commit together. Both are kits of
4
+ their own: `as`, `withSession` and `transaction` give back **another kit**
5
+ over the same clients, and leave the one they came from alone.
6
+
7
+ ```ts
8
+ import { ObjectId } from 'mongodb';
9
+ import { kit } from './db'; // the kit `createKit` returned
10
+
11
+ const actor = new ObjectId();
12
+
13
+ const post = await kit.as(actor).db.posts.create({ title: 'a' });
14
+ post.createdBy; // the actor
15
+
16
+ const plain = await kit.db.posts.create({ title: 'b' });
17
+ plain.createdBy; // null — the kit it came from never changed
18
+ ```
19
+
20
+ ## `as(actor)`
21
+
22
+ One call stamps every collection of the kit: `createdBy` on a create,
23
+ `updatedBy` on an update, `deletedBy` on a soft delete — whichever stamps the
24
+ definition asked for.
25
+
26
+ ```ts
27
+ const writer = kit.as(actor);
28
+ await writer.db.users.create({ email: 'ada@example.com' }); // createdBy
29
+ await writer.db.posts.create({ title: 'a' }); // createdBy
30
+ writer.actor; // the actor
31
+ writer.clients.default === kit.clients.default; // the same client
32
+ writer.db.users !== kit.db.users; // its own collections
33
+ ```
34
+
35
+ The actor's **type** is the one the collections agree on: a kit whose
36
+ collections all stamp an `ObjectId` takes an `ObjectId`. A kit whose
37
+ collections stamp no actor has no `as` to call — its type is `never` — and so
38
+ does one whose collections stamp actors of different types, since a single
39
+ call could not stamp both.
40
+
41
+ ```ts
42
+ type KitActor<C>; // the intersection of every wired definition's ActorOf
43
+ ```
44
+
45
+ ## `withSession(session)`
46
+
47
+ The kit's collections all run in that session; `undefined` takes it away
48
+ again.
49
+
50
+ ```ts
51
+ const session = kit.clients.default.startSession();
52
+ try {
53
+ const inSession = kit.withSession(session);
54
+ await inSession.db.users.create({ email: 'ada@example.com' });
55
+ inSession.withSession(undefined).session; // undefined
56
+ } finally {
57
+ await session.endSession();
58
+ }
59
+ ```
60
+
61
+ `as` and `withSession` compose, in either order, and each keeps what the
62
+ other set.
63
+
64
+ ## `transaction(fn, options?)`
65
+
66
+ The body is given a kit whose collections are **all** in the transaction:
67
+ nothing has to be threaded through.
68
+
69
+ ```ts
70
+ const written = await kit.as(actor).transaction(async (tx) => {
71
+ const team = await tx.db.teams.create({ name: 'Core' });
72
+ await tx.db.users.update(userId, { teamId: team._id });
73
+ return team;
74
+ });
75
+ ```
76
+
77
+ If the body throws, everything it wrote is rolled back and the error comes
78
+ back out:
79
+
80
+ ```ts
81
+ await kit.transaction(async (tx) => {
82
+ await tx.db.users.create({ email: 'ada@example.com' });
83
+ throw new Error('no');
84
+ });
85
+ // rejects with 'no'; the user is not there
86
+ await kit.db.users.count(); // 0
87
+ ```
88
+
89
+ | Option | Type | Default | Effect |
90
+ | --- | --- | --- | --- |
91
+ | `on` | `DbName<C>` | the only client | Which database's client carries the transaction. Required once the kit holds more than one **client** |
92
+ | `readConcern`, `writeConcern`, `readPreference`, `maxCommitTimeMS` | the driver's `TransactionOptions` | the client's | Passed to the driver as they are |
93
+
94
+ ```ts
95
+ await kit.transaction(
96
+ (tx) => tx.db.users.create({ email: 'ada@example.com' }),
97
+ { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } },
98
+ );
99
+ ```
100
+
101
+ ### What the body must accept
102
+
103
+ - **It may run twice.** The driver retries it from the start on a transient
104
+ error, so it must hold nothing MongoDB would not roll back — no email sent,
105
+ no counter raised in Redis, no file written.
106
+ - **A transaction inside a transaction joins the outer one**, and takes no
107
+ `on`: the session has already decided. MongoDB has no savepoints, so an
108
+ inner failure takes the whole transaction with it.
109
+ - **It reaches one client's databases.** With `{ on: 'main' }`, an operation
110
+ on a database of another client carries a session that client does not own,
111
+ and the driver refuses it.
112
+ - **A replica set is required**, which is MongoDB's own rule for
113
+ transactions, not this package's.
114
+
115
+ ```ts
116
+ await kit.transaction(async (outer) => {
117
+ await outer.db.users.create({ email: 'ada@example.com' });
118
+ await outer.transaction(async (inner) => {
119
+ inner.session === outer.session; // true: it joined
120
+ await inner.db.posts.create({ title: 'a' });
121
+ });
122
+ });
123
+ ```
124
+
125
+ `{ on }` is required at **run time**, not by the types, and cannot be: two
126
+ databases on one URI share a client and need none, so what decides is the
127
+ number of clients. A kit holding two without it throws a `TypeError` naming
128
+ what to write.
129
+
130
+ ## In a request
131
+
132
+ The kit is built once; each request derives the kit that stamps its user, and
133
+ a handler is handed services built on it rather than the kit itself.
134
+
135
+ ```ts
136
+ import { tryObjectId } from '@nxgt/mongo';
137
+ import { createMiddleware } from 'hono/factory';
138
+ import type { Kit } from '../db';
139
+ import { buildServices } from '../context';
140
+
141
+ export const provideServices = (kit: Kit) =>
142
+ createMiddleware(async (c, next) => {
143
+ const actor = tryObjectId(c.req.header('x-user-id'));
144
+ if (!actor) return c.json({ message: 'errors.unauthenticated' }, 401);
145
+ c.set('services', buildServices(kit.as(actor)));
146
+ await next();
147
+ });
148
+ ```
149
+
150
+ ```ts
151
+ export class TeamService {
152
+ constructor(private readonly kit: Kit) {}
153
+
154
+ /** Two collections, one commit — and both writes stamped with the request's user. */
155
+ createWithOwner(name: string, ownerId: ObjectId) {
156
+ return this.kit.transaction(async (tx) => {
157
+ const team = await tx.db.teams.create({ name });
158
+ await tx.db.users.update(ownerId, { teamId: team._id });
159
+ return team;
160
+ });
161
+ }
162
+ }
163
+ ```
164
+
165
+ The service holds the request's kit, so `this.kit.transaction` already stamps
166
+ the right actor: the transaction inherits it.
167
+
168
+ ## Closing a derived kit
169
+
170
+ `close()` on a kit from `as`, `withSession` or a transaction throws — the
171
+ clients belong to the kit `createKit` returned, and that is the one to close.
172
+
173
+ ## Signatures
174
+
175
+ ```ts
176
+ type KitActor<C> = [ActorOf<WiredDefinition<C>>] extends [never]
177
+ ? never
178
+ : UnionToIntersection<ActorOf<WiredDefinition<C>>>;
179
+
180
+ type KitTransactionOptions<C> = TransactionOptions & { on?: DbName<C> };
181
+
182
+ interface MongoKit<C> {
183
+ as(actor: KitActor<C>): MongoKit<C>;
184
+ withSession(session: ClientSession | undefined): MongoKit<C>;
185
+ transaction<T>(
186
+ fn: (kit: MongoKit<C>) => Promise<T>,
187
+ options?: KitTransactionOptions<C>,
188
+ ): Promise<T>;
189
+ }
190
+ ```
191
+
192
+ ## Next
193
+
194
+ - [The `db` scope](db-scope.md) — what a derived kit shares, and what it
195
+ builds again.
196
+ - [Troubleshooting](../troubleshooting.md) — the messages these refusals use.
@@ -0,0 +1,229 @@
1
+ # Configuration
2
+
3
+ `defineConfig` describes an application's MongoDB — where each database is,
4
+ and which collections live on it — and checks that description before
5
+ anything connects.
6
+
7
+ ```ts
8
+ import { defineConfig } from '@nxgt/mongo-kit';
9
+ import * as collections from './models';
10
+
11
+ export const config = defineConfig({
12
+ uri: process.env.MONGO_URI!,
13
+ collections,
14
+ });
15
+ ```
16
+
17
+ It opens no socket and reads no environment variable of its own: the
18
+ application reads `process.env`, and what is wrong with the configuration
19
+ throws here, where the application starts, rather than at the first query.
20
+ [`createKit`](db-scope.md) is what connects.
21
+
22
+ ## The collections
23
+
24
+ `collections` is a module object, the one `import * as` gives:
25
+
26
+ ```ts
27
+ // src/models/index.ts
28
+ export * from './users.model';
29
+ export * from './posts.model';
30
+ ```
31
+
32
+ ```ts
33
+ // src/models/users.model.ts
34
+ import { defineCollection, id, objectId } from '@nxgt/mongo';
35
+ import { z } from 'zod';
36
+
37
+ export const users = defineCollection({
38
+ name: 'users',
39
+ schema: z.object({
40
+ _id: id(),
41
+ email: z.string(),
42
+ name: z.string().optional(),
43
+ }),
44
+ timestamps: true,
45
+ actors: { type: objectId() },
46
+ indexes: [{ key: { email: 1 }, unique: true, name: 'users_email_unique' }],
47
+ });
48
+ ```
49
+
50
+ Every export that is a `defineCollection` becomes a key on the scope, under
51
+ the name it is **exported** by; a function, a constant or a type in the same
52
+ module is left where it is. The key is what the application reads
53
+ (`kit.db.users`) and the definition's own `name` is what the server holds, so
54
+ `export const users = defineCollection({ name: 'app_users', … })` is
55
+ `kit.db.users` here and `app_users` there.
56
+
57
+ Two exports pointing at one server collection are refused, and so is a key
58
+ the driver's `Db` already answers to (`command`, `watch`, `collection`, …):
59
+ it would be unreachable on the scope. Both are compile errors, and both are
60
+ checked again against the object at `createKit`.
61
+
62
+ ## Options
63
+
64
+ | Option | Type | Default | Effect |
65
+ | --- | --- | --- | --- |
66
+ | `uri` | `string` | — | Where to connect. One of `uri` and `client`, never both |
67
+ | `client` | `MongoClient` | — | A client the application opened. The kit uses it and never closes it |
68
+ | `clientOptions` | `MongoClientOptions` | `{}` | Passed to the driver with `uri`. Refused beside `client`, which has its own |
69
+ | `database` | `string` | the URI's, else `test` | The database's name |
70
+ | `collections` | module object | — | `import * as collections from './models'` |
71
+ | `options` | `KitCollectionOptions<AnyCollectionDefinition>` | `{}` | `@nxgt/mongo`'s collection options, for every collection of this database |
72
+ | `optionsFor` | `{ [key]?: KitCollectionOptions<Def> }` | `{}` | The same, per key, merged **over** `options` |
73
+ | `autoSync` | `boolean` | `false` | Sync each collection before its first operation |
74
+
75
+ `options` and `optionsFor` take `@nxgt/mongo`'s own collection options —
76
+ `maxPageSize`, `coerce`, `validate`, `softDelete`, `touchUpdatedAt`,
77
+ `optimisticLock`, `hooks` — minus the four the kit decides itself:
78
+
79
+ ```ts
80
+ defineConfig({
81
+ uri: process.env.MONGO_URI!,
82
+ collections,
83
+ options: { maxPageSize: 50 },
84
+ optionsFor: { posts: { softDelete: false } },
85
+ });
86
+ ```
87
+
88
+ `db`, `session`, `actor` and `autoSync` are **not** collection options here.
89
+ The database is named by its key, [`as` and `withSession`](actor-and-transactions.md)
90
+ carry the actor and the session, and `autoSync` is the database's. One of
91
+ them under `options` does not compile; under `optionsFor` the types cannot
92
+ see that deep, and `defineConfig` throws instead.
93
+
94
+ `optionsFor` under a key no collection is wired under does not compile
95
+ either, and the message names the key.
96
+
97
+ ### `autoSync`
98
+
99
+ ```ts
100
+ defineConfig({ uri: server.uri, collections, autoSync: true });
101
+ ```
102
+
103
+ Each collection is synced before its first operation, once per database — for
104
+ tests and for local development. In production [`kit.sync()`](sync.md) is a
105
+ deployment step: `collMod` needs the `dbAdmin` role, and an index build runs
106
+ outside any transaction.
107
+
108
+ ## Several databases
109
+
110
+ They name themselves, and the names are the keys on `kit.databases` and on
111
+ the sync report:
112
+
113
+ ```ts
114
+ import { defineConfig } from '@nxgt/mongo-kit';
115
+ import * as collections from './models';
116
+ import * as events from './events';
117
+
118
+ export const config = defineConfig({
119
+ databases: {
120
+ main: { uri: process.env.MONGO_URI!, collections },
121
+ analytics: {
122
+ uri: process.env.ANALYTICS_URI!,
123
+ database: 'analytics',
124
+ collections: events,
125
+ },
126
+ },
127
+ });
128
+ ```
129
+
130
+ Each entry takes the same keys as a lone database. A config that names none
131
+ is held under `default`, so `kit.databases.default` is the long way of
132
+ writing `kit.db`.
133
+
134
+ Two databases on one URI **share one client** — that is what `connectMongo`
135
+ already does — and must then be configured with the same `clientOptions`: a
136
+ second hold on a client opened with other options is refused.
137
+
138
+ ## The kit's type
139
+
140
+ `KitOf<typeof config>` is the type of the kit this config produces, for a
141
+ service or a module that declares it rather than reading it off
142
+ `await createKit(…)`:
143
+
144
+ ```ts
145
+ import { defineConfig, type KitOf } from '@nxgt/mongo-kit';
146
+ import * as collections from './models';
147
+
148
+ export const config = defineConfig({
149
+ uri: process.env.MONGO_URI!,
150
+ collections,
151
+ options: { maxPageSize: 50 },
152
+ });
153
+
154
+ /** This application's kit, read from the configuration rather than written twice. */
155
+ export type Kit = KitOf<typeof config>;
156
+ ```
157
+
158
+ ```ts
159
+ import type { Kit } from './db';
160
+
161
+ export class UserService {
162
+ constructor(private readonly kit: Kit) {}
163
+
164
+ create(input: { email: string }) {
165
+ return this.kit.db.users.create(input);
166
+ }
167
+ }
168
+ ```
169
+
170
+ ## What it throws
171
+
172
+ Every check is a `TypeError`, thrown from `defineConfig`, before anything
173
+ connects. The message names the database it is about:
174
+
175
+ ```ts
176
+ defineConfig({ collections });
177
+ // TypeError: defineConfig: database "default" has neither a uri nor a client
178
+ ```
179
+
180
+ - a database with both a `uri` and a `client`, or neither;
181
+ - `clientOptions` beside a `client` the kit did not open;
182
+ - a `collections` object with no definition in it — the usual cause is a
183
+ default export, or an object of schemas rather than of definitions;
184
+ - two keys wiring the same server collection;
185
+ - `optionsFor` under a key the database does not wire;
186
+ - `db`, `session`, `actor` or `autoSync` inside `options` or `optionsFor`.
187
+
188
+ [Troubleshooting](../troubleshooting.md) has each message with its fix.
189
+
190
+ ## Signatures
191
+
192
+ ```ts
193
+ function defineConfig<const C extends KitConfigInput>(
194
+ config: C & Checked<C>,
195
+ ): KitConfig<C>;
196
+
197
+ type KitConfigInput =
198
+ | DatabaseConfig<object>
199
+ | { databases: Record<string, DatabaseConfig<object>> };
200
+
201
+ interface DatabaseConfig<C> {
202
+ uri?: string;
203
+ client?: MongoClient;
204
+ clientOptions?: MongoClientOptions;
205
+ database?: string;
206
+ collections: C;
207
+ options?: KitCollectionOptions<AnyCollectionDefinition>;
208
+ optionsFor?: {
209
+ [K in keyof CollectionsOf<C>]?: KitCollectionOptions<CollectionsOf<C>[K]>;
210
+ };
211
+ autoSync?: boolean;
212
+ }
213
+
214
+ type KitCollectionOptions<Def> = Omit<
215
+ CollectionOptions<Def>,
216
+ 'db' | 'session' | 'actor' | 'autoSync'
217
+ >;
218
+
219
+ type KitOf<Config> = Config extends KitConfig<infer C> ? MongoKit<C> : never;
220
+ ```
221
+
222
+ `CollectionsOf`, `CollectionsIn`, `DbName`, `NoCollision`, `ReservedName` and
223
+ `Unwired` are exported too: they are what the refusals above are written
224
+ with.
225
+
226
+ ## Next
227
+
228
+ - [The `db` scope](db-scope.md) — what `createKit` gives back.
229
+ - [Syncing](sync.md) — making the server match the definitions.
@@ -0,0 +1,173 @@
1
+ # The `db` scope
2
+
3
+ `createKit` opens what the [configuration](configuration.md) describes and
4
+ gives back a `MongoKit`, whose `db` is the driver's `Db` with every collection
5
+ typed on it.
6
+
7
+ ```ts
8
+ import { createKit, defineConfig } from '@nxgt/mongo-kit';
9
+ import * as collections from './models';
10
+
11
+ await using kit = await createKit(
12
+ defineConfig({ uri: process.env.MONGO_URI!, collections }),
13
+ );
14
+
15
+ const user = await kit.db.users.create({ email: 'ada@example.com' });
16
+ const posts = await kit.db.posts.findMany({ filter: { authorId: user._id } });
17
+ await kit.db.command({ ping: 1 }); // the driver's Db, untouched
18
+ ```
19
+
20
+ `kit.db.users` is exactly what `getCollection(db, users)` gives — the
21
+ driver's `Collection` with `@nxgt/mongo`'s pagination, soft delete,
22
+ optimistic locking and stamps on it — so everything that package documents
23
+ about a collection holds here.
24
+
25
+ ## What the kit holds
26
+
27
+ | Member | Type | Effect |
28
+ | --- | --- | --- |
29
+ | `db` | `SoleScope<C>` | The only database's scope. `never` when the kit has several, and reading it anyway throws |
30
+ | `databases` | `{ [name]: DbScope }` | Every scope, under the name the config gave it — `default` when it named none |
31
+ | `clients` | `{ [name]: MongoClient }` | The client of each database. Two databases on one URI share one |
32
+ | `actor` | `KitActor<C> \| undefined` | What this kit stamps into the `*By` fields |
33
+ | `session` | `ClientSession \| undefined` | The session every collection of this kit runs in |
34
+ | `as(actor)` | `MongoKit<C>` | [Another kit, stamping that actor](actor-and-transactions.md) |
35
+ | `withSession(session)` | `MongoKit<C>` | [Another kit, in that session](actor-and-transactions.md) |
36
+ | `transaction(fn, options?)` | `Promise<T>` | [`fn` with a kit in a transaction](actor-and-transactions.md) |
37
+ | `sync(options?)` | `Promise<Record<DbName<C>, SyncReport[]>>` | [A deployment step](sync.md) |
38
+ | `close()` | `Promise<void>` | Gives back what the kit opened. Idempotent |
39
+
40
+ A kit is `AsyncDisposable`, so `await using kit = await createKit(config)`
41
+ closes it at the end of the block.
42
+
43
+ ## The scope is a `Db` underneath
44
+
45
+ The collections are own properties; everything else is read through to the
46
+ driver's `Db`:
47
+
48
+ ```ts
49
+ Object.keys(kit.db); // ['users', 'posts'] — not the driver's members
50
+ 'users' in kit.db; // true
51
+ 'command' in kit.db; // true
52
+
53
+ const { command } = kit.db; // a driver method read off it is bound
54
+ await command({ ping: 1 });
55
+ kit.db.databaseName; // 'app'
56
+ ```
57
+
58
+ A collection is built the **first time it is read**, and kept:
59
+
60
+ ```ts
61
+ kit.db.users === kit.db.users; // true
62
+ ```
63
+
64
+ So a kit derived per request pays for the collections that request touches
65
+ and for no others.
66
+
67
+ ## Several databases
68
+
69
+ ```ts
70
+ await kit.databases.main.users.create({ email: 'ada@example.com' });
71
+ await kit.databases.analytics.events.create({ kind: 'signup' });
72
+ kit.clients.main === kit.clients.analytics; // true when one URI wires both
73
+ ```
74
+
75
+ `kit.db` is then `never`, and reading it anyway — from JavaScript, or across
76
+ an `any` — throws a `TypeError` naming the databases to read instead: with
77
+ two of them there is no "the" database.
78
+
79
+ ## Closing
80
+
81
+ ```ts
82
+ await kit.close();
83
+ ```
84
+
85
+ It gives back the clients it opened, and leaves alone a `client` the config
86
+ handed it: what it did not open is not its to close, `await using` included.
87
+ Only the kit `createKit` returned may be closed — one from `as`,
88
+ `withSession` or a transaction shares those clients and throws.
89
+
90
+ ## In a request
91
+
92
+ A kit is built once, at startup, and each request derives its own. Nothing
93
+ else has to be wired: no `getCollection` at the call site, no client passed
94
+ around.
95
+
96
+ ```ts
97
+ // src/services/users.service.ts
98
+ import type { Kit } from '../db';
99
+
100
+ export class UserService {
101
+ constructor(private readonly kit: Kit) {}
102
+
103
+ list(page?: number) {
104
+ return this.kit.db.users.paginate({ page });
105
+ }
106
+
107
+ create(input: { email: string }) {
108
+ return this.kit.db.users.create(input);
109
+ }
110
+ }
111
+ ```
112
+
113
+ ```ts
114
+ // src/middlewares/services.ts
115
+ import { tryObjectId } from '@nxgt/mongo';
116
+ import { createMiddleware } from 'hono/factory';
117
+ import type { Kit } from '../db';
118
+ import { UserService } from '../services/users.service';
119
+
120
+ export const provideServices = (kit: Kit) =>
121
+ createMiddleware(async (c, next) => {
122
+ const actor = tryObjectId(c.req.header('x-user-id'));
123
+ if (!actor) return c.json({ message: 'errors.unauthenticated' }, 401);
124
+ // One kit per request, stamping that user; the kit it came from is
125
+ // untouched, so a request's actor never leaks into the next.
126
+ c.set('services', { users: new UserService(kit.as(actor)) });
127
+ await next();
128
+ });
129
+ ```
130
+
131
+ ```ts
132
+ // src/index.ts
133
+ import { createKit } from '@nxgt/mongo-kit';
134
+ import { buildApp } from './app';
135
+ import { config } from './db';
136
+
137
+ const kit = await createKit(config);
138
+ Bun.serve({ fetch: buildApp(kit).fetch });
139
+ ```
140
+
141
+ A handler reads `c.get('services')` and never reaches the kit itself, so it
142
+ cannot write as somebody else and cannot close it.
143
+
144
+ ## Signatures
145
+
146
+ ```ts
147
+ function createKit<C>(config: KitConfig<C>): Promise<MongoKit<C>>;
148
+
149
+ type DbScope<C> = {
150
+ readonly [K in keyof CollectionsOf<C>]: TypedCollection<CollectionsOf<C>[K]>;
151
+ } & Db;
152
+
153
+ interface MongoKit<C> extends AsyncDisposable {
154
+ readonly db: SoleScope<C>;
155
+ readonly databases: { readonly [N in DbName<C>]: DbScope<CollectionsIn<C, N>> };
156
+ readonly clients: { readonly [N in DbName<C>]: MongoClient };
157
+ readonly actor: KitActor<C> | undefined;
158
+ readonly session: ClientSession | undefined;
159
+ as(actor: KitActor<C>): MongoKit<C>;
160
+ withSession(session: ClientSession | undefined): MongoKit<C>;
161
+ transaction<T>(
162
+ fn: (kit: MongoKit<C>) => Promise<T>,
163
+ options?: KitTransactionOptions<C>,
164
+ ): Promise<T>;
165
+ sync(options?: SyncOptions): Promise<Record<DbName<C>, SyncReport[]>>;
166
+ close(): Promise<void>;
167
+ }
168
+ ```
169
+
170
+ ## Next
171
+
172
+ - [The actor, sessions and transactions](actor-and-transactions.md).
173
+ - [Syncing](sync.md).