@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.
@@ -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.