@owlmeans/mongo-resource 0.1.15 → 0.1.16-rc.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.
Files changed (53) hide show
  1. package/README.md +51 -6
  2. package/agent-meta/instructions/mongo-resource.instructions.md +44 -9
  3. package/agent-meta/manifest.json +2 -2
  4. package/agent-meta/skills/mongo-resource/SKILL.md +150 -17
  5. package/build/consts.d.ts +16 -0
  6. package/build/consts.d.ts.map +1 -1
  7. package/build/consts.js +16 -0
  8. package/build/consts.js.map +1 -1
  9. package/build/declarations.d.ts +11 -0
  10. package/build/declarations.d.ts.map +1 -0
  11. package/build/declarations.js +29 -0
  12. package/build/declarations.js.map +1 -0
  13. package/build/index.d.ts +3 -0
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js +3 -0
  16. package/build/index.js.map +1 -1
  17. package/build/resource.d.ts.map +1 -1
  18. package/build/resource.js +58 -23
  19. package/build/resource.js.map +1 -1
  20. package/build/types.d.ts +53 -2
  21. package/build/types.d.ts.map +1 -1
  22. package/build/utils/index.d.ts +2 -0
  23. package/build/utils/index.d.ts.map +1 -1
  24. package/build/utils/index.js +2 -0
  25. package/build/utils/index.js.map +1 -1
  26. package/build/utils/life-cycle.d.ts +25 -1
  27. package/build/utils/life-cycle.d.ts.map +1 -1
  28. package/build/utils/life-cycle.js +89 -11
  29. package/build/utils/life-cycle.js.map +1 -1
  30. package/build/utils/migrations.d.ts +24 -0
  31. package/build/utils/migrations.d.ts.map +1 -0
  32. package/build/utils/migrations.js +129 -0
  33. package/build/utils/migrations.js.map +1 -0
  34. package/build/utils/refs.d.ts +74 -0
  35. package/build/utils/refs.d.ts.map +1 -0
  36. package/build/utils/refs.js +197 -0
  37. package/build/utils/refs.js.map +1 -0
  38. package/build/utils/schema.d.ts +8 -0
  39. package/build/utils/schema.d.ts.map +1 -1
  40. package/build/utils/schema.js +25 -0
  41. package/build/utils/schema.js.map +1 -1
  42. package/package.json +5 -5
  43. package/src/consts.ts +20 -0
  44. package/src/declarations.ts +42 -0
  45. package/src/index.ts +4 -1
  46. package/src/resource.ts +76 -28
  47. package/src/types.ts +58 -2
  48. package/src/utils/index.ts +2 -0
  49. package/src/utils/life-cycle.ts +117 -15
  50. package/src/utils/migrations.ts +171 -0
  51. package/src/utils/refs.ts +240 -0
  52. package/src/utils/schema.ts +32 -0
  53. package/tests/refs.spec.ts +95 -0
package/README.md CHANGED
@@ -4,9 +4,11 @@ MongoDB-backed `Resource<T>` implementation — the primary database resource fo
4
4
 
5
5
  ## Overview
6
6
 
7
- - `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?)` — factory for MongoDB resources
8
- - `MongoResource<T>` — extends `Resource<T>` with MongoDB collection, indexing, and field encryption
9
- - Supports CRUD, list/pagination, AJV schema validation, and field-level locking (encryption)
7
+ - `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, collectionName?)` — factory for MongoDB resources
8
+ - `MongoResource<T>` — extends `Resource<T>` with MongoDB collection, indexing, field encryption, code migrations and ObjectId references
9
+ - Supports CRUD, list/pagination, AJV schema validation ($jsonSchema collection validators), and field-level locking (encryption)
10
+ - Declared references convert between the string ids records carry and the `ObjectId`s the collection stores — the same way `_id` already does
11
+ - Code migrations run automatically at resource initialization, tracked in a per-database `_owlmeans_migrations` ledger
10
12
  - Used for all persistent data models in server applications
11
13
 
12
14
  ## Installation
@@ -53,19 +55,61 @@ const list = await projects.list({ criteria: { entityId } })
53
55
 
54
56
  ## API
55
57
 
