@nxgt/mongo-kit 0.1.4 → 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 +15 -0
- package/docs/README.md +14 -0
- package/docs/guide/actor-and-transactions.md +196 -0
- package/docs/guide/configuration.md +229 -0
- package/docs/guide/db-scope.md +173 -0
- package/docs/guide/discover-collections.md +83 -0
- package/docs/guide/sync.md +108 -0
- package/docs/roadmap.md +62 -0
- package/docs/troubleshooting.md +565 -0
- package/package.json +4 -3
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).
|