@owlmeans/mongo-resource 0.1.15 → 0.1.16
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 +54 -10
- package/agent-meta/manifest.json +4 -11
- package/agent-meta/skills/mongo-resource/SKILL.md +150 -17
- package/build/consts.d.ts +16 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +16 -0
- package/build/consts.js.map +1 -1
- package/build/declarations.d.ts +11 -0
- package/build/declarations.d.ts.map +1 -0
- package/build/declarations.js +29 -0
- package/build/declarations.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +58 -23
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +53 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils/index.d.ts +2 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +2 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/life-cycle.d.ts +25 -1
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +89 -11
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/migrations.d.ts +24 -0
- package/build/utils/migrations.d.ts.map +1 -0
- package/build/utils/migrations.js +129 -0
- package/build/utils/migrations.js.map +1 -0
- package/build/utils/refs.d.ts +74 -0
- package/build/utils/refs.d.ts.map +1 -0
- package/build/utils/refs.js +197 -0
- package/build/utils/refs.js.map +1 -0
- package/build/utils/schema.d.ts +8 -0
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +25 -0
- package/build/utils/schema.js.map +1 -1
- package/package.json +5 -5
- package/src/consts.ts +20 -0
- package/src/declarations.ts +42 -0
- package/src/index.ts +4 -1
- package/src/resource.ts +76 -28
- package/src/types.ts +58 -2
- package/src/utils/index.ts +2 -0
- package/src/utils/life-cycle.ts +117 -15
- package/src/utils/migrations.ts +171 -0
- package/src/utils/refs.ts +240 -0
- package/src/utils/schema.ts +32 -0
- package/tests/refs.spec.ts +95 -0
- package/agent-meta/instructions/mongo-resource.instructions.md +0 -30
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
|
|
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>`
|
|
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
|
|
|
@@ -83,10 +128,9 @@ Extends `Resource<T>` with:
|
|
|
83
128
|
<!-- owlmeans:agent-guidance:start -->
|
|
84
129
|
## Agent guidance
|
|
85
130
|
|
|
86
|
-
This package ships embedded
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
(`.claude/skills/` and `.github/instructions/`):
|
|
131
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
132
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
133
|
+
your project's skill store (`.agents/skills/`):
|
|
90
134
|
|
|
91
135
|
```sh
|
|
92
136
|
npx @owlmeans/agent-skills
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/mongo-resource",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-08-
|
|
4
|
+
"version": "0.1.16",
|
|
5
|
+
"generatedAt": "2026-08-14T10:14:48.849Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -10,14 +10,7 @@
|
|
|
10
10
|
"name": "mongo-resource",
|
|
11
11
|
"category": "package-specific",
|
|
12
12
|
"file": "skills/mongo-resource/SKILL.md",
|
|
13
|
-
"canonicalPath": ".
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"kind": "instruction",
|
|
17
|
-
"name": "mongo-resource",
|
|
18
|
-
"category": "package-specific",
|
|
19
|
-
"file": "instructions/mongo-resource.instructions.md",
|
|
20
|
-
"canonicalPath": ".github/instructions/mongo-resource.instructions.md"
|
|
13
|
+
"canonicalPath": ".agents/skills/mongo-resource/SKILL.md"
|
|
21
14
|
}
|
|
22
15
|
]
|
|
23
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mongo-resource
|
|
3
|
-
description: How to use @owlmeans/mongo-resource — MongoDB-backed Resource implementation
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/mongo-resource": "^0.1.16"` 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>(
|
|
18
|
-
| `MongoResource<T>`
|
|
19
|
-
|
|
|
20
|
-
|
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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/
|
|
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
|
package/build/consts.d.ts.map
CHANGED
|
@@ -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
|
package/build/consts.js.map
CHANGED
|
@@ -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
|
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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"}
|
package/build/resource.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"
|
|
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"}
|
package/build/resource.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context';
|
|
2
2
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE } from './consts.js';
|
|
3
|
+
import { MigrationStage } from '@owlmeans/resource';
|
|
3
4
|
import { initializeCollection } from './utils/life-cycle.js';
|
|
5
|
+
import { getDeclaration } from './declarations.js';
|
|
4
6
|
import { ObjectId } from 'mongodb';
|
|
5
7
|
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed, prepareListOptions } from '@owlmeans/resource';
|
|
6
8
|
import { getSchemaSecureFeilds } from './helper.js';
|
|
9
|
+
import { demarshalRefs, identityCriteria, makeRefMigration, marshalCriteria, marshalReference, refMigrationName } from './utils/refs.js';
|
|
7
10
|
export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlias = DEFAULT_DB_ALIAS, makeCustomResource, collectionName) => {
|
|
8
11
|
const location = `mongo-resource:${alias}`;
|
|
12
|
+
/**
|
|
13
|
+
* Live view — references may be declared after the resource is built, and the
|
|
14
|
+
* declarations are module scoped so they survive `reinitializeContext`.
|
|
15
|
+
*/
|
|
16
|
+
const refs = () => getDeclaration(alias).references;
|
|
17
|
+
const demarshal = (record) => {
|
|
18
|
+
record.id = record._id instanceof ObjectId ? record._id.toString() : record._id;
|
|
19
|
+
delete record._id;
|
|
20
|
+
return demarshalRefs(record, refs());
|
|
21
|
+
};
|
|
9
22
|
const resource = appendContextual(alias, {
|
|
10
23
|
get: async (id, field, opts) => {
|
|
11
24
|
const record = await resource.load(id, field, opts);
|
|
@@ -20,14 +33,12 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
20
33
|
field = field.field;
|
|
21
34
|
}
|
|
22
35
|
field = field ?? '_id';
|
|
23
|
-
const criteria = '_id' === field ? new ObjectId(id) : id;
|
|
24
36
|
if (opts?.ttl != null) {
|
|
25
37
|
throw new UnsupportedArgumentError('ttl');
|
|
26
38
|
}
|
|
27
|
-
const record = await resource.collection.findOne(
|
|
39
|
+
const record = await resource.collection.findOne(identityCriteria(field, id, refs()));
|
|
28
40
|
if (record != null) {
|
|
29
|
-
record
|
|
30
|
-
delete record._id;
|
|
41
|
+
demarshal(record);
|
|
31
42
|
}
|
|
32
43
|
return record;
|
|
33
44
|
},
|
|
@@ -47,12 +58,11 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
47
58
|
throw new MisshapedRecord('id');
|
|
48
59
|
}
|
|
49
60
|
const original = await resource.get(id, field);
|
|
50
|
-
const criteria = '_id' === field ? new ObjectId(id) : id;
|
|
51
61
|
const replace = { ...record, _id: new ObjectId(original.id) };
|
|
52
62
|
if (replace.id != null) {
|
|
53
63
|
delete replace.id;
|
|
54
64
|
}
|
|
55
|
-
const result = await resource.collection.replaceOne(
|
|
65
|
+
const result = await resource.collection.replaceOne(identityCriteria(field, id, refs()), _prepareValues(replace, resource.schema, refs()));
|
|
56
66
|
if (!result.acknowledged) {
|
|
57
67
|
throw new RecordUpdateFailed(`${field}:${id}`);
|
|
58
68
|
}
|
|
@@ -90,7 +100,7 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
90
100
|
}
|
|
91
101
|
const result = await resource.collection.insertOne({
|
|
92
102
|
...resource.getDefaults(),
|
|
93
|
-
..._prepareValues(record, resource.schema)
|
|
103
|
+
..._prepareValues(record, resource.schema, refs())
|
|
94
104
|
});
|
|
95
105
|
if (!result.acknowledged) {
|
|
96
106
|
throw new RecordUpdateFailed(`creation`);
|
|
@@ -124,8 +134,7 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
124
134
|
if (id == null) {
|
|
125
135
|
throw new MisshapedRecord('id');
|
|
126
136
|
}
|
|
127
|
-
const
|
|
128
|
-
const result = await resource.collection.deleteOne({ [field]: criteria });
|
|
137
|
+
const result = await resource.collection.deleteOne(identityCriteria(field, _id, refs()));
|
|
129
138
|
if (!result.acknowledged || result.deletedCount === 0) {
|
|
130
139
|
return null;
|
|
131
140
|
}
|
|
@@ -140,7 +149,7 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
140
149
|
},
|
|
141
150
|
list: async (criteria, opts) => {
|
|
142
151
|
const options = prepareListOptions(DEFAULT_PAGE_SIZE, criteria, opts);
|
|
143
|
-
criteria = options.criteria;
|
|
152
|
+
criteria = marshalCriteria(options.criteria, refs()) ?? {};
|
|
144
153
|
const pager = options.pager ?? {};
|
|
145
154
|
const size = pager?.size ?? DEFAULT_PAGE_SIZE;
|
|
146
155
|
const total = await resource.collection.countDocuments(criteria);
|
|
@@ -158,12 +167,7 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
158
167
|
}
|
|
159
168
|
const items = await cursor.toArray();
|
|
160
169
|
return {
|
|
161
|
-
pager, items: items.map(item => {
|
|
162
|
-
const _item = { ...item };
|
|
163
|
-
_item.id = item._id.toString();
|
|
164
|
-
delete _item._id;
|
|
165
|
-
return _item;
|
|
166
|
-
})
|
|
170
|
+
pager, items: items.map(item => demarshal({ ...item }))
|
|
167
171
|
};
|
|
168
172
|
},
|
|
169
173
|
lock: async (record, fields) => {
|
|
@@ -200,7 +204,30 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
200
204
|
resource.indexes = resource.indexes ?? [];
|
|
201
205
|
resource.indexes.push({ name, index, options });
|
|
202
206
|
return resource;
|
|
203
|
-
}
|
|
207
|
+
},
|
|
208
|
+
/**
|
|
209
|
+
* `migration` and `reference` are `this`-returning in the interface, which an object
|
|
210
|
+
* literal can't express — hence the member level casts: the implementations return
|
|
211
|
+
* the closed over `resource`, which is that very object.
|
|
212
|
+
*/
|
|
213
|
+
migration: ((name, apply, stage) => {
|
|
214
|
+
getDeclaration(alias).migrations.register(name, apply, stage);
|
|
215
|
+
return resource;
|
|
216
|
+
}),
|
|
217
|
+
migrations: () => getDeclaration(alias).migrations,
|
|
218
|
+
reference: ((field, opts) => {
|
|
219
|
+
const declaration = getDeclaration(alias);
|
|
220
|
+
const options = typeof opts === 'string' ? { resource: opts } : opts ?? {};
|
|
221
|
+
declaration.references.set(field, { field, resource: options.resource, noIndex: options.noIndex });
|
|
222
|
+
/**
|
|
223
|
+
* The system migration that converts the field's pre-existing string ids. Registered
|
|
224
|
+
* here rather than at init so it precedes migrations the app declares after its
|
|
225
|
+
* `reference()` calls — the field's type contract is the foundation those build on.
|
|
226
|
+
*/
|
|
227
|
+
declaration.migrations.register(refMigrationName(field), makeRefMigration(field), MigrationStage.Pre);
|
|
228
|
+
return resource;
|
|
229
|
+
}),
|
|
230
|
+
references: () => [...getDeclaration(alias).references.values()]
|
|
204
231
|
});
|
|
205
232
|
// Explicit collection name override (decoupled from the registration alias, which may
|
|
206
233
|
// contain characters that aren't valid in a collection name). Survives reinitializeContext
|
|
@@ -214,7 +241,7 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
214
241
|
await mongo.ready();
|
|
215
242
|
const db = await mongo.db(dbAlias);
|
|
216
243
|
const config = mongo.config(dbAlias);
|
|
217
|
-
resource.collection = await initializeCollection(db, config, resource);
|
|
244
|
+
resource.collection = await initializeCollection(db, config, resource, context);
|
|
218
245
|
};
|
|
219
246
|
resource.reinitializeContext = (context) => {
|
|
220
247
|
const resource = (makeCustomResource?.(dbAlias, serviceAlias)
|
|
@@ -224,15 +251,23 @@ export const makeMongoResource = (alias, dbAlias = DEFAULT_DB_ALIAS, serviceAlia
|
|
|
224
251
|
};
|
|
225
252
|
return resource;
|
|
226
253
|
};
|
|
227
|
-
const _prepareValues = (obj, schema) => {
|
|
254
|
+
const _prepareValues = (obj, schema, refs) => {
|
|
255
|
+
/**
|
|
256
|
+
* Declared references convert independently of the schema — the schema is optional,
|
|
257
|
+
* and where it exists it declares these fields as strings, whose coercion below would
|
|
258
|
+
* undo the conversion.
|
|
259
|
+
*/
|
|
260
|
+
if (refs != null && refs.size > 0) {
|
|
261
|
+
obj = Object.fromEntries(Object.entries(obj).map(([key, value]) => refs.has(key) ? [key, marshalReference(key, value)] : [key, value]));
|
|
262
|
+
}
|
|
228
263
|
// @TODO Validate keys from additional properties in the root
|
|
229
264
|
return schema != null ? Object.fromEntries(Object.entries(obj).map(([key, value]) => {
|
|
230
|
-
|
|
231
|
-
// How to properly transform them?
|
|
232
|
-
// What if _id isn't an Object Id?
|
|
233
|
-
if (key === '_id') {
|
|
265
|
+
if (key === '_id' && !(value instanceof ObjectId)) {
|
|
234
266
|
return [key, new ObjectId(value)];
|
|
235
267
|
}
|
|
268
|
+
if (key === '_id' || refs?.has(key)) {
|
|
269
|
+
return [key, value];
|
|
270
|
+
}
|
|
236
271
|
// A null/undefined value has nothing to coerce — pass it through for any declared
|
|
237
272
|
// type. Without this guard the object-map and array branches below call
|
|
238
273
|
// `Object.entries`/`.map` on `undefined` and throw (and `new Date(null)` would
|