56
- ### `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?): T`
58
+ ### `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, collectionName?): T`
57
59
 
58
60
  Creates a MongoDB resource. `dbAlias` defaults to `DEFAULT_DB_ALIAS` (`'mongo'`).
61
+ `collectionName` overrides the physical collection name (otherwise `resourcePrefix + alias`).
62
+ Pass the maker itself as the 4th argument so `schema`/`index()` survive context switches;
63
+ `migration()`/`reference()` survive regardless (module-scope declarations keyed by alias).
59
64
 
60
65
  ### `MongoResource<T>`
61
66
 
62
- Extends `Resource<T>` with:
67
+ Extends `Resource<T>` (and the shared `MigratableResource<MongoTx>` capability) with:
63
68
  - `collection: Collection` — MongoDB collection
64
- - `db(): Promise<Db>` get the MongoDB database
69
+ - `db(): Promise<Db>` / `client(): Promise<MongoClient>`
65
70
  - `index(name, spec, options?): this` — define a collection index
71
+ - `reference(field, targetAlias?): this` / `references()` — declare that a field stores another record's id (see below)
72
+ - `migration(name, apply, stage?): this` / `migrations()` — register a code migration (see below)
66
73
  - `lock(record, fields?)` / `unlock(record, fields?)` — encrypt/decrypt secure fields
67
74
  - `getDefaults(): Partial<T>` — default values derived from schema
68
75
 
76
+ ### ObjectId references
77
+
78
+ `reference(field, targetAlias?)` declares that a record field references another record's id.
79
+ The resource then treats the field exactly like `_id`:
80
+
81
+ - Records and criteria carry **strings**; the collection stores **`ObjectId`s**. Conversion is
82
+ automatic on every read, write and lookup — including `$in`-style operator objects,
83
+ `$and`/`$or`/`$nor` branches and arrays of ids. `id` criteria are mapped onto `_id`.
84
+ - Writes are strict (a non-24-hex value throws `MisshapedRecord`); reads and criteria are
85
+ tolerant (a non-id value simply matches nothing).
86
+ - The field gets a mongo-level index (`ref_<field>`) automatically, unless an index with the
87
+ identical key pattern is already declared, and the collection validator declares it
88
+ `objectId`.
89
+ - Declaring a reference registers the system migration `$ref:<field>@1` (pre stage) that
90
+ converts pre-existing string ids in place — idempotent and interrupt-safe. On every boot
91
+ the collection is additionally probed for convertible strings and repaired if the ledger
92
+ and the data disagree (the double check). Conversion bypasses document validation, which
93
+ requires the `bypassDocumentValidation` privilege (`dbOwner`/`root` hold it).
94
+ - Only declare fields whose values really are another record's `id`. Business keys, composite
95
+ keys, external provider ids and slugs must stay strings — converting them corrupts data.
96
+ - Raw `resource.collection.*` access bypasses the conversion: marshal filter values with
97
+ `marshalReference(field, value)` and convert read-back ids to strings yourself.
98
+
99
+ ### Migrations
100
+
101
+ `migration(name, apply, stage?)` registers a code migration, applied once per database in
102
+ declaration order and recorded in the `_owlmeans_migrations` collection (one ledger per
103
+ database — an Entity-layer database tracks its own).
104
+
105
+ - `MigrationStage.Pre` runs before the validator/index update, `Post` after. On a collection
106
+ created by this very boot, registered migrations are **baselined** (recorded, not run).
107
+ - Bodies receive a `MongoTx` (`db`, `collection`, `use(alias)`, `ref(alias)`) and must be
108
+ **idempotent** — multi-document transactions are unavailable on a standalone `mongod`, so
109
+ the ledger claims-then-completes and an interrupted body may re-run.
110
+ - The checksum fingerprints the body's source text: keep bodies at module scope; an edited
111
+ applied body raises `MigrationConflict` at boot.
112
+
69
113
  ### `Resource<T>` methods (all implemented)
70
114
 
71
115
  `get`, `load`, `create`, `update`, `save`, `delete`, `pick`, `list`
@@ -74,6 +118,7 @@ Extends `Resource<T>` with:
74
118
 
75
119
  - `DEFAULT_DB_ALIAS` — `'mongo'`
