@nxgt/mongo-kit 0.1.4 → 0.2.0
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 +56 -4
- package/dist/config/checks.d.ts.map +1 -1
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/discover.d.ts.map +1 -1
- package/dist/errors/kit-error.d.ts +41 -0
- package/dist/errors/kit-error.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -30
- package/dist/index.js.map +11 -10
- package/dist/kit/context.d.ts.map +1 -1
- package/dist/kit/create-kit.d.ts.map +1 -1
- package/dist/kit/derive.d.ts.map +1 -1
- package/dist/kit/transaction.d.ts.map +1 -1
- package/docs/README.md +15 -0
- package/docs/guide/actor-and-transactions.md +196 -0
- package/docs/guide/configuration.md +230 -0
- package/docs/guide/db-scope.md +174 -0
- package/docs/guide/discover-collections.md +85 -0
- package/docs/guide/errors.md +198 -0
- package/docs/guide/sync.md +108 -0
- package/docs/roadmap.md +68 -0
- package/docs/troubleshooting.md +595 -0
- package/package.json +4 -3
|
@@ -0,0 +1,230 @@
|
|
|
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 [`KitError`](errors.md) with `code: 'CONFIG'`, thrown from
|
|
173
|
+
`defineConfig`, before anything connects. The message names the database it
|
|
174
|
+
is about, and so does `error.database`:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
defineConfig({ collections });
|
|
178
|
+
// KitError: defineConfig: database "default" has neither a uri nor a client
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- a database with both a `uri` and a `client`, or neither;
|
|
182
|
+
- `clientOptions` beside a `client` the kit did not open;
|
|
183
|
+
- a `collections` object with no definition in it — the usual cause is a
|
|
184
|
+
default export, or an object of schemas rather than of definitions;
|
|
185
|
+
- two keys wiring the same server collection;
|
|
186
|
+
- `optionsFor` under a key the database does not wire;
|
|
187
|
+
- `db`, `session`, `actor` or `autoSync` inside `options` or `optionsFor`.
|
|
188
|
+
|
|
189
|
+
[Troubleshooting](../troubleshooting.md) has each message with its fix.
|
|
190
|
+
|
|
191
|
+
## Signatures
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
function defineConfig<const C extends KitConfigInput>(
|
|
195
|
+
config: C & Checked<C>,
|
|
196
|
+
): KitConfig<C>;
|
|
197
|
+
|
|
198
|
+
type KitConfigInput =
|
|
199
|
+
| DatabaseConfig<object>
|
|
200
|
+
| { databases: Record<string, DatabaseConfig<object>> };
|
|
201
|
+
|
|
202
|
+
interface DatabaseConfig<C> {
|
|
203
|
+
uri?: string;
|
|
204
|
+
client?: MongoClient;
|
|
205
|
+
clientOptions?: MongoClientOptions;
|
|
206
|
+
database?: string;
|
|
207
|
+
collections: C;
|
|
208
|
+
options?: KitCollectionOptions<AnyCollectionDefinition>;
|
|
209
|
+
optionsFor?: {
|
|
210
|
+
[K in keyof CollectionsOf<C>]?: KitCollectionOptions<CollectionsOf<C>[K]>;
|
|
211
|
+
};
|
|
212
|
+
autoSync?: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
type KitCollectionOptions<Def> = Omit<
|
|
216
|
+
CollectionOptions<Def>,
|
|
217
|
+
'db' | 'session' | 'actor' | 'autoSync'
|
|
218
|
+
>;
|
|
219
|
+
|
|
220
|
+
type KitOf<Config> = Config extends KitConfig<infer C> ? MongoKit<C> : never;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`CollectionsOf`, `CollectionsIn`, `DbName`, `NoCollision`, `ReservedName` and
|
|
224
|
+
`Unwired` are exported too: they are what the refusals above are written
|
|
225
|
+
with.
|
|
226
|
+
|
|
227
|
+
## Next
|
|
228
|
+
|
|
229
|
+
- [The `db` scope](db-scope.md) — what `createKit` gives back.
|
|
230
|
+
- [Syncing](sync.md) — making the server match the definitions.
|
|
@@ -0,0 +1,174 @@
|
|
|
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 [`KitError`](errors.md) with
|
|
77
|
+
`code: 'SEVERAL_DATABASES'`, naming the databases to read instead: with two
|
|
78
|
+
of them there is no "the" database.
|
|
79
|
+
|
|
80
|
+
## Closing
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await kit.close();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
It gives back the clients it opened, and leaves alone a `client` the config
|
|
87
|
+
handed it: what it did not open is not its to close, `await using` included.
|
|
88
|
+
Only the kit `createKit` returned may be closed — one from `as`,
|
|
89
|
+
`withSession` or a transaction shares those clients and throws.
|
|
90
|
+
|
|
91
|
+
## In a request
|
|
92
|
+
|
|
93
|
+
A kit is built once, at startup, and each request derives its own. Nothing
|
|
94
|
+
else has to be wired: no `getCollection` at the call site, no client passed
|
|
95
|
+
around.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// src/services/users.service.ts
|
|
99
|
+
import type { Kit } from '../db';
|
|
100
|
+
|
|
101
|
+
export class UserService {
|
|
102
|
+
constructor(private readonly kit: Kit) {}
|
|
103
|
+
|
|
104
|
+
list(page?: number) {
|
|
105
|
+
return this.kit.db.users.paginate({ page });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
create(input: { email: string }) {
|
|
109
|
+
return this.kit.db.users.create(input);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// src/middlewares/services.ts
|
|
116
|
+
import { tryObjectId } from '@nxgt/mongo';
|
|
117
|
+
import { createMiddleware } from 'hono/factory';
|
|
118
|
+
import type { Kit } from '../db';
|
|
119
|
+
import { UserService } from '../services/users.service';
|
|
120
|
+
|
|
121
|
+
export const provideServices = (kit: Kit) =>
|
|
122
|
+
createMiddleware(async (c, next) => {
|
|
123
|
+
const actor = tryObjectId(c.req.header('x-user-id'));
|
|
124
|
+
if (!actor) return c.json({ message: 'errors.unauthenticated' }, 401);
|
|
125
|
+
// One kit per request, stamping that user; the kit it came from is
|
|
126
|
+
// untouched, so a request's actor never leaks into the next.
|
|
127
|
+
c.set('services', { users: new UserService(kit.as(actor)) });
|
|
128
|
+
await next();
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// src/index.ts
|
|
134
|
+
import { createKit } from '@nxgt/mongo-kit';
|
|
135
|
+
import { buildApp } from './app';
|
|
136
|
+
import { config } from './db';
|
|
137
|
+
|
|
138
|
+
const kit = await createKit(config);
|
|
139
|
+
Bun.serve({ fetch: buildApp(kit).fetch });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A handler reads `c.get('services')` and never reaches the kit itself, so it
|
|
143
|
+
cannot write as somebody else and cannot close it.
|
|
144
|
+
|
|
145
|
+
## Signatures
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
function createKit<C>(config: KitConfig<C>): Promise<MongoKit<C>>;
|
|
149
|
+
|
|
150
|
+
type DbScope<C> = {
|
|
151
|
+
readonly [K in keyof CollectionsOf<C>]: TypedCollection<CollectionsOf<C>[K]>;
|
|
152
|
+
} & Db;
|
|
153
|
+
|
|
154
|
+
interface MongoKit<C> extends AsyncDisposable {
|
|
155
|
+
readonly db: SoleScope<C>;
|
|
156
|
+
readonly databases: { readonly [N in DbName<C>]: DbScope<CollectionsIn<C, N>> };
|
|
157
|
+
readonly clients: { readonly [N in DbName<C>]: MongoClient };
|
|
158
|
+
readonly actor: KitActor<C> | undefined;
|
|
159
|
+
readonly session: ClientSession | undefined;
|
|
160
|
+
as(actor: KitActor<C>): MongoKit<C>;
|
|
161
|
+
withSession(session: ClientSession | undefined): MongoKit<C>;
|
|
162
|
+
transaction<T>(
|
|
163
|
+
fn: (kit: MongoKit<C>) => Promise<T>,
|
|
164
|
+
options?: KitTransactionOptions<C>,
|
|
165
|
+
): Promise<T>;
|
|
166
|
+
sync(options?: SyncOptions): Promise<Record<DbName<C>, SyncReport[]>>;
|
|
167
|
+
close(): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Next
|
|
172
|
+
|
|
173
|
+
- [The actor, sessions and transactions](actor-and-transactions.md).
|
|
174
|
+
- [Syncing](sync.md).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# `discoverCollections`
|
|
2
|
+
|
|
3
|
+
Reads the collection definitions a glob matches, for a **script** that has no
|
|
4
|
+
kit to work from — a sync, a migration, a one-off run from the repository.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { connectMongo, syncCollections } from '@nxgt/mongo';
|
|
8
|
+
import { discoverCollections } from '@nxgt/mongo-kit';
|
|
9
|
+
|
|
10
|
+
const mongo = await connectMongo(process.env.MONGO_URI!);
|
|
11
|
+
const definitions = await discoverCollections({ glob: 'src/**/*.model.ts' });
|
|
12
|
+
await syncCollections(mongo.db, definitions);
|
|
13
|
+
await mongo.close();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It gives `@nxgt/mongo`'s `AnyCollectionDefinition[]`, sorted by path and with
|
|
17
|
+
each definition appearing once.
|
|
18
|
+
|
|
19
|
+
## It is for scripts, and for nothing else
|
|
20
|
+
|
|
21
|
+
- **It produces no types.** A glob is read at run time, so the compiler sees
|
|
22
|
+
nothing: everything it returns is an `AnyCollectionDefinition`.
|
|
23
|
+
- **It does not survive bundling.** A bundler cannot follow a glob, so the
|
|
24
|
+
matched files are not in the bundle and nothing is found.
|
|
25
|
+
- **It needs the Bun runtime**: the glob is `Bun.Glob`. A Node script gets
|
|
26
|
+
`ReferenceError: Bun is not defined`.
|
|
27
|
+
- **It imports every file it matches**, so their top level runs — and a model
|
|
28
|
+
file's top level registers its definition.
|
|
29
|
+
|
|
30
|
+
An application wires its collections with
|
|
31
|
+
`import * as collections from './models'`, which a bundler follows and the
|
|
32
|
+
compiler sees. See [Configuration](configuration.md).
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
| Option | Type | Default | Effect |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `glob` | `string` | — | Relative to `cwd`: `'src/models/*.model.ts'`. Required |
|
|
39
|
+
| `cwd` | `string` | `process.cwd()` | Where the glob starts |
|
|
40
|
+
| `export` | `string` | — | Read one export by name in each file. Without it, every export that is a definition is taken |
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// every definition each file exports
|
|
44
|
+
await discoverCollections({ glob: 'test/models/*.model.ts', cwd });
|
|
45
|
+
|
|
46
|
+
// only the export called `definition`, in each matched file
|
|
47
|
+
await discoverCollections({
|
|
48
|
+
glob: 'test/models/*.model.ts',
|
|
49
|
+
cwd,
|
|
50
|
+
export: 'definition',
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A glob that matches nothing gives `[]`.
|
|
55
|
+
|
|
56
|
+
## What it throws
|
|
57
|
+
|
|
58
|
+
All three are a [`KitError`](errors.md) with `code: 'DISCOVERY'`. The two
|
|
59
|
+
that are about a file carry its path on `key`; the missing-glob one has no
|
|
60
|
+
path yet, so its `key` is `undefined`:
|
|
61
|
+
|
|
62
|
+
- `discoverCollections: a glob is required` — `glob` missing or empty.
|
|
63
|
+
- `discoverCollections: <path> exports no definition named "<name>"` — with
|
|
64
|
+
`export`, a matched file that has no definition under that name. Without
|
|
65
|
+
`export`, such a file simply contributes nothing.
|
|
66
|
+
- `discoverCollections: <a> and <b> both define the collection "<name>"` —
|
|
67
|
+
two files describing one server collection.
|
|
68
|
+
|
|
69
|
+
## Signatures
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
interface DiscoverOptions {
|
|
73
|
+
glob: string;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
export?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function discoverCollections(
|
|
79
|
+
options: DiscoverOptions,
|
|
80
|
+
): Promise<AnyCollectionDefinition[]>;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Next
|
|
84
|
+
|
|
85
|
+
- [Syncing](sync.md) — the same step for an application that has a kit.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
`KitError` is what this package refuses — a configuration, a name or a call
|
|
4
|
+
that cannot work — with a `code` beside the sentence, so nothing has to match
|
|
5
|
+
the message text.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig, KitError } from '@nxgt/mongo-kit';
|
|
9
|
+
import * as collections from './models';
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
defineConfig({ uri: process.env.MONGO_URI!, collections });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error instanceof KitError) {
|
|
15
|
+
error.code; // 'CONFIG'
|
|
16
|
+
error.database; // 'default' — the database it is about, when one is named
|
|
17
|
+
error.key; // the config key, collection key or path, when one is
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Errors from the collections themselves — a duplicate key, a failed
|
|
24
|
+
validation, a missing document — are `@nxgt/mongo`'s `DataError` and its
|
|
25
|
+
subclasses, unchanged: `kit.db.users` *is* one of its collections. `KitError`
|
|
26
|
+
is only about the wiring.
|
|
27
|
+
|
|
28
|
+
## The codes
|
|
29
|
+
|
|
30
|
+
| `code` | Thrown by | When |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `CONFIG` | `defineConfig` | the configuration cannot work: no `uri` and no `client`, both at once, `clientOptions` beside a `client`, a `collections` with no definition in it, two keys on one server collection, `optionsFor` under a key nothing is wired under, or one of the four options the kit decides |
|
|
33
|
+
| `COLLISION` | `createKit` | a collection is wired under a name the driver's `Db` already has — `command`, `watch`, `collection`… — so it would be unreachable |
|
|
34
|
+
| `NO_DATABASE` | `transaction(fn, { on: '<name>' })` | this kit holds no database under that name; the message lists the ones it has. Reading `kit.databases.<name>` does **not** throw — an unknown key is plain `undefined` |
|
|
35
|
+
| `SEVERAL_DATABASES` | reading `kit.db` | the kit holds more than one database, so there is no "the" database to give |
|
|
36
|
+
| `TRANSACTION` | `transaction` | the kit holds several clients and the call named none, or it is already in a session and still passed `{ on }` |
|
|
37
|
+
| `DERIVED` | `close` | the kit came from `as`, `withSession` or a transaction: the clients are the root kit's |
|
|
38
|
+
| `DISCOVERY` | `discoverCollections` | the glob is missing, a matched file has no definition under the `export` asked for, or two files define the same server collection |
|
|
39
|
+
|
|
40
|
+
`code` is the field to switch on: it survives a build that ends up with two
|
|
41
|
+
copies of the package, which `instanceof` does not.
|
|
42
|
+
|
|
43
|
+
## What it carries
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
class KitError extends TypeError {
|
|
47
|
+
readonly code: KitErrorCode;
|
|
48
|
+
/** The database it is about, when one is named. */
|
|
49
|
+
readonly database: string | undefined;
|
|
50
|
+
/** The config key, the collection key or the path it is about. */
|
|
51
|
+
readonly key: string | undefined;
|
|
52
|
+
|
|
53
|
+
constructor(code: KitErrorCode, message: string, options?: KitErrorOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type KitErrorCode =
|
|
57
|
+
| 'CONFIG'
|
|
58
|
+
| 'COLLISION'
|
|
59
|
+
| 'NO_DATABASE'
|
|
60
|
+
| 'SEVERAL_DATABASES'
|
|
61
|
+
| 'TRANSACTION'
|
|
62
|
+
| 'DERIVED'
|
|
63
|
+
| 'DISCOVERY';
|
|
64
|
+
|
|
65
|
+
interface KitErrorOptions {
|
|
66
|
+
database?: string | undefined;
|
|
67
|
+
key?: string | undefined;
|
|
68
|
+
cause?: unknown;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`database` is the key the database is named by in the configuration —
|
|
73
|
+
`default` for a lone one — and `key` is the collection key, the config key or
|
|
74
|
+
the file path the refusal is about. Neither is ever a URI: a connection
|
|
75
|
+
string holds the password, and this package prints none.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { createKit, KitError } from '@nxgt/mongo-kit';
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await createKit(config);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof KitError && error.code === 'COLLISION') {
|
|
84
|
+
error.database; // 'main'
|
|
85
|
+
error.key; // 'command' — the export to rename
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## It is a `TypeError`
|
|
92
|
+
|
|
93
|
+
`KitError` extends **`TypeError`**, not `Error`, unlike `@nxgt/mongo`'s
|
|
94
|
+
`DataError` or `@nxgt/redis`'s `RedisError`. Every one of these is a call or
|
|
95
|
+
a configuration written wrong, which is what `TypeError` means — and this
|
|
96
|
+
package threw bare `TypeError`s before the class existed, so nothing that
|
|
97
|
+
already catches one stopped matching:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
try {
|
|
101
|
+
defineConfig({ databases: {} } as never);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
error instanceof KitError; // true
|
|
104
|
+
error instanceof TypeError; // true — still what it always was
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
What is new is the `code`, which a `catch` can switch on instead of reading
|
|
109
|
+
the sentence.
|
|
110
|
+
|
|
111
|
+
## Where each one comes from
|
|
112
|
+
|
|
113
|
+
Nothing below reaches a request handler in a working application: they are
|
|
114
|
+
start-up and wiring failures, and `defineConfig` is deliberately the earliest
|
|
115
|
+
of them.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createKit, defineConfig, KitError } from '@nxgt/mongo-kit';
|
|
119
|
+
import * as collections from './models';
|
|
120
|
+
|
|
121
|
+
// CONFIG — before anything connects.
|
|
122
|
+
defineConfig({ collections } as never);
|
|
123
|
+
// KitError: defineConfig: database "default" has neither a uri nor a client
|
|
124
|
+
|
|
125
|
+
// COLLISION — at createKit, against the driver's own Db.
|
|
126
|
+
await createKit(
|
|
127
|
+
defineConfig({ uri: process.env.MONGO_URI!, collections: { command: users } as never }),
|
|
128
|
+
);
|
|
129
|
+
// KitError: createKit: database "default" wires a collection under "command", …
|
|
130
|
+
|
|
131
|
+
// NO_DATABASE — a transaction named on a database this kit does not hold.
|
|
132
|
+
// The types refuse the name, so this is the call that came through an `any`,
|
|
133
|
+
// or from JavaScript. Reading `kit.databases.nowhere` gives `undefined`
|
|
134
|
+
// instead: only `on` looks a name up.
|
|
135
|
+
await kit.transaction(async () => {}, { on: 'nowhere' as never });
|
|
136
|
+
// KitError: This kit has no database "nowhere": it has "main", "analytics".
|
|
137
|
+
|
|
138
|
+
// SEVERAL_DATABASES — `kit.db` with more than one. Its type is `never`.
|
|
139
|
+
kit.db;
|
|
140
|
+
// KitError: kit.db: this kit has several databases. Read the one you mean, as `kit.databases.main`.
|
|
141
|
+
|
|
142
|
+
// TRANSACTION — several clients, and no `{ on }`.
|
|
143
|
+
await kit.transaction(async (tx) => { /* … */ });
|
|
144
|
+
// KitError: transaction: this kit holds more than one client, …
|
|
145
|
+
|
|
146
|
+
// DERIVED — closing a kit that `as` derived.
|
|
147
|
+
await kit.as(userId).close();
|
|
148
|
+
// KitError: close: this kit came from `as`, `withSession` or a transaction. …
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A name no database has, and `{ on: 'nowhere' }` with it, do not compile
|
|
152
|
+
either: the types refuse them where they are written. The run-time refusal is
|
|
153
|
+
what catches the call that arrived through an `any`, or from JavaScript — and
|
|
154
|
+
`kit.db` on a kit with several databases, whose type is already `never`.
|
|
155
|
+
|
|
156
|
+
## A start-up that reports instead of crashing
|
|
157
|
+
|
|
158
|
+
The useful thing to do with a `KitError` is to say which database and which
|
|
159
|
+
key, because that is what the fix needs:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { createKit, defineConfig, KitError } from '@nxgt/mongo-kit';
|
|
163
|
+
import * as collections from './models';
|
|
164
|
+
|
|
165
|
+
export async function startDatabase() {
|
|
166
|
+
try {
|
|
167
|
+
return await createKit(
|
|
168
|
+
defineConfig({ uri: process.env.MONGO_URI!, collections }),
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof KitError) {
|
|
172
|
+
console.error(
|
|
173
|
+
`mongo: ${error.code}` +
|
|
174
|
+
(error.database ? ` on "${error.database}"` : '') +
|
|
175
|
+
(error.key ? ` at "${error.key}"` : ''),
|
|
176
|
+
error.message,
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
throw error; // a driver error: a host that does not answer, a bad password
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
MongoDB's own refusal to connect is **not** a `KitError`: a host that does not
|
|
186
|
+
answer, a wrong password, a replica set with no primary are the driver's
|
|
187
|
+
errors, and they reach the caller unchanged from `createKit`. A database that
|
|
188
|
+
fails to open gives back every connection opened before it.
|
|
189
|
+
|
|
190
|
+
## Next
|
|
191
|
+
|
|
192
|
+
- [Configuration](configuration.md) — what `defineConfig` checks, key by key.
|
|
193
|
+
- [The `db` scope](db-scope.md) — `kit.db`, `kit.databases` and the names
|
|
194
|
+
that are refused.
|
|
195
|
+
- [The actor, sessions and transactions](actor-and-transactions.md) — `{ on }`,
|
|
196
|
+
and the kits that cannot be closed.
|
|
197
|
+
- [Troubleshooting](../troubleshooting.md) — the same errors, indexed by the
|
|
198
|
+
message you are staring at.
|