@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { MisshapedRecord } from '@owlmeans/resource';
|
|
2
|
+
import { ObjectId } from 'mongodb';
|
|
3
|
+
/**
|
|
4
|
+
* The only shape a stored reference is converted from. Deliberately stricter than
|
|
5
|
+
* `ObjectId.isValid`, which also accepts any 12 character string and would silently
|
|
6
|
+
* swallow short business keys.
|
|
7
|
+
*/
|
|
8
|
+
const HEX24 = /^[0-9a-fA-F]{24}$/;
|
|
9
|
+
export const isObjectIdHex = (value) => typeof value === 'string' && HEX24.test(value);
|
|
10
|
+
/**
|
|
11
|
+
* Write side of a declared reference: the string id a record carries becomes the
|
|
12
|
+
* `ObjectId` the collection stores. Arrays convert elementwise.
|
|
13
|
+
*
|
|
14
|
+
* Strict on purpose — a declared reference holding something that is not a mongo id is
|
|
15
|
+
* either a mis-declared field (should never have been a reference) or a bug at the call
|
|
16
|
+
* site, and storing it as a string would silently reintroduce the mixed type state this
|
|
17
|
+
* mechanism exists to remove.
|
|
18
|
+
*
|
|
19
|
+
* @throws {MisshapedRecord}
|
|
20
|
+
*/
|
|
21
|
+
export const marshalReference = (field, value) => {
|
|
22
|
+
if (value == null) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
if (value instanceof ObjectId) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map(item => marshalReference(field, item));
|
|
30
|
+
}
|
|
31
|
+
if (isObjectIdHex(value)) {
|
|
32
|
+
return new ObjectId(value);
|
|
33
|
+
}
|
|
34
|
+
throw new MisshapedRecord(`ref:${field}`);
|
|
35
|
+
};
|
|
36
|
+
/** Read side: `ObjectId` back to the string records carry. Tolerates not yet migrated strings. */
|
|
37
|
+
export const demarshalReference = (value) => {
|
|
38
|
+
if (value instanceof ObjectId) {
|
|
39
|
+
return value.toString();
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.map(demarshalReference);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
/** Convert every declared reference of a fetched document back to string ids, in place. */
|
|
47
|
+
export const demarshalRefs = (record, refs) => {
|
|
48
|
+
if (refs.size < 1) {
|
|
49
|
+
return record;
|
|
50
|
+
}
|
|
51
|
+
for (const field of refs.keys()) {
|
|
52
|
+
const value = record[field];
|
|
53
|
+
if (value != null) {
|
|
54
|
+
record[field] = demarshalReference(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return record;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Operators whose operand is never an id — a 24 hex string under `$regex` is a pattern,
|
|
61
|
+
* not a reference.
|
|
62
|
+
*/
|
|
63
|
+
const OPAQUE_OPERATORS = ['$regex', '$options', '$type', '$size', '$mod', '$exists', '$where'];
|
|
64
|
+
const LOGICAL_OPERATORS = ['$and', '$or', '$nor'];
|
|
65
|
+
const marshalCriteriaValue = (value) => {
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
return isObjectIdHex(value) ? new ObjectId(value) : value;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.map(marshalCriteriaValue);
|
|
71
|
+
}
|
|
72
|
+
if (value != null && typeof value === 'object' && !(value instanceof ObjectId) && !(value instanceof Date)) {
|
|
73
|
+
return Object.fromEntries(Object.entries(value).map(([operator, operand]) => OPAQUE_OPERATORS.includes(operator)
|
|
74
|
+
? [operator, operand]
|
|
75
|
+
: [operator, marshalCriteriaValue(operand)]));
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Convert list criteria the way records are converted: values addressed at `_id` or at a
|
|
81
|
+
* declared reference become `ObjectId`s, and the `id` alias records actually carry is
|
|
82
|
+
* mapped onto `_id` — documents never store `id`, so before this mapping such criteria
|
|
83
|
+
* silently matched nothing.
|
|
84
|
+
*
|
|
85
|
+
* Tolerant by design: a value that is not 24 hex passes through unconverted. Criteria are
|
|
86
|
+
* matched against the collection, and against an `ObjectId` typed field a stray string
|
|
87
|
+
* matches nothing — which is exactly what it matched before the field was converted.
|
|
88
|
+
*/
|
|
89
|
+
export const marshalCriteria = (criteria, refs) => {
|
|
90
|
+
if (criteria == null) {
|
|
91
|
+
return criteria;
|
|
92
|
+
}
|
|
93
|
+
return Object.fromEntries(Object.entries(criteria).map(([key, value]) => {
|
|
94
|
+
if (LOGICAL_OPERATORS.includes(key) && Array.isArray(value)) {
|
|
95
|
+
return [key, value.map(sub => marshalCriteria(sub, refs))];
|
|
96
|
+
}
|
|
97
|
+
if (key === 'id' || key === '_id') {
|
|
98
|
+
return ['_id', marshalCriteriaValue(value)];
|
|
99
|
+
}
|
|
100
|
+
if (refs.has(key)) {
|
|
101
|
+
return [key, marshalCriteriaValue(value)];
|
|
102
|
+
}
|
|
103
|
+
return [key, value];
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Criteria for addressing a single record by a field — `get`/`load`/`update`/`delete`.
|
|
108
|
+
*
|
|
109
|
+
* `_id` keeps its historical strictness (an invalid id throws through the driver). The
|
|
110
|
+
* `id` alias and declared references convert tolerantly, so a caller probing a reference
|
|
111
|
+
* with a value that is not a mongo id gets "not found" rather than a throw.
|
|
112
|
+
*/
|
|
113
|
+
export const identityCriteria = (field, id, refs) => {
|
|
114
|
+
if (field === '_id') {
|
|
115
|
+
return { _id: new ObjectId(id) };
|
|
116
|
+
}
|
|
117
|
+
if (field === 'id') {
|
|
118
|
+
return { _id: isObjectIdHex(id) ? new ObjectId(id) : id };
|
|
119
|
+
}
|
|
120
|
+
if (refs.has(field)) {
|
|
121
|
+
return { [field]: isObjectIdHex(id) ? new ObjectId(id) : id };
|
|
122
|
+
}
|
|
123
|
+
return { [field]: id };
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Name of the system migration that converts a reference field's stored strings.
|
|
127
|
+
*
|
|
128
|
+
* The `@1` is the body's version: the shared body below fingerprints identically for
|
|
129
|
+
* every field, so an edit to it would raise `MigrationConflict` against every ledger on
|
|
130
|
+
* the next boot. Any semantic change to {@link convertReferenceField} must bump this
|
|
131
|
+
* suffix instead — the old name stays applied, the new one runs (idempotently) once.
|
|
132
|
+
*/
|
|
133
|
+
export const refMigrationName = (field) => `$ref:${field}@1`;
|
|
134
|
+
/** Ledger registered body of the system reference migration. */
|
|
135
|
+
export const makeRefMigration = (field) => async (tx) => {
|
|
136
|
+
await convertReferenceField(tx.collection, field);
|
|
137
|
+
};
|
|
138
|
+
const convertScalar = (path) => ({
|
|
139
|
+
$cond: [
|
|
140
|
+
{
|
|
141
|
+
$and: [
|
|
142
|
+
{ $eq: [{ $type: path }, 'string'] },
|
|
143
|
+
{ $regexMatch: { input: path, regex: HEX24 } }
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{ $toObjectId: path },
|
|
147
|
+
path
|
|
148
|
+
]
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* Convert one reference field's stored string ids to `ObjectId`s — the body of the
|
|
152
|
+
* system migration and of the boot time reconciliation probe alike.
|
|
153
|
+
*
|
|
154
|
+
* Idempotent and interrupt safe: it matches only documents where the field (or one of
|
|
155
|
+
* its elements) is still a string, converts only values that are actually 24 hex, and
|
|
156
|
+
* leaves everything else exactly as it was. Safe to run concurrently from several
|
|
157
|
+
* replicas — a document converts once, the loser's filter no longer matches it.
|
|
158
|
+
*
|
|
159
|
+
* Validation is bypassed deliberately: at `Pre` stage the collection still carries the
|
|
160
|
+
* validator that declares the field a *string*, and after the switch a legacy document
|
|
161
|
+
* may violate the schema in unrelated ways — either would wedge the boot on a write
|
|
162
|
+
* that only makes the data more correct. (Bypassing requires the connection's user to
|
|
163
|
+
* hold the `bypassDocumentValidation` privilege — `dbOwner`/`root` do.)
|
|
164
|
+
*/
|
|
165
|
+
export const convertReferenceField = async (collection, field) => {
|
|
166
|
+
const result = await collection.updateMany(
|
|
167
|
+
/** An array valued field matches `$type: 'string'` when any element is a string. */
|
|
168
|
+
{ [field]: { $type: 'string' } }, [{
|
|
169
|
+
$set: {
|
|
170
|
+
[field]: {
|
|
171
|
+
$cond: [
|
|
172
|
+
{ $eq: [{ $type: `$${field}` }, 'array'] },
|
|
173
|
+
{ $map: { input: `$${field}`, as: 'ref', in: convertScalar('$$ref') } },
|
|
174
|
+
convertScalar(`$${field}`)
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}], { bypassDocumentValidation: true });
|
|
179
|
+
return result.modifiedCount;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* The second half of the double check the reference migration promises: the ledger says
|
|
183
|
+
* whether the migration ran; this probes whether the collection actually holds no
|
|
184
|
+
* convertible strings — and repairs it when the two disagree (a restored backup, a
|
|
185
|
+
* write from a legacy process, a ledger created by hand).
|
|
186
|
+
*/
|
|
187
|
+
export const reconcileReferences = async (collection, refs, alias) => {
|
|
188
|
+
for (const ref of refs) {
|
|
189
|
+
const remnant = await collection.findOne({ [ref.field]: { $type: 'string', $regex: HEX24 } }, { projection: { _id: 1 } });
|
|
190
|
+
if (remnant != null) {
|
|
191
|
+
const converted = await convertReferenceField(collection, ref.field);
|
|
192
|
+
console.warn(`@owlmeans/mongo-resource: ${alias}.${ref.field} held string ids outside the migration`
|
|
193
|
+
+ ` ledger — converted ${converted} document(s)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=refs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refs.js","sourceRoot":"","sources":["../../src/utils/refs.ts"],"names":[],"mappings":"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"}
|
package/build/utils/schema.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { AnySchema } from 'ajv';
|
|
2
2
|
import type { Document } from 'mongodb';
|
|
3
|
+
import type { MongoReference } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Declared references are stored as `ObjectId`s while the AJV schema — which describes
|
|
6
|
+
* the records the app exchanges — keeps calling them strings. The collection validator
|
|
7
|
+
* describes what's stored, so the reference fields are overridden here after the plain
|
|
8
|
+
* conversion. Nullability and array shape carry over from the declared property.
|
|
9
|
+
*/
|
|
10
|
+
export declare const applyReferenceTypes: (mongoSchema: Document, schema: AnySchema, refs: MongoReference[]) => Document;
|
|
3
11
|
export declare const schemaToMongoSchema: (schema: AnySchema) => Document;
|
|
4
12
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,KAAK,CAAA;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEvC,eAAO,MAAM,mBAAmB,GAAI,QAAQ,SAAS,KAAG,QAmCvD,CAAA"}
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,KAAK,CAAA;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,aAAa,QAAQ,EAAE,QAAQ,SAAS,EAAE,MAAM,cAAc,EAAE,KAC/D,QAoBF,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,QAAQ,SAAS,KAAG,QAmCvD,CAAA"}
|
package/build/utils/schema.js
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared references are stored as `ObjectId`s while the AJV schema — which describes
|
|
3
|
+
* the records the app exchanges — keeps calling them strings. The collection validator
|
|
4
|
+
* describes what's stored, so the reference fields are overridden here after the plain
|
|
5
|
+
* conversion. Nullability and array shape carry over from the declared property.
|
|
6
|
+
*/
|
|
7
|
+
export const applyReferenceTypes = (mongoSchema, schema, refs) => {
|
|
8
|
+
if (mongoSchema.properties == null || refs.length < 1) {
|
|
9
|
+
return mongoSchema;
|
|
10
|
+
}
|
|
11
|
+
const properties = schema.properties ?? {};
|
|
12
|
+
for (const ref of refs) {
|
|
13
|
+
const declared = properties[ref.field];
|
|
14
|
+
if (declared == null || mongoSchema.properties[ref.field] == null) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
mongoSchema.properties[ref.field] = declared.type === 'array'
|
|
18
|
+
? {
|
|
19
|
+
bsonType: declared.nullable ? ['array', 'null'] : 'array',
|
|
20
|
+
items: { bsonType: 'objectId' }
|
|
21
|
+
}
|
|
22
|
+
: { bsonType: declared.nullable ? ['objectId', 'null'] : 'objectId' };
|
|
23
|
+
}
|
|
24
|
+
return mongoSchema;
|
|
25
|
+
};
|
|
1
26
|
export const schemaToMongoSchema = (schema) => {
|
|
2
27
|
const _schema = schema;
|
|
3
28
|
if (_schema.type !== 'object') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/mongo-resource",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"mongodb": "*"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@owlmeans/context": "^0.1.
|
|
30
|
-
"@owlmeans/resource": "^0.1.
|
|
31
|
-
"@owlmeans/server-context": "^0.1.
|
|
29
|
+
"@owlmeans/context": "^0.1.16",
|
|
30
|
+
"@owlmeans/resource": "^0.1.16",
|
|
31
|
+
"@owlmeans/server-context": "^0.1.16"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@owlmeans/dep-config": "workspace:*",
|
|
35
|
-
"@owlmeans/test-integration": "^0.1.
|
|
35
|
+
"@owlmeans/test-integration": "^0.1.16",
|
|
36
36
|
"@types/bun": "^1.3.14",
|
|
37
37
|
"@types/node": "^26.1.0",
|
|
38
38
|
"mongodb": "^6.9.0",
|
package/src/consts.ts
CHANGED
|
@@ -2,3 +2,23 @@
|
|
|
2
2
|
export const DEFAULT_DB_ALIAS = 'mongo'
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_PAGE_SIZE = 10
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collection that records which code-registered migrations have already been applied.
|
|
8
|
+
*
|
|
9
|
+
* One ledger per database, which is the right boundary: `dbName()` already varies the
|
|
10
|
+
* database per Entity/User layer, so a tenant's migrations are tracked with the tenant's
|
|
11
|
+
* data and dropping the database drops the ledger with it.
|
|
12
|
+
*/
|
|
13
|
+
export const DEF_MIGRATIONS_COLLECTION = '_owlmeans_migrations'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How long a replica waits for another replica's in-flight migration before giving up.
|
|
17
|
+
* Bounded because the alternative is a pod that hangs on boot with no diagnostic.
|
|
18
|
+
*/
|
|
19
|
+
export const DEF_MIGRATION_WAIT = 60000
|
|
20
|
+
|
|
21
|
+
export const DEF_MIGRATION_POLL = 250
|
|
22
|
+
|
|
23
|
+
/** `E11000` — the unique index on `(alias, name)` rejecting a second replica's claim. */
|
|
24
|
+
export const MONGO_DUPLICATE_KEY = 11000
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createMigrationRegistry } from '@owlmeans/resource'
|
|
2
|
+
import type { MigrationRegistry } from '@owlmeans/resource'
|
|
3
|
+
|
|
4
|
+
import type { MongoReference, MongoTx } from './types.js'
|
|
5
|
+
|
|
6
|
+
export interface MongoDeclaration {
|
|
7
|
+
migrations: MigrationRegistry<MongoTx>
|
|
8
|
+
/** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
|
|
9
|
+
references: Map<string, MongoReference>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-alias migration store, held at module scope rather than on the resource object.
|
|
14
|
+
*
|
|
15
|
+
* `reinitializeContext` rebuilds every resource from the maker, which drops anything a
|
|
16
|
+
* caller attached by chaining afterwards. For indexes that is survivable — they already
|
|
17
|
+
* exist in the database and `updateIndexes` only ever adds. Migrations are not: a layer
|
|
18
|
+
* switch points the resource at a *different* database, and a registry emptied by the
|
|
19
|
+
* rebuild would mean the entity database silently never gets the transformation. Keying
|
|
20
|
+
* by alias makes the declarations outlive any number of context switches.
|
|
21
|
+
*/
|
|
22
|
+
const declarations: Map<string, MongoDeclaration> = new Map()
|
|
23
|
+
|
|
24
|
+
export const getDeclaration = (alias: string): MongoDeclaration => {
|
|
25
|
+
let declaration = declarations.get(alias)
|
|
26
|
+
if (declaration == null) {
|
|
27
|
+
declaration = { migrations: createMigrationRegistry<MongoTx>(), references: new Map() }
|
|
28
|
+
declarations.set(alias, declaration)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return declaration
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
|
|
35
|
+
export const resetDeclarations = (alias?: string): void => {
|
|
36
|
+
if (alias == null) {
|
|
37
|
+
declarations.clear()
|
|
38
|
+
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
declarations.delete(alias)
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
export type * from './types.js'
|
|
3
3
|
export * from './consts.js'
|
|
4
|
+
export * from './declarations.js'
|
|
5
|
+
export * from './utils/migrations.js'
|
|
6
|
+
export * from './utils/refs.js'
|
|
4
7
|
export * from './resource.js'
|
|
5
|
-
export * from './helper.js'
|
|
8
|
+
export * from './helper.js'
|
package/src/resource.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
2
|
import type { BasicContext, Contextual } from '@owlmeans/context'
|
|
3
3
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE } from './consts.js'
|
|
4
|
+
import { MigrationStage } from '@owlmeans/resource'
|
|
4
5
|
import type { ListCriteria, ResourceMaker, ResourceRecord } from '@owlmeans/resource'
|
|
5
6
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
6
|
-
import type { MongoDbService, MongoResource } from './types.js'
|
|
7
|
+
import type { MongoDbService, MongoReference, MongoRefOptions, MongoResource, MongoTx } from './types.js'
|
|
7
8
|
import { initializeCollection } from './utils/life-cycle.js'
|
|
9
|
+
import { getDeclaration } from './declarations.js'
|
|
8
10
|
import { ObjectId } from 'mongodb'
|
|
9
11
|
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed, prepareListOptions } from '@owlmeans/resource'
|
|
10
12
|
import type { JSONSchemaType } from 'ajv'
|
|
11
13
|
import { getSchemaSecureFeilds } from './helper.js'
|
|
14
|
+
import {
|
|
15
|
+
demarshalRefs, identityCriteria, makeRefMigration, marshalCriteria, marshalReference,
|
|
16
|
+
refMigrationName
|
|
17
|
+
} from './utils/refs.js'
|
|
12
18
|
|
|
13
19
|
type Config = ServerConfig
|
|
14
20
|
type Context<C extends Config = Config> = ServerContext<C>
|
|
@@ -21,6 +27,18 @@ export const makeMongoResource = <
|
|
|
21
27
|
): T => {
|
|
22
28
|
const location = `mongo-resource:${alias}`
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Live view — references may be declared after the resource is built, and the
|
|
32
|
+
* declarations are module scoped so they survive `reinitializeContext`.
|
|
33
|
+
*/
|
|
34
|
+
const refs = (): Map<string, MongoReference> => getDeclaration(alias).references
|
|
35
|
+
|
|
36
|
+
const demarshal = <Type extends ResourceRecord>(record: Type & { _id?: unknown }): Type => {
|
|
37
|
+
record.id = record._id instanceof ObjectId ? record._id.toString() : record._id as string
|
|
38
|
+
delete record._id
|
|
39
|
+
return demarshalRefs(record, refs())
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
const resource: T = appendContextual<T>(alias, {
|
|
25
43
|
get: async (id, field, opts) => {
|
|
26
44
|
const record = await resource.load(id, field, opts)
|
|
@@ -37,15 +55,13 @@ export const makeMongoResource = <
|
|
|
37
55
|
field = field.field
|
|
38
56
|
}
|
|
39
57
|
field = field ?? '_id'
|
|
40
|
-
const criteria = '_id' === field ? new ObjectId(id) : id
|
|
41
58
|
if (opts?.ttl != null) {
|
|
42
59
|
throw new UnsupportedArgumentError('ttl')
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
const record = await resource.collection.findOne(
|
|
62
|
+
const record = await resource.collection.findOne(identityCriteria(field, id, refs()))
|
|
46
63
|
if (record != null) {
|
|
47
|
-
record
|
|
48
|
-
delete (record as any)._id
|
|
64
|
+
demarshal(record)
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
return record
|
|
@@ -70,16 +86,14 @@ export const makeMongoResource = <
|
|
|
70
86
|
|
|
71
87
|
const original = await resource.get(id, field)
|
|
72
88
|
|
|
73
|
-
const criteria = '_id' === field ? new ObjectId(id) : id
|
|
74
|
-
|
|
75
89
|
const replace = { ...record, _id: new ObjectId(original.id) }
|
|
76
90
|
if (replace.id != null) {
|
|
77
91
|
delete replace.id
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
const result = await resource.collection.replaceOne(
|
|
81
|
-
|
|
82
|
-
_prepareValues(replace, resource.schema as JSONSchemaType<any
|
|
95
|
+
identityCriteria(field, id, refs()),
|
|
96
|
+
_prepareValues(replace, resource.schema as JSONSchemaType<any>, refs())
|
|
83
97
|
)
|
|
84
98
|
if (!result.acknowledged) {
|
|
85
99
|
throw new RecordUpdateFailed(`${field}:${id}`)
|
|
@@ -128,7 +142,7 @@ export const makeMongoResource = <
|
|
|
128
142
|
}
|
|
129
143
|
const result = await resource.collection.insertOne({
|
|
130
144
|
...resource.getDefaults(),
|
|
131
|
-
..._prepareValues(record, resource.schema as JSONSchemaType<any
|
|
145
|
+
..._prepareValues(record, resource.schema as JSONSchemaType<any>, refs())
|
|
132
146
|
})
|
|
133
147
|
|
|
134
148
|
if (!result.acknowledged) {
|
|
@@ -168,9 +182,7 @@ export const makeMongoResource = <
|
|
|
168
182
|
throw new MisshapedRecord('id')
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const result = await resource.collection.deleteOne({ [field]: criteria })
|
|
185
|
+
const result = await resource.collection.deleteOne(identityCriteria(field, _id as string, refs()))
|
|
174
186
|
if (!result.acknowledged || result.deletedCount === 0) {
|
|
175
187
|
return null
|
|
176
188
|
}
|
|
@@ -190,7 +202,7 @@ export const makeMongoResource = <
|
|
|
190
202
|
list: async (criteria, opts) => {
|
|
191
203
|
const options = prepareListOptions(DEFAULT_PAGE_SIZE, criteria, opts)
|
|
192
204
|
|
|
193
|
-
criteria = options.criteria
|
|
205
|
+
criteria = marshalCriteria(options.criteria, refs()) ?? {}
|
|
194
206
|
const pager = options.pager ?? {}
|
|
195
207
|
|
|
196
208
|
const size = pager?.size ?? DEFAULT_PAGE_SIZE
|
|
@@ -218,13 +230,7 @@ export const makeMongoResource = <
|
|
|
218
230
|
const items = await cursor.toArray()
|
|
219
231
|
|
|
220
232
|
return {
|
|
221
|
-
pager, items: items.map(item => {
|
|
222
|
-
const _item: R = { ...item } as any
|
|
223
|
-
_item.id = item._id.toString()
|
|
224
|
-
delete (_item as any)._id
|
|
225
|
-
|
|
226
|
-
return _item
|
|
227
|
-
})
|
|
233
|
+
pager, items: items.map(item => demarshal({ ...item } as unknown as R))
|
|
228
234
|
}
|
|
229
235
|
},
|
|
230
236
|
|
|
@@ -270,7 +276,35 @@ export const makeMongoResource = <
|
|
|
270
276
|
resource.indexes = resource.indexes ?? []
|
|
271
277
|
resource.indexes.push({ name, index, options })
|
|
272
278
|
return resource
|
|
273
|
-
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* `migration` and `reference` are `this`-returning in the interface, which an object
|
|
283
|
+
* literal can't express — hence the member level casts: the implementations return
|
|
284
|
+
* the closed over `resource`, which is that very object.
|
|
285
|
+
*/
|
|
286
|
+
migration: ((name: string, apply: (tx: MongoTx) => Promise<void>, stage?: MigrationStage) => {
|
|
287
|
+
getDeclaration(alias).migrations.register(name, apply, stage)
|
|
288
|
+
return resource
|
|
289
|
+
}) as T['migration'],
|
|
290
|
+
|
|
291
|
+
migrations: () => getDeclaration(alias).migrations,
|
|
292
|
+
|
|
293
|
+
reference: ((field: string, opts?: string | MongoRefOptions) => {
|
|
294
|
+
const declaration = getDeclaration(alias)
|
|
295
|
+
const options = typeof opts === 'string' ? { resource: opts } : opts ?? {}
|
|
296
|
+
declaration.references.set(field, { field, resource: options.resource, noIndex: options.noIndex })
|
|
297
|
+
/**
|
|
298
|
+
* The system migration that converts the field's pre-existing string ids. Registered
|
|
299
|
+
* here rather than at init so it precedes migrations the app declares after its
|
|
300
|
+
* `reference()` calls — the field's type contract is the foundation those build on.
|
|
301
|
+
*/
|
|
302
|
+
declaration.migrations.register(refMigrationName(field), makeRefMigration(field), MigrationStage.Pre)
|
|
303
|
+
|
|
304
|
+
return resource
|
|
305
|
+
}) as T['reference'],
|
|
306
|
+
|
|
307
|
+
references: () => [...getDeclaration(alias).references.values()]
|
|
274
308
|
} as Partial<T>)
|
|
275
309
|
|
|
276
310
|
// Explicit collection name override (decoupled from the registration alias, which may
|
|
@@ -286,7 +320,9 @@ export const makeMongoResource = <
|
|
|
286
320
|
await mongo.ready()
|
|
287
321
|
const db = await mongo.db(dbAlias)
|
|
288
322
|
const config = mongo.config(dbAlias)
|
|
289
|
-
resource.collection = await initializeCollection(
|
|
323
|
+
resource.collection = await initializeCollection(
|
|
324
|
+
db, config, resource as unknown as MongoResource<ResourceRecord>, context
|
|
325
|
+
)
|
|
290
326
|
}
|
|
291
327
|
|
|
292
328
|
resource.reinitializeContext = <Type extends Contextual>(context: BasicContext<Config>) => {
|
|
@@ -301,15 +337,27 @@ export const makeMongoResource = <
|
|
|
301
337
|
return resource
|
|
302
338
|
}
|
|
303
339
|
|
|
304
|
-
const _prepareValues = <T extends ResourceRecord>(
|
|
340
|
+
const _prepareValues = <T extends ResourceRecord>(
|
|
341
|
+
obj: T, schema?: JSONSchemaType<T>, refs?: Map<string, MongoReference>
|
|
342
|
+
): T => {
|
|
343
|
+
/**
|
|
344
|
+
* Declared references convert independently of the schema — the schema is optional,
|
|
345
|
+
* and where it exists it declares these fields as strings, whose coercion below would
|
|
346
|
+
* undo the conversion.
|
|
347
|
+
*/
|
|
348
|
+
if (refs != null && refs.size > 0) {
|
|
349
|
+
obj = Object.fromEntries(Object.entries(obj).map(([key, value]) =>
|
|
350
|
+
refs.has(key) ? [key, marshalReference(key, value)] : [key, value]
|
|
351
|
+
)) as T
|
|
352
|
+
}
|
|
305
353
|
// @TODO Validate keys from additional properties in the root
|
|
306
354
|
return schema != null ? Object.fromEntries(Object.entries(obj).map(([key, value]) => {
|
|
307
|
-
|
|
308
|
-
// How to properly transform them?
|
|
309
|
-
// What if _id isn't an Object Id?
|
|
310
|
-
if (key === '_id') {
|
|
355
|
+
if (key === '_id' && !(value instanceof ObjectId)) {
|
|
311
356
|
return [key, new ObjectId(value as string)]
|
|
312
357
|
}
|
|
358
|
+
if (key === '_id' || refs?.has(key)) {
|
|
359
|
+
return [key, value]
|
|
360
|
+
}
|
|
313
361
|
// A null/undefined value has nothing to coerce — pass it through for any declared
|
|
314
362
|
// type. Without this guard the object-map and array branches below call
|
|
315
363
|
// `Object.entries`/`.map` on `undefined` and throw (and `new Date(null)` would
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Resource, ResourceRecord, ResourceDbService, DbLocker, ResourceLocker, MigratableResource
|
|
3
|
+
} from '@owlmeans/resource'
|
|
2
4
|
import type { Collection, CreateIndexesOptions, Db, IndexSpecification, MongoClient } from 'mongodb'
|
|
3
5
|
import type { AnySchema } from 'ajv'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/**
|
|
8
|
+
* What a mongo migration is handed.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not a session or transaction: multi-document transactions require a replica
|
|
11
|
+
* set, and a standalone `mongod` — the usual development and CI target — rejects them
|
|
12
|
+
* outright. A migration therefore has to be written to tolerate being interrupted partway,
|
|
13
|
+
* which in practice means idempotent updates rather than read-then-write.
|
|
14
|
+
*/
|
|
15
|
+
export interface MongoTx {
|
|
16
|
+
db: Db
|
|
17
|
+
/** The owning resource's collection. */
|
|
18
|
+
collection: Collection
|
|
19
|
+
/** Another registered mongo resource's collection; omit the alias for the owning one. */
|
|
20
|
+
use: (alias?: string) => Collection
|
|
21
|
+
/** A collection *name*, for `$lookup.from` and other stages that take one rather than a handle. */
|
|
22
|
+
ref: (alias?: string) => string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A declared ObjectId reference — a record field that stores another record's id.
|
|
27
|
+
*
|
|
28
|
+
* The resource converts the field between the string ids records carry and the `ObjectId`
|
|
29
|
+
* the collection stores, exactly the way it already does for `_id`: strings in records and
|
|
30
|
+
* criteria, `ObjectId` on the wire. Declaring a reference also gives the field a mongo
|
|
31
|
+
* level index and registers the system migration that converts pre-existing string values.
|
|
32
|
+
*/
|
|
33
|
+
export interface MongoReference {
|
|
34
|
+
/** Top level record property holding the reference (a single id or an array of ids). */
|
|
35
|
+
field: string
|
|
36
|
+
/** Alias of the referenced resource. Informational — conversion never resolves it. */
|
|
37
|
+
resource?: string
|
|
38
|
+
/** Skip the automatic `{ [field]: 1 }` index. */
|
|
39
|
+
noIndex?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MongoRefOptions {
|
|
43
|
+
resource?: string
|
|
44
|
+
noIndex?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MongoResource<T extends ResourceRecord> extends Resource<T>, ResourceLocker<T>, MigratableResource<MongoTx> {
|
|
6
48
|
name?: string
|
|
7
49
|
schema?: AnySchema
|
|
8
50
|
indexes?: Array<{ name: string, index: IndexSpecification, options?: CreateIndexesOptions }>
|
|
@@ -10,6 +52,20 @@ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, Re
|
|
|
10
52
|
db: () => Promise<Db>
|
|
11
53
|
client: () => Promise<MongoClient>
|
|
12
54
|
index: <Type extends MongoResource<T>>(name: string, index: IndexSpecification, options?: CreateIndexesOptions) => Type
|
|
55
|
+
/**
|
|
56
|
+
* Declare that a field stores another record's id.
|
|
57
|
+
*
|
|
58
|
+
* Chainable and idempotent like {@link MigratableResource.migration}, and stored the same
|
|
59
|
+
* way — per alias at module scope — because losing the declaration to a context rebuild
|
|
60
|
+
* would silently stop the string/ObjectId conversion for the field.
|
|
61
|
+
*
|
|
62
|
+
* Declare only fields whose values really are mongo ids (assigned from another record's
|
|
63
|
+
* `id`). Composite keys, external provider ids, DIDs and business slugs must stay
|
|
64
|
+
* strings — converting them corrupts the collection.
|
|
65
|
+
*/
|
|
66
|
+
reference: (field: string, opts?: string | MongoRefOptions) => this
|
|
67
|
+
/** The declared references of this alias. */
|
|
68
|
+
references: () => MongoReference[]
|
|
13
69
|
getDefaults: () => Partial<T>
|
|
14
70
|
}
|
|
15
71
|
|