76
120
  - `DEFAULT_PAGE_SIZE` — `10`
121
+ - `DEF_MIGRATIONS_COLLECTION` — `'_owlmeans_migrations'`
77
122
 
78
123
  ## Related Packages
79
124
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "How to use @owlmeans/mongo-resource — MongoDB-backed Resource implementation. Builds a typed CRUD resource on top of @owlmeans/mongo."
2
+ description: "How to use @owlmeans/mongo-resource — MongoDB-backed Resource with AJV-schema validators, code migrations and ObjectId reference conversion. Use when defining a mongo resource, declaring record references, or writing mongo migrations."
3
3
  applyTo: "**/*.ts, **/*.tsx"
4
4
  ---
5
5
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -7,24 +7,59 @@ applyTo: "**/*.ts, **/*.tsx"
7
7
  # @owlmeans/mongo-resource
8
8
 
9
9
  **Layer:** Infra
10
- **Install:** `"@owlmeans/mongo-resource": "^0.1.15"` in `dependencies`
10
+ **Install:** `"@owlmeans/mongo-resource": "^0.1.16-rc.0"` in `dependencies` (peers `mongodb`, `ajv`)
11
11
 
12
12
  ## Key Exports
13
13
 
14
14
  | Export | Description |
15
15
  |--------|-------------|
16
- | `makeMongoResource<T>(options)` | MongoDB-backed Resource factory |
17
- | `MongoResource<T>` types | Resource interface |
18
- | Constants | Default collection prefix |
19
- | Helpers | Index creation, query helpers |
16
+ | `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, collectionName?)` | The resource factory (positional args, NOT an options object) |
17
+ | `MongoResource<T>` | `Resource<T>` + `collection`, `index`, `reference`/`references`, `migration`/`migrations`, `lock`/`unlock` |
18
+ | `MongoTx` | Migration façade: `db`, `collection`, `use(alias)`, `ref(alias)` |
19
+ | `marshalReference`, `demarshalReference`, `marshalCriteria`, `identityCriteria`, `isObjectIdHex` | Reference conversion layer reuse for raw driver access |
20
+ | `getDeclaration`, `resetDeclarations` | Module-scope migration/reference declarations per alias |
20
21
 
21
22
  ## Usage
22
23
 
23
24
  ```typescript
24
- import { makeMongoResource } from '@owlmeans/mongo-resource'
25
- context.registerResource(makeMongoResource<Project>({ alias: 'projects', collection: 'projects' }))
25
+ export const makeStoryResource: ResourceMaker<StoryRecord, StoryResource> = (dbAlias, serviceAlias) => {
26
+ const resource = makeMongoResource<StoryRecord, StoryResource>(RES_STORY, dbAlias, serviceAlias, makeStoryResource)
27
+ resource.schema = StorySchema
28
+ resource.reference('projectId', RES_PROJECT) // field stores another record's id
29
+ resource.index('code', { projectId: 1, code: 1 }, { sparse: true })
30
+ resource.migration('0001-backfill', async tx => { /* idempotent body */ })
31
+ return resource
32
+ }
26
33
  ```
27
34
 
35
+ ## ObjectId references
36
+
37
+ - `reference(field, targetAlias?)` — records/criteria carry strings, the collection stores
38
+ `ObjectId`s (converted like `_id`); the field gets an automatic index (`ref_<field>`)
39
+ unless an identical key pattern is already declared; the validator declares it `objectId`.
40
+ - Writes are strict (non-24-hex throws `MisshapedRecord`), reads/criteria tolerant. `id`
41
+ criteria map onto `_id`.
42
+ - Declaring one registers the system migration `$ref:<field>@1` (Pre) that converts existing
43
+ strings; the boot additionally probes the collection and repairs drift (the double check).
44
+ Any semantic edit to the shared conversion body must bump the `@N` suffix.
45
+ - NEVER declare business/external keys as references: `entityId`/`entity` slugs, composite
46
+ `profileId`, `credentials.userId` (external key — but `profile.userId` IS a reference),
47
+ Stripe/Cloudflare/GitHub ids, minted slugs/tokens.
48
+ - Raw `resource.collection.*` access bypasses conversion — marshal filters with
49
+ `marshalReference` and stringify read-back ids by hand.
50
+
51
+ ## Migrations
52
+
53
+ - `migration(name, apply, stage?)` — applied once per database in declaration order,
54
+ ledgered in `_owlmeans_migrations`. `Pre` runs before validator/index update, `Post` after.
55
+ Fresh collections baseline (record without running).
56
+ - No multi-document transactions (standalone mongod) → bodies MUST be idempotent; the ledger
57
+ claims-then-completes on a unique `(alias, name)` index.
58
+ - Checksums fingerprint the body's source text: keep bodies at module scope; an edited
59
+ applied body raises `MigrationConflict`.
60
+ - `migration()`/`reference()` survive `reinitializeContext` (module-scope declarations);
61
+ `schema`/`index()` survive only when the maker is passed as the 4th factory argument.
62
+
28
63
  ## Depends On
