@owlmeans/mongo-resource 0.1.14 → 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
@@ -0,0 +1,129 @@
1
+ import { DEF_MIGRATION_POLL, DEF_MIGRATION_WAIT, DEF_MIGRATIONS_COLLECTION, MONGO_DUPLICATE_KEY } from '../consts.js';
2
+ import { mongoCollectionName } from './name.js';
3
+ const isDuplicateKey = (error) => error?.code === MONGO_DUPLICATE_KEY;
4
+ /**
5
+ * A transaction façade over the database. Alias resolution is injected so migrations can
6
+ * address other resources without importing the context.
7
+ *
8
+ * Names are resolved through {@link mongoCollectionName}, not through the other resource's
9
+ * live `collection` handle, because at migration time that resource may not have
10
+ * initialized yet — registration order is not dependency order. The name is a pure
11
+ * function of the config and the alias, so it's knowable either way.
12
+ */
13
+ export const makeMongoTx = (db, collection, config, context, self) => {
14
+ const ref = (alias) => {
15
+ if (alias == null || alias === self) {
16
+ return collection.collectionName;
17
+ }
18
+ return mongoCollectionName(config, context.resource(alias));
19
+ };
20
+ return {
21
+ db,
22
+ collection,
23
+ ref,
24
+ use: alias => alias == null || alias === self ? collection : db.collection(ref(alias))
25
+ };
26
+ };
27
+ const waitForClaim = async (ledger, alias, name) => {
28
+ const deadline = Date.now() + DEF_MIGRATION_WAIT;
29
+ while (Date.now() < deadline) {
30
+ const row = await ledger.findOne({ alias, name });
31
+ if (row == null) {
32
+ /** The claim was withdrawn — the other replica's migration failed, and so must this boot. */
33
+ throw new Error(`${alias}/${name}: abandoned by the replica that claimed it`);
34
+ }
35
+ if (row.completedAt != null) {
36
+ return;
37
+ }
38
+ await new Promise(resolve => setTimeout(resolve, DEF_MIGRATION_POLL));
39
+ }
40
+ throw new Error(`${alias}/${name}: still running elsewhere after ${DEF_MIGRATION_WAIT}ms`);
41
+ };
42
+ /**
43
+ * Migration ledger, one collection per database.
44
+ *
45
+ * Where the Postgres store commits the ledger row inside the migration's own transaction,
46
+ * this one can't — see {@link MongoTx}. It uses claim-then-complete instead: the unique
47
+ * index on `(alias, name)` is the mutual exclusion primitive, so a replica that loses the
48
+ * race to insert waits for the winner rather than running the same migration twice.
49
+ */
50
+ export const makeMongoMigrationStore = (db, tx, name = DEF_MIGRATIONS_COLLECTION) => {
51
+ const ledger = () => db.collection(name);
52
+ return {
53
+ ensure: async () => {
54
+ await ledger().createIndex({ alias: 1, name: 1 }, { name: 'alias_name_unique', unique: true });
55
+ },
56
+ applied: async (alias) => {
57
+ /**
58
+ * Completed rows only. A claim still holding a null `completedAt` belongs to a replica
59
+ * running right now, and treating it as applied would let this process carry on against
60
+ * a half-migrated database.
61
+ */
62
+ const rows = await ledger().find({ alias, completedAt: { $ne: null } }).toArray();
63
+ return rows.reduce((applied, row) => {
64
+ applied[row.name] = row.checksum;
65
+ return applied;
66
+ }, {});
67
+ },
68
+ baseline: async (alias, migrations) => {
69
+ const now = new Date();
70
+ /**
71
+ * `$setOnInsert` upserts rather than plain inserts: two replicas creating the same
72
+ * collection at once both baseline, and the second must be a no-op, not a crash.
73
+ */
74
+ await ledger().bulkWrite(migrations.map(migration => ({
75
+ updateOne: {
76
+ filter: { alias, name: migration.name },
77
+ update: {
78
+ $setOnInsert: {
79
+ alias,
80
+ name: migration.name,
81
+ stage: migration.stage,
82
+ checksum: migration.checksum,
83
+ baseline: true,
84
+ startedAt: now,
85
+ completedAt: now,
86
+ durationMs: 0
87
+ }
88
+ },
89
+ upsert: true
90
+ }
91
+ })), { ordered: false });
92
+ },
93
+ run: async (alias, migration) => {
94
+ const started = Date.now();
95
+ try {
96
+ await ledger().insertOne({
97
+ alias,
98
+ name: migration.name,
99
+ stage: migration.stage,
100
+ checksum: migration.checksum,
101
+ baseline: false,
102
+ startedAt: new Date(started),
103
+ completedAt: null,
104
+ durationMs: null
105
+ });
106
+ }
107
+ catch (error) {
108
+ if (!isDuplicateKey(error)) {
109
+ throw error;
110
+ }
111
+ return await waitForClaim(ledger(), alias, migration.name);
112
+ }
113
+ try {
114
+ await migration.apply(tx);
115
+ }
116
+ catch (error) {
117
+ /**
118
+ * Withdraw the claim so the next boot retries. Postgres rolls its ledger row back
119
+ * with the migration itself; without a transaction the claim has to be released by
120
+ * hand, and leaving it would wedge every future boot on a migration that never ran.
121
+ */
122
+ await ledger().deleteOne({ alias, name: migration.name }).catch(() => undefined);
123
+ throw error;
124
+ }
125
+ await ledger().updateOne({ alias, name: migration.name }, { $set: { completedAt: new Date(), durationMs: Date.now() - started } });
126
+ }
127
+ };
128
+ };
129
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/utils/migrations.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,kBAAkB,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,mBAAmB,EACvF,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAc/C,MAAM,cAAc,GAAG,CAAC,KAAc,EAAW,EAAE,CAChD,KAAkC,EAAE,IAAI,KAAK,mBAAmB,CAAA;AAEnE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,EAAM,EAAE,UAAsB,EAAE,MAAgB,EAAE,OAA0B,EAAE,IAAY,EACjF,EAAE;IACX,MAAM,GAAG,GAAG,CAAC,KAAc,EAAU,EAAE;QACrC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACpC,OAAO,UAAU,CAAC,cAAc,CAAA;QAClC,CAAC;QAED,OAAO,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAgC,KAAK,CAAC,CAAC,CAAA;IAC5F,CAAC,CAAA;IAED,OAAO;QACL,EAAE;QACF,UAAU;QACV,GAAG;QACH,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;KACvF,CAAA;AACH,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,KAAK,EACxB,MAAgC,EAAE,KAAa,EAAE,IAAY,EAC9C,EAAE;IACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB,CAAA;IAChD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YAChB,6FAA6F;YAC7F,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,4CAA4C,CAAC,CAAA;QAC/E,CAAC;QACD,IAAI,GAAG,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;YAC5B,OAAM;QACR,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAA;IACvE,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,mCAAmC,kBAAkB,IAAI,CAAC,CAAA;AAC5F,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,EAAM,EAAE,EAAW,EAAE,OAAe,yBAAyB,EACpC,EAAE;IAC3B,MAAM,MAAM,GAAG,GAA6B,EAAE,CAAC,EAAE,CAAC,UAAU,CAAe,IAAI,CAAC,CAAA;IAEhF,OAAO;QACL,MAAM,EAAE,KAAK,IAAI,EAAE;YACjB,MAAM,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAChG,CAAC;QAED,OAAO,EAAE,KAAK,EAAC,KAAK,EAAC,EAAE;YACrB;;;;eAIG;YACH,MAAM,IAAI,GAAG,MAAM,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;YAEjF,OAAO,IAAI,CAAC,MAAM,CAAgC,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;gBACjE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAA;gBAEhC,OAAO,OAAO,CAAA;YAChB,CAAC,EAAE,EAAE,CAAC,CAAA;QACR,CAAC;QAED,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YACpC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;YACtB;;;eAGG;YACH,MAAM,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACpD,SAAS,EAAE;oBACT,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;oBACvC,MAAM,EAAE;wBACN,YAAY,EAAE;4BACZ,KAAK;4BACL,IAAI,EAAE,SAAS,CAAC,IAAI;4BACpB,KAAK,EAAE,SAAS,CAAC,KAAK;4BACtB,QAAQ,EAAE,SAAS,CAAC,QAAQ;4BAC5B,QAAQ,EAAE,IAAI;4BACd,SAAS,EAAE,GAAG;4BACd,WAAW,EAAE,GAAG;4BAChB,UAAU,EAAE,CAAC;yBACd;qBACF;oBACD,MAAM,EAAE,IAAI;iBACb;aACF,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;QAC1B,CAAC;QAED,GAAG,EAAE,KAAK,EAAE,KAAa,EAAE,SAA6B,EAAE,EAAE;YAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC1B,IAAI,CAAC;gBACH,MAAM,MAAM,EAAE,CAAC,SAAS,CAAC;oBACvB,KAAK;oBACL,IAAI,EAAE,SAAS,CAAC,IAAI;oBACpB,KAAK,EAAE,SAAS,CAAC,KAAK;oBACtB,QAAQ,EAAE,SAAS,CAAC,QAAQ;oBAC5B,QAAQ,EAAE,KAAK;oBACf,SAAS,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC;oBAC5B,WAAW,EAAE,IAAI;oBACjB,UAAU,EAAE,IAAI;iBACjB,CAAC,CAAA;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC3B,MAAM,KAAK,CAAA;gBACb,CAAC;gBAED,OAAO,MAAM,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;YAC5D,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC3B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf;;;;mBAIG;gBACH,MAAM,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;gBAChF,MAAM,KAAK,CAAA;YACb,CAAC;YAED,MAAM,MAAM,EAAE,CAAC,SAAS,CACtB,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,EAC/B,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,EAAE,CACxE,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,74 @@
1
+ import type { ListCriteria } from '@owlmeans/resource';
2
+ import type { Collection, Document } from 'mongodb';
3
+ import type { MongoReference, MongoTx } from '../types.js';
4
+ export declare const isObjectIdHex: (value: unknown) => value is string;
5
+ /**
6
+ * Write side of a declared reference: the string id a record carries becomes the
7
+ * `ObjectId` the collection stores. Arrays convert elementwise.
8
+ *
9
+ * Strict on purpose — a declared reference holding something that is not a mongo id is
10
+ * either a mis-declared field (should never have been a reference) or a bug at the call
11
+ * site, and storing it as a string would silently reintroduce the mixed type state this
12
+ * mechanism exists to remove.
13
+ *
14
+ * @throws {MisshapedRecord}
15
+ */
16
+ export declare const marshalReference: (field: string, value: unknown) => unknown;
17
+ /** Read side: `ObjectId` back to the string records carry. Tolerates not yet migrated strings. */
18
+ export declare const demarshalReference: (value: unknown) => unknown;
19
+ /** Convert every declared reference of a fetched document back to string ids, in place. */
20
+ export declare const demarshalRefs: <T extends {}>(record: T, refs: Map<string, MongoReference>) => T;
21
+ /**
22
+ * Convert list criteria the way records are converted: values addressed at `_id` or at a
23
+ * declared reference become `ObjectId`s, and the `id` alias records actually carry is
24
+ * mapped onto `_id` — documents never store `id`, so before this mapping such criteria
25
+ * silently matched nothing.
26
+ *
27
+ * Tolerant by design: a value that is not 24 hex passes through unconverted. Criteria are
28
+ * matched against the collection, and against an `ObjectId` typed field a stray string
29
+ * matches nothing — which is exactly what it matched before the field was converted.
30
+ */
31
+ export declare const marshalCriteria: (criteria: ListCriteria | undefined, refs: Map<string, MongoReference>) => ListCriteria | undefined;
32
+ /**
33
+ * Criteria for addressing a single record by a field — `get`/`load`/`update`/`delete`.
34
+ *
35
+ * `_id` keeps its historical strictness (an invalid id throws through the driver). The
36
+ * `id` alias and declared references convert tolerantly, so a caller probing a reference
37
+ * with a value that is not a mongo id gets "not found" rather than a throw.
38
+ */
39
+ export declare const identityCriteria: (field: string, id: string, refs: Map<string, MongoReference>) => Document;
40
+ /**
41
+ * Name of the system migration that converts a reference field's stored strings.
42
+ *
43
+ * The `@1` is the body's version: the shared body below fingerprints identically for
44
+ * every field, so an edit to it would raise `MigrationConflict` against every ledger on
45
+ * the next boot. Any semantic change to {@link convertReferenceField} must bump this
46
+ * suffix instead — the old name stays applied, the new one runs (idempotently) once.
47
+ */
48
+ export declare const refMigrationName: (field: string) => string;
49
+ /** Ledger registered body of the system reference migration. */
50
+ export declare const makeRefMigration: (field: string) => (tx: MongoTx) => Promise<void>;
51
+ /**
52
+ * Convert one reference field's stored string ids to `ObjectId`s — the body of the
53
+ * system migration and of the boot time reconciliation probe alike.
54
+ *
55
+ * Idempotent and interrupt safe: it matches only documents where the field (or one of
56
+ * its elements) is still a string, converts only values that are actually 24 hex, and
57
+ * leaves everything else exactly as it was. Safe to run concurrently from several
58
+ * replicas — a document converts once, the loser's filter no longer matches it.
59
+ *
60
+ * Validation is bypassed deliberately: at `Pre` stage the collection still carries the
61
+ * validator that declares the field a *string*, and after the switch a legacy document
62
+ * may violate the schema in unrelated ways — either would wedge the boot on a write
63
+ * that only makes the data more correct. (Bypassing requires the connection's user to
64
+ * hold the `bypassDocumentValidation` privilege — `dbOwner`/`root` do.)
65
+ */
66
+ export declare const convertReferenceField: (collection: Collection, field: string) => Promise<number>;
67
+ /**
68
+ * The second half of the double check the reference migration promises: the ledger says
69
+ * whether the migration ran; this probes whether the collection actually holds no
70
+ * convertible strings — and repairs it when the two disagree (a restored backup, a
71
+ * write from a legacy process, a ledger created by hand).
72
+ */
73
+ export declare const reconcileReferences: (collection: Collection, refs: MongoReference[], alias: string) => Promise<void>;
74
+ //# sourceMappingURL=refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/utils/refs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAS1D,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACR,CAAA;AAEhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,EAAE,OAAO,OAAO,KAAG,OAehE,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,kBAAkB,GAAI,OAAO,OAAO,KAAG,OASnD,CAAA;AAED,2FAA2F;AAC3F,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,KAAG,CAY1F,CAAA;AA4BD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,YAAY,GAAG,SAAS,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,KACpE,YAAY,GAAG,SAkBjB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,EAAE,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,KAC3D,QAYF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAA2B,CAAA;AAE5E,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,MAAY,IAAI,OAAO,KAAG,OAAO,CAAC,IAAI,CAEnF,CAAA;AAeD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,GAAU,YAAY,UAAU,EAAE,OAAO,MAAM,KAAG,OAAO,CAAC,MAAM,CAmBjG,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,YAAY,UAAU,EAAE,MAAM,cAAc,EAAE,EAAE,OAAO,MAAM,KAC5D,OAAO,CAAC,IAAI,CAcd,CAAA"}
@@ -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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"}
@@ -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"}
@@ -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":"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"}
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.14",
3
+ "version": "0.1.16-rc.0",
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.14",
30
- "@owlmeans/resource": "^0.1.14",
31
- "@owlmeans/server-context": "^0.1.14"
29
+ "@owlmeans/context": "^0.1.16-rc.0",
30
+ "@owlmeans/resource": "^0.1.16-rc.0",
31
+ "@owlmeans/server-context": "^0.1.16-rc.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@owlmeans/dep-config": "workspace:*",
35
- "@owlmeans/test-integration": "^0.1.14",
35
+ "@owlmeans/test-integration": "^0.1.16-rc.0",
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'