29
64
 
30
- - `@owlmeans/mongo`, `@owlmeans/resource`, `@owlmeans/server-context`
65
+ - `@owlmeans/mongo` (service), `@owlmeans/resource` (contracts + migration framework), `@owlmeans/server-context`
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@owlmeans/mongo-resource",
4
- "version": "0.1.15",
5
- "generatedAt": "2026-08-07T17:06:32.896Z",
4
+ "version": "0.1.16-rc.0",
5
+ "generatedAt": "2026-08-11T14:02:34.974Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mongo-resource
3
- description: How to use @owlmeans/mongo-resource — MongoDB-backed Resource implementation. Builds a typed CRUD resource on top of @owlmeans/mongo. Auto-invoked when defining a resource backed by MongoDB.
3
+ description: How to use @owlmeans/mongo-resource — MongoDB-backed Resource implementation with AJV-schema validators, code migrations and ObjectId reference conversion. Auto-invoked when defining a resource backed by MongoDB, declaring record references, or writing mongo migrations.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,32 +8,165 @@ user-invocable: false
8
8
  # @owlmeans/mongo-resource
9
9
 
10
10
  **Layer:** Infra
11
- **Install:** `"@owlmeans/mongo-resource": "^0.1.15"` in `dependencies`
11
+ **Install:** `"@owlmeans/mongo-resource": "^0.1.16-rc.0"` in `dependencies` (peers `mongodb`, `ajv`)
12
+
13
+ The Mongo counterpart of [[postgres-resource]]. A collection has no structure of its own, so
14
+ here the resource layer owns the *validator* (`$jsonSchema` from the AJV schema), the indexes,
15
+ the code migrations, and the string↔`ObjectId` conversion for `_id` **and every declared
16
+ reference**.
12
17
 
13
18
  ## Key Exports
14
19
 
15
20
  | Export | Description |
16
21
  |--------|-------------|
17
- | `makeMongoResource<T>(options)` | Factory for a MongoDB-backed Resource<T> |
18
- | `MongoResource<T>` types | Resource interface |
19
- | Constants | Default collection prefix |
20
- | Helpers | Index creation, query helpers |
22
+ | `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, collectionName?)` | The resource factory. Aliases default to `DEFAULT_DB_ALIAS` (`'mongo'`). `collectionName` overrides the collection (else `resourcePrefix + alias`). |
23
+ | `MongoResource<T>` | `Resource<T>` + `collection`, `index`, `reference`/`references`, `migration`/`migrations` (the shared `MigratableResource` capability), `lock`/`unlock`, `getDefaults`. |
24
+ | `MongoDbService`, `MongoTx` | Service contract implemented by `@owlmeans/mongo`; the façade handed to migrations (`db`, `collection`, `use(alias)`, `ref(alias)`). |
25
+ | `MongoReference`, `MongoRefOptions` | A declared ObjectId reference and the `reference()` options (`resource`, `noIndex`). |
26
+ | `marshalReference`, `demarshalReference`, `marshalCriteria`, `identityCriteria`, `isObjectIdHex` | The conversion layer — reuse these wherever raw driver access bypasses the resource. |
27
+ | `convertReferenceField`, `reconcileReferences`, `refMigrationName` | The system reference migration's machinery. |
28
+ | `makeMongoTx`, `makeMongoMigrationStore` | The migration store (ledger) implementation. |
29
+ | `getDeclaration`, `resetDeclarations` | Module-scope migration/reference declarations, keyed by alias. |
30
+ | `schemaToMongoSchema`, `applyReferenceTypes`, `mongoCollectionName`, `updateIndexes` | Validator compilation and lifecycle helpers (deep import `utils/`). |
31
+ | `DEFAULT_DB_ALIAS`, `DEFAULT_PAGE_SIZE`, `DEF_MIGRATIONS_COLLECTION` | Constants. |
21
32
 
22
- ## Usage
33
+ ## Usage — the maker pattern
23
34
 
24
35
  ```typescript
25
- import { makeMongoResource } from '@owlmeans/mongo-resource'
26
-
27
- context.registerResource(makeMongoResource<Project>({
28
- alias: 'projects',
29
- collection: 'projects',
30
- }))
36
+ export const makeProjectStoryResource: ResourceMaker<ProjectStoryRecord, ProjectStoryResource> =
37
+ (dbAlias, serviceAlias) => {
38
+ const resource = makeMongoResource<ProjectStoryRecord, ProjectStoryResource>(
39
+ RES_PROJECT_STORY, dbAlias, serviceAlias, makeProjectStoryResource
40
+ )
41
+ resource.schema = ProjectStorySchema
42
+ resource.reference('projectId', RES_PROJECT)
43
+ resource.index('code', { projectId: 1, code: 1 }, { sparse: true })
44
+ resource.migration('0001-backfill-code', async tx => { /* ... */ })
31
45
 
32
- // In a handler:
33
- const projects = ctx.getResource<Resource<Project>>('projects')
34
- await projects.create({ name: 'New', entityId })
46
+ return resource
47
+ }
48
+ context.registerResource(makeProjectStoryResource())
35
49
  ```
36
50
 
51
+ Pass the maker itself as the 4th argument — `reinitializeContext` re-runs it, which is what
52
+ carries `schema` and `index()` calls across context switches. `migration()` and `reference()`
53
+ survive regardless: they live in module-scope declarations keyed by alias, because losing one
54
+ silently loses a data transformation.
55
+
56
+ ## ObjectId references
57
+
58
+ A field that stores **another record's id** is declared with `reference(field, targetAlias?)`.
59
+ The resource then behaves for that field exactly as it does for `_id`:
60
+
61
+ - **Records and criteria carry strings; the collection stores `ObjectId`s.** Conversion is
62
+ automatic in `create`/`update`/`save` (write), `get`/`load`/`list`/`delete`/`pick` (read and
63
+ lookup), and in `list` criteria — including `$in`/`$ne`-style operator objects, `$and`/`$or`/
64
+ `$nor` branches, and arrays of ids (elementwise). `$regex`/`$type`-style operands are left
65
+ alone.
66
+ - **Writes are strict**: storing a non-24-hex value in a declared reference throws
67
+ `MisshapedRecord('ref:<field>')` — a silent string would reintroduce the mixed-type state.
68
+ Reads and criteria are tolerant: an unconverted legacy string comes back as-is; a non-id
69
+ criteria value simply matches nothing (the auth `userId ?? profileId` fallback relies on
70
+ this).
71
+ - **`id` criteria address `_id`.** Documents never store an `id` field, so `list({ id })` and
72
+ `load(x, 'id')` are mapped onto `_id` with conversion — before this mapping they silently
73
+ matched nothing.
74
+ - **The field is indexed** automatically (`ref_<field>`), unless `noIndex: true` or the
75
+ resource already declares an index with the identical key pattern (mongo forbids two
76
+ indexes over the same keys; a declared unique index wins).
77
+ - **The validator declares the field `objectId`** (nullable/array shapes carry over from the
78
+ AJV property) — after the switch a raw string write is rejected at the collection level.
79
+
80
+ ### The system migration and its double check
81
+
82
+ Declaring a reference registers `$ref:<field>@1` at `Pre` stage: an idempotent, interrupt-safe
83
+ `updateMany` that converts stored 24-hex strings (scalar or array elements) to `ObjectId`s and
84
+ leaves everything else untouched. On restart the ledger says whether it ran; **independently**,
85
+ after structure update the boot probes the collection for convertible strings and repairs any
86
+ drift (restored backup, legacy writer), logging a warning. Both paths run with
87
+ `bypassDocumentValidation` — the connection's user must hold that privilege (`dbOwner`/`root`
88
+ do).
89
+
90
+ The `@1` in the name is the body's version. The body is shared by every field, so its checksum
91
+ never distinguishes them — **any semantic edit to `convertReferenceField` must bump the
92
+ version suffix** in `refMigrationName`, or every already-applied ledger raises
93
+ `MigrationConflict` at boot.
94
+
95
+ ### What is NOT a reference — do not declare these
96
+
97
+ Only fields assigned from another record's `.id` qualify. Known traps from the live codebase:
98
+
99
+ - `entityId` / `entity` — IAM entity slug (also a Keycloak realm and a k8s namespace label)
100
+ - `profileId` — composite key `"{type}:{accountId}"`; `credentials.userId` — external
101
+ provider key `"{type}:{service}:{sub}"` (while `profile.userId` **is** a reference)
102
+ - Stripe ids (`externalId`, `productId`, `taxId`), Cloudflare `providerId`, GitHub numeric ids
103
+ - locally minted slugs/tokens (`linkId`, `alias`, `slug`, `code`, `credential`)
104
+
105
+ Converting one of these corrupts the collection and breaks unique indexes. When in doubt,
106
+ trace what the writer actually assigns.
107
+
108
+ ### Raw driver access bypasses all of this
109
+
110
+ `resource.collection.find/aggregate/findOneAndUpdate` see `ObjectId`s. Marshal filter values
111
+ with `marshalReference(field, value)` and convert read-back documents' reference fields (and
112
+ `_id`) to strings by hand — or better, stay on the resource API.
113
+
114
+ ## Migrations
115
+
116
+ `resource.migration(name, apply, stage?)` — the shared `MigratableResource` capability from
117
+ [[resource]]. Applied once per database, in declaration order, ledgered in
118
+ `_owlmeans_migrations` (one ledger per database, so an Entity-layer database tracks its own).
119
+
120
+ - `Pre` runs **before** the validator is updated and indexes reconcile; `Post` after. A `Pre`
121
+ body writes shapes the *old* validator allows; a `Post` body the *new* one.
122
+ - On a collection this boot just created, every registered migration is **baselined**
123
+ (recorded, not run) — a fresh collection is born at head.
124
+ - **No transactions**: a standalone `mongod` (the dev/CI target) rejects multi-document
125
+ transactions, so the ledger claims-then-completes — the unique `(alias, name)` index is the
126
+ mutual exclusion; a replica losing the race waits for the winner; a failed body withdraws
127
+ the claim so the next boot retries. Consequence: **write migration bodies idempotent** —
128
+ they may be interrupted and re-run.
129
+ - The checksum fingerprints the body's **source text**. Keep bodies at module scope; an edited
130
+ applied body raises `MigrationConflict`, a throwing one `MigrationError` and the boot
131
+ aborts. A body that closes over a loop variable fingerprints the wrapper — the trap
132
+ `createMigrationRegistry` documents.
133
+ - Inside a body, `tx.use(alias)` / `tx.ref(alias)` address other registered resources'
134
+ collections by alias — resolved from config, so registration order does not matter (unlike
135
+ Postgres `{{alias}}`).
136
+
137
+ ## Lifecycle order at `init()`
138
+
139
+ 1. probe for the collection
140
+ 2. absent → baseline all migrations; present → run `Pre` (system `$ref:` first if declared before app migrations)
141
+ 3. create collection (validator + indexes) or update validator + reconcile indexes
142
+ 4. present → run `Post`
143
+ 5. reconcile declared references (probe + repair — the double check)
144
+
145
+ ## Method semantics worth remembering
146
+
147
+ | Method | Semantics |
148
+ |---|---|
149
+ | `create` | refuses a caller-supplied id (`RecordExists`) |
150
+ | `update` | **replaces** the whole record (no merge) |
151
+ | `pick` | deletes the record it returns |
152
+ | `load`/`get` | rejects `opts.ttl` (`UnsupportedArgumentError`); second arg selects the lookup field |
153
+ | `list` | criteria go through reference conversion; documents never store `id` — use `id` criteria freely, they map to `_id` |
154
+ | `lock`/`unlock` | encrypt/decrypt `secure: true` schema fields via the db service |
155
+
156
+ ## Tests
157
+
158
+ Unit specs (conversion layer): `bun test ./tests` in this package — ungated. Integration
159
+ specs that build a real `ServerContext` live in `@owlmeans/mongo` (`migration.spec.ts`,
160
+ `references.spec.ts`), gated on `MONGO_URL` (see [[testing-integration]]); a dev port-forward
161
+ to the cluster mongo satisfies the checked-in `.env`.
162
+
37
163
  ## Depends On
38
164
 
39
- - `@owlmeans/mongo`, `@owlmeans/resource`, `@owlmeans/server-context`
165
+ - `@owlmeans/resource` · `@owlmeans/context` · `@owlmeans/server-context`
166
+ - peer `mongodb`, `ajv`
167
+
168
+ ## Related
169
+
170
+ - [[mongo]] — the connection service this resolves through
171
+ - [[resource]] — `Resource<T>`, `MigratableResource`, the migration framework, errors
172
+ - [[postgres-resource]] — the Postgres counterpart (structure reconciliation instead of validators)
package/build/consts.d.ts CHANGED
@@ -1,3 +1,19 @@
1
1
  export declare const DEFAULT_DB_ALIAS = "mongo";
2
2
  export declare const DEFAULT_PAGE_SIZE = 10;
3
+ /**
4
+ * Collection that records which code-registered migrations have already been applied.
5
+ *
6
+ * One ledger per database, which is the right boundary: `dbName()` already varies the
7
+ * database per Entity/User layer, so a tenant's migrations are tracked with the tenant's
8
+ * data and dropping the database drops the ledger with it.
9
+ */
10
+ export declare const DEF_MIGRATIONS_COLLECTION = "_owlmeans_migrations";
11
+ /**
12
+ * How long a replica waits for another replica's in-flight migration before giving up.
13
+ * Bounded because the alternative is a pod that hangs on boot with no diagnostic.
14
+ */
15
+ export declare const DEF_MIGRATION_WAIT = 60000;
16
+ export declare const DEF_MIGRATION_POLL = 250;
17
+ /** `E11000` — the unique index on `(alias, name)` rejecting a second replica's claim. */
18
+ export declare const MONGO_DUPLICATE_KEY = 11000;
3
19
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,eAAO,MAAM,iBAAiB,KAAK,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,eAAO,MAAM,iBAAiB,KAAK,CAAA;AAEnC;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAE/D;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAQ,CAAA;AAEvC,eAAO,MAAM,kBAAkB,MAAM,CAAA;AAErC,yFAAyF;AACzF,eAAO,MAAM,mBAAmB,QAAQ,CAAA"}
package/build/consts.js CHANGED
@@ -1,3 +1,19 @@
1
1
  export const DEFAULT_DB_ALIAS = 'mongo';
2
2
  export const DEFAULT_PAGE_SIZE = 10;
3
+ /**
4
+ * Collection that records which code-registered migrations have already been applied.
5
+ *
6
+ * One ledger per database, which is the right boundary: `dbName()` already varies the
7
+ * database per Entity/User layer, so a tenant's migrations are tracked with the tenant's
8
+ * data and dropping the database drops the ledger with it.
9
+ */
10
+ export const DEF_MIGRATIONS_COLLECTION = '_owlmeans_migrations';
11
+ /**
12
+ * How long a replica waits for another replica's in-flight migration before giving up.
13
+ * Bounded because the alternative is a pod that hangs on boot with no diagnostic.
14
+ */
15
+ export const DEF_MIGRATION_WAIT = 60000;
16
+ export const DEF_MIGRATION_POLL = 250;
17
+ /** `E11000` — the unique index on `(alias, name)` rejecting a second replica's claim. */
18
+ export const MONGO_DUPLICATE_KEY = 11000;
3
19
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAA;AAEnC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,sBAAsB,CAAA;AAE/D;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAA;AAEvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC,yFAAyF;AACzF,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { MigrationRegistry } from '@owlmeans/resource';
2
+ import type { MongoReference, MongoTx } from './types.js';
3
+ export interface MongoDeclaration {
4
+ migrations: MigrationRegistry<MongoTx>;
5
+ /** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
6
+ references: Map<string, MongoReference>;
7
+ }
8
+ export declare const getDeclaration: (alias: string) => MongoDeclaration;
9
+ /** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
10
+ export declare const resetDeclarations: (alias?: string) => void;
11
+ //# sourceMappingURL=declarations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../src/declarations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAA;IACtC,2FAA2F;IAC3F,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CACxC;AAcD,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,gBAQ9C,CAAA;AAED,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,IAOlD,CAAA"}
@@ -0,0 +1,29 @@
1
+ import { createMigrationRegistry } from '@owlmeans/resource';
2
+ /**
3
+ * Per-alias migration store, held at module scope rather than on the resource object.
4
+ *
5
+ * `reinitializeContext` rebuilds every resource from the maker, which drops anything a
6
+ * caller attached by chaining afterwards. For indexes that is survivable — they already
7
+ * exist in the database and `updateIndexes` only ever adds. Migrations are not: a layer
8
+ * switch points the resource at a *different* database, and a registry emptied by the
9
+ * rebuild would mean the entity database silently never gets the transformation. Keying
10
+ * by alias makes the declarations outlive any number of context switches.
11
+ */
12
+ const declarations = new Map();
13
+ export const getDeclaration = (alias) => {
14
+ let declaration = declarations.get(alias);
15
+ if (declaration == null) {
16
+ declaration = { migrations: createMigrationRegistry(), references: new Map() };
17
+ declarations.set(alias, declaration);
18
+ }
19
+ return declaration;
20
+ };
21
+ /** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
22
+ export const resetDeclarations = (alias) => {
23
+ if (alias == null) {
24
+ declarations.clear();
25
+ return;
26
+ }
27
+ declarations.delete(alias);
28
+ };
29
+ //# sourceMappingURL=declarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarations.js","sourceRoot":"","sources":["../src/declarations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAW5D;;;;;;;;;GASG;AACH,MAAM,YAAY,GAAkC,IAAI,GAAG,EAAE,CAAA;AAE7D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAoB,EAAE;IAChE,IAAI,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzC,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;QACxB,WAAW,GAAG,EAAE,UAAU,EAAE,uBAAuB,EAAW,EAAE,UAAU,EAAE,IAAI,GAAG,EAAE,EAAE,CAAA;QACvF,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IACtC,CAAC;IAED,OAAO,WAAW,CAAA;AACpB,CAAC,CAAA;AAED,8FAA8F;AAC9F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAQ,EAAE;IACxD,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,YAAY,CAAC,KAAK,EAAE,CAAA;QAEpB,OAAM;IACR,CAAC;IACD,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC5B,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export type * from './types.js';
2
2
  export * from './consts.js';
3
+ export * from './declarations.js';
4
+ export * from './utils/migrations.js';
5
+ export * from './utils/refs.js';
3
6
  export * from './resource.js';
4
7
  export * from './helper.js';
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
package/build/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from './consts.js';
2
+ export * from './declarations.js';
3
+ export * from './utils/migrations.js';
4
+ export * from './utils/refs.js';
2
5
  export * from './resource.js';
3
6
  export * from './helper.js';
4
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAErF,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,YAAY,CAAA;AAU/D,eAAO,MAAM,iBAAiB,GAC5B,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAEvE,OAAO,MAAM,EAAE,UAAS,MAAyB,EAAE,eAAc,MAAyB,EAC1F,qBAAqB,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,iBAAiB,MAAM,KAChE,CAyRF,CAAA"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAErF,OAAO,KAAK,EAAmD,aAAa,EAAW,MAAM,YAAY,CAAA;AAezG,eAAO,MAAM,iBAAiB,GAC5B,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAEvE,OAAO,MAAM,EAAE,UAAS,MAAyB,EAAE,eAAc,MAAyB,EAC1F,qBAAqB,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,iBAAiB,MAAM,KAChE,CAuTF,CAAA"}