@owlmeans/mongo-resource 0.1.18-rc.8 → 0.1.18

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 (116) hide show
  1. package/README.md +232 -71
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/mongo-resource/SKILL.md +89 -44
  4. package/build/consts.d.ts +8 -4
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +8 -4
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +4 -9
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +28 -21
  15. package/build/declarations.js.map +1 -1
  16. package/build/index.d.ts +4 -0
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +1 -0
  19. package/build/index.js.map +1 -1
  20. package/build/resource.d.ts +2 -2
  21. package/build/resource.d.ts.map +1 -1
  22. package/build/resource.js +100 -130
  23. package/build/resource.js.map +1 -1
  24. package/build/types.d.ts +11 -5
  25. package/build/types.d.ts.map +1 -1
  26. package/build/types.local.d.ts +4 -0
  27. package/build/types.local.d.ts.map +1 -0
  28. package/build/types.local.js +2 -0
  29. package/build/types.local.js.map +1 -0
  30. package/build/utils/consts.local.d.ts +25 -0
  31. package/build/utils/consts.local.d.ts.map +1 -0
  32. package/build/utils/consts.local.js +31 -0
  33. package/build/utils/consts.local.js.map +1 -0
  34. package/build/utils/criteria/types.d.ts +27 -0
  35. package/build/utils/criteria/types.d.ts.map +1 -0
  36. package/build/utils/criteria/types.js +2 -0
  37. package/build/utils/criteria/types.js.map +1 -0
  38. package/build/utils/criteria.d.ts +9 -0
  39. package/build/utils/criteria.d.ts.map +1 -0
  40. package/build/utils/criteria.js +204 -0
  41. package/build/utils/criteria.js.map +1 -0
  42. package/build/utils/index.d.ts +1 -0
  43. package/build/utils/index.d.ts.map +1 -1
  44. package/build/utils/index.js +1 -0
  45. package/build/utils/index.js.map +1 -1
  46. package/build/utils/indexes/types.d.ts +19 -0
  47. package/build/utils/indexes/types.d.ts.map +1 -0
  48. package/build/utils/indexes/types.js +2 -0
  49. package/build/utils/indexes/types.js.map +1 -0
  50. package/build/utils/indexes.d.ts +3 -4
  51. package/build/utils/indexes.d.ts.map +1 -1
  52. package/build/utils/indexes.js +44 -28
  53. package/build/utils/indexes.js.map +1 -1
  54. package/build/utils/life-cycle/types.d.ts +36 -0
  55. package/build/utils/life-cycle/types.d.ts.map +1 -0
  56. package/build/utils/life-cycle/types.js +2 -0
  57. package/build/utils/life-cycle/types.js.map +1 -0
  58. package/build/utils/life-cycle.d.ts +3 -30
  59. package/build/utils/life-cycle.d.ts.map +1 -1
  60. package/build/utils/life-cycle.js +96 -112
  61. package/build/utils/life-cycle.js.map +1 -1
  62. package/build/utils/migrations.d.ts.map +1 -1
  63. package/build/utils/migrations.js +1 -2
  64. package/build/utils/migrations.js.map +1 -1
  65. package/build/utils/name.d.ts.map +1 -1
  66. package/build/utils/name.js +1 -5
  67. package/build/utils/name.js.map +1 -1
  68. package/build/utils/refs/types.d.ts +78 -0
  69. package/build/utils/refs/types.d.ts.map +1 -0
  70. package/build/utils/refs/types.js +2 -0
  71. package/build/utils/refs/types.js.map +1 -0
  72. package/build/utils/refs.d.ts +13 -67
  73. package/build/utils/refs.d.ts.map +1 -1
  74. package/build/utils/refs.js +115 -158
  75. package/build/utils/refs.js.map +1 -1
  76. package/build/utils/schema/types.d.ts +16 -0
  77. package/build/utils/schema/types.d.ts.map +1 -0
  78. package/build/utils/schema/types.js +2 -0
  79. package/build/utils/schema/types.js.map +1 -0
  80. package/build/utils/schema.d.ts +3 -11
  81. package/build/utils/schema.d.ts.map +1 -1
  82. package/build/utils/schema.js +86 -88
  83. package/build/utils/schema.js.map +1 -1
  84. package/build/utils/types.local.d.ts +13 -0
  85. package/build/utils/types.local.d.ts.map +1 -0
  86. package/build/utils/types.local.js +2 -0
  87. package/build/utils/types.local.js.map +1 -0
  88. package/package.json +10 -9
  89. package/src/consts.ts +8 -4
  90. package/src/declarations/types.ts +12 -0
  91. package/src/declarations.ts +32 -28
  92. package/src/index.ts +4 -0
  93. package/src/resource.ts +121 -163
  94. package/src/types.local.ts +5 -0
  95. package/src/types.ts +13 -7
  96. package/src/utils/consts.local.ts +35 -0
  97. package/src/utils/criteria/types.ts +28 -0
  98. package/src/utils/criteria.ts +235 -0
  99. package/src/utils/index.ts +1 -0
  100. package/src/utils/indexes/types.ts +20 -0
  101. package/src/utils/indexes.ts +48 -28
  102. package/src/utils/life-cycle/types.ts +39 -0
  103. package/src/utils/life-cycle.ts +120 -134
  104. package/src/utils/migrations.ts +3 -17
  105. package/src/utils/name.ts +1 -6
  106. package/src/utils/refs/types.ts +79 -0
  107. package/src/utils/refs.ts +147 -189
  108. package/src/utils/schema/types.ts +17 -0
  109. package/src/utils/schema.ts +92 -91
  110. package/src/utils/types.local.ts +13 -0
  111. package/tests/context.ts +3 -3
  112. package/tests/criteria.spec.ts +116 -0
  113. package/tests/crud.spec.ts +2 -2
  114. package/tests/indexes.spec.ts +42 -0
  115. package/tests/refs.spec.ts +5 -3
  116. package/build/.gitkeep +0 -0
@@ -1,166 +1,152 @@
1
1
  import { MigrationStage, runMigrations } from '@owlmeans/resource'
2
2
  import type { DbConfig, MigrationReport, ResourceRecord } from '@owlmeans/resource'
3
3
  import type { BasicContext } from '@owlmeans/context'
4
+ import { logger } from '@owlmeans/log'
4
5
  import type { MongoReference, MongoResource } from '../types.js'
5
6
  import type { Db, Collection, Document, IndexSpecification } from 'mongodb'
6
7
  import { DEF_MIGRATIONS_COLLECTION } from '../consts.js'
7
- import { getDeclaration } from '../declarations.js'
8
+ import { mongoDeclarationHelper } from '../declarations.js'
9
+ import type { MongoLifeCycleUtils } from './life-cycle/types.js'
8
10
  import { mongoCollectionName } from './name.js'
9
- import { applyReferenceTypes, schemaToMongoSchema } from './schema.js'
10
- import { updateIndexes } from './indexes.js'
11
+ import { mongoSchemaUtils } from './schema.js'
12
+ import { mongoIndexUtils } from './indexes.js'
11
13
  import { makeMongoMigrationStore, makeMongoTx } from './migrations.js'
12
- import { reconcileReferences } from './refs.js'
13
-
14
- /**
15
- * Bring a resource's collection to the shape its schema declares.
16
- *
17
- * Order is deliberate, and matches the Postgres counterpart:
18
- *
19
- * 1. probe for the collection
20
- * 2. absent → *baseline* every registered migration; present → run the `pre` ones
21
- * (including the system `$ref:` migrations that convert declared references)
22
- * 3. create the collection, or update its validator and indexes
23
- * 4. present → run the `post` migrations
24
- * 5. reconcile declared references — the second half of their double check: the ledger
25
- * said whether the `$ref:` migration ran, this probes the collection itself and
26
- * converts whatever strings still slipped through
27
- *
28
- * Step 2 is what stops the two mechanisms colliding. A `pre` migration runs before the
29
- * validator is tightened, so it can reshape documents the new validator would reject. On a
30
- * collection this call just created there is nothing to reshape — replaying a historical
31
- * migration against a collection born in its final shape would at best scan for nothing —
32
- * so the migrations are recorded as satisfied instead of run.
33
- *
34
- * `context` is optional so the existing three-argument call keeps working; without it a
35
- * migration's `use`/`ref` can only address the owning resource.
36
- */
37
- export const initializeCollection = async (
38
- db: Db, config: DbConfig, resource: MongoResource<ResourceRecord>,
39
- context?: BasicContext<any>
40
- ): Promise<Collection> => {
41
- const name = mongoCollectionName(config, resource)
42
- const fresh = !await db.listCollections({ name }).hasNext()
43
-
44
- appendReferenceIndexes(resource)
45
-
46
- const migrate = prepareMigrations(db, config, resource, name, context)
47
- await migrate(fresh ? { baseline: true } : { stage: MigrationStage.Pre })
48
-
49
- const collection = fresh
50
- ? await createCollection(db, name, resource)
51
- : await updateCollection(db, name, resource)
52
-
53
- if (!fresh) {
54
- await migrate({ stage: MigrationStage.Post })
55
- await reconcileReferences(collection, referencesOf(resource), resource.alias)
56
- }
14
+ import { mongoRefHelper } from './refs.js'
57
15
 
58
- return collection
59
- }
16
+ const log = logger('mongo-resource')
60
17
 
61
- /** Tolerates hand-built resource objects that predate the reference capability. */
62
- const referencesOf = (resource: MongoResource<ResourceRecord>): MongoReference[] =>
63
- resource.references?.() ?? []
64
-
65
- /**
66
- * A declared reference is indexed at the mongo level — that's part of its contract. The
67
- * index is appended unless the resource already declares one with the same key pattern
68
- * (mongo refuses two indexes over identical keys, and the existing one — possibly
69
- * unique — wins).
70
- */
71
- const appendReferenceIndexes = (resource: MongoResource<ResourceRecord>): void => {
72
- for (const ref of referencesOf(resource)) {
73
- if (ref.noIndex === true) {
74
- continue
75
- }
76
- resource.indexes = resource.indexes ?? []
77
- const spec = JSON.stringify({ [ref.field]: 1 })
78
- const present = resource.indexes.some(index =>
79
- JSON.stringify(index.index as IndexSpecification) === spec || index.name === `ref_${ref.field}`
80
- )
81
- if (!present) {
82
- resource.indexes.push({ name: `ref_${ref.field}`, index: { [ref.field]: 1 } })
18
+ export const makeMongoLifeCycleUtils = (db: Db): MongoLifeCycleUtils => {
19
+ const { applyReferenceTypes, schemaToMongoSchema } = mongoSchemaUtils
20
+
21
+ const initializeCollection = async (
22
+ config: DbConfig, resource: MongoResource<ResourceRecord>,
23
+ context?: BasicContext<any>
24
+ ): Promise<Collection> => {
25
+ const name = mongoCollectionName(config, resource)
26
+ const fresh = !await db.listCollections({ name }).hasNext()
27
+
28
+ appendReferenceIndexes(resource)
29
+
30
+ const migrate = prepareMigrations(config, resource, name, context)
31
+ await migrate(fresh ? { baseline: true } : { stage: MigrationStage.Pre })
32
+
33
+ const collection = fresh
34
+ ? await createCollection(name, resource)
35
+ : await updateCollection(name, resource)
36
+
37
+ if (!fresh) {
38
+ await migrate({ stage: MigrationStage.Post })
39
+ await mongoRefHelper.reconcileReferences(collection, referencesOf(resource), resource.alias)
83
40
  }
84
- }
85
- }
86
41
 
87
- /**
88
- * Bind the migration runner to this resource's database, or hand back a no-op when nothing
89
- * is registered — the overwhelmingly common case, and one that shouldn't pay for a ledger.
90
- */
91
- const prepareMigrations = (
92
- db: Db, config: DbConfig, resource: MongoResource<ResourceRecord>, name: string,
93
- context?: BasicContext<any>
94
- ): (opts: { stage?: MigrationStage, baseline?: boolean }) => Promise<MigrationReport | null> => {
95
- const registry = getDeclaration(resource.alias).migrations
96
- if (registry.list().length < 1) {
97
- return async () => null
42
+ return collection
98
43
  }
99
44
 
100
- const ledgerName = (config.meta as { migrationsCollection?: string } | undefined)?.migrationsCollection
101
- ?? DEF_MIGRATIONS_COLLECTION
45
+ /** Tolerates hand-built resource objects that predate the reference capability. */
46
+ const referencesOf = (resource: MongoResource<ResourceRecord>): MongoReference[] =>
47
+ resource.references?.() ?? []
48
+
102
49
  /**
103
- * `db.collection()` never round-trips, so taking the handle before the collection exists
104
- * is safe — and on the fresh path the transaction is only ever baselined, never used.
50
+ * A declared reference is indexed at the mongo level — that's part of its contract. The
51
+ * index is appended unless the resource already declares one with the same key pattern
52
+ * (mongo refuses two indexes over identical keys, and the existing one — possibly
53
+ * unique — wins).
105
54
  */
106
- const tx = makeMongoTx(db, db.collection(name), config, context as BasicContext<any>, resource.alias)
107
- const store = makeMongoMigrationStore(db, tx, ledgerName)
108
-
109
- return async opts => {
110
- const report = await runMigrations(resource.alias, registry, store, opts)
111
- if (report.applied.length > 0) {
112
- console.log(
113
- `@owlmeans/mongo-resource: ${name} applied ${report.stage} migrations —`
114
- + ` ${report.applied.join(', ')}`
55
+ const appendReferenceIndexes = (resource: MongoResource<ResourceRecord>): void => {
56
+ for (const ref of referencesOf(resource)) {
57
+ if (ref.noIndex === true) {
58
+ continue
59
+ }
60
+ resource.indexes = resource.indexes ?? []
61
+ const spec = JSON.stringify({ [ref.field]: 1 })
62
+ const present = resource.indexes.some(index =>
63
+ JSON.stringify(index.index as IndexSpecification) === spec || index.name === `ref_${ref.field}`
115
64
  )
65
+ if (!present) {
66
+ resource.indexes.push({ name: `ref_${ref.field}`, index: { [ref.field]: 1 } })
67
+ }
116
68
  }
117
-
118
- return report
119
69
  }
120
- }
121
70
 
122
- export const createCollection = async (db: Db, name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
123
- const collection = await db.createCollection(name, {
124
- ...(resource.schema != null ? {
125
- validator: {
126
- $jsonSchema: patchJsonSchema(applyReferenceTypes(
127
- schemaToMongoSchema(resource.schema), resource.schema, resource.references()
128
- ))
71
+ /**
72
+ * Bind the migration runner to this resource's database, or hand back a no-op when nothing
73
+ * is registered — the overwhelmingly common case, and one that shouldn't pay for a ledger.
74
+ */
75
+ const prepareMigrations = (
76
+ config: DbConfig, resource: MongoResource<ResourceRecord>, name: string,
77
+ context?: BasicContext<any>
78
+ ): (opts: { stage?: MigrationStage, baseline?: boolean }) => Promise<MigrationReport | null> => {
79
+ const registry = mongoDeclarationHelper.getDeclaration(resource.alias).migrations
80
+ if (registry.list().length < 1) {
81
+ return async () => null
82
+ }
83
+
84
+ const ledgerName = (config.meta as { migrationsCollection?: string } | undefined)?.migrationsCollection
85
+ ?? DEF_MIGRATIONS_COLLECTION
86
+ /**
87
+ * `db.collection()` never round-trips, so taking the handle before the collection exists
88
+ * is safe — and on the fresh path the transaction is only ever baselined, never used.
89
+ */
90
+ const tx = makeMongoTx(db, db.collection(name), config, context as BasicContext<any>, resource.alias)
91
+ const store = makeMongoMigrationStore(db, tx, ledgerName)
92
+
93
+ return async opts => {
94
+ const report = await runMigrations(resource.alias, registry, store, opts)
95
+ if (report.applied.length > 0) {
96
+ log.info('Migrations applied', {
97
+ resource: resource.alias, collection: name, stage: report.stage, applied: report.applied,
98
+ }, { event: 'migration.applied' })
129
99
  }
130
- } : {})
131
- })
132
-
133
- if (resource.indexes != null) {
134
- await Promise.all(resource.indexes.map(
135
- async index => await collection.createIndex(index.index, {
136
- name: index.name, ...((index.options != null) ? index.options : {})
137
- })
138
- ))
100
+
101
+ return report
102
+ }
139
103
  }
140
104
 
141
- return collection
142
- }
105
+ const createCollection = async (name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
106
+ const collection = await db.createCollection(name, {
107
+ ...(resource.schema != null ? {
108
+ validator: {
109
+ $jsonSchema: patchJsonSchema(applyReferenceTypes(
110
+ schemaToMongoSchema(resource.schema), resource.schema, resource.references()
111
+ ))
112
+ }
113
+ } : {})
114
+ })
115
+
116
+ if (resource.indexes != null) {
117
+ await Promise.all(resource.indexes.map(
118
+ async index => await collection.createIndex(index.index, {
119
+ name: index.name, ...((index.options != null) ? index.options : {})
120
+ })
121
+ ))
122
+ }
143
123
 
144
- export const updateCollection = async (db: Db, name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
145
- if (resource.schema != null) {
146
- const $jsonSchema = patchJsonSchema(applyReferenceTypes(
147
- schemaToMongoSchema(resource.schema), resource.schema, resource.references()
148
- ))
149
- await db.command({ collMod: name, validator: { $jsonSchema } })
124
+ return collection
150
125
  }
151
126
 
152
- const collection = db.collection(name)
127
+ const updateCollection = async (name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
128
+ if (resource.schema != null) {
129
+ const $jsonSchema = patchJsonSchema(applyReferenceTypes(
130
+ schemaToMongoSchema(resource.schema), resource.schema, resource.references()
131
+ ))
132
+ await db.command({ collMod: name, validator: { $jsonSchema } })
133
+ }
153
134
 
154
- await updateIndexes(collection, resource)
135
+ const collection = db.collection(name)
155
136
 
156
- return collection
157
- }
137
+ await mongoIndexUtils.updateIndexes(collection, resource)
158
138
 
159
- const patchJsonSchema = (schema: Document): Document => {
160
- if (schema.properties != null) {
161
- if (schema.properties._id == null) {
162
- schema.properties._id = { bsonType: 'objectId' }
139
+ return collection
140
+ }
141
+
142
+ const patchJsonSchema = (schema: Document): Document => {
143
+ if (schema.properties != null) {
144
+ if (schema.properties._id == null) {
145
+ schema.properties._id = { bsonType: 'objectId' }
146
+ }
163
147
  }
148
+ return schema
164
149
  }
165
- return schema
150
+
151
+ return { initializeCollection, createCollection, updateCollection }
166
152
  }
@@ -1,25 +1,11 @@
1
1
  import type { BasicContext } from '@owlmeans/context'
2
- import type { DbConfig, Migration, MigrationStage, MigrationStore, ResourceRecord } from '@owlmeans/resource'
2
+ import type { DbConfig, Migration, MigrationStore, ResourceRecord } from '@owlmeans/resource'
3
3
  import type { Collection, Db } from 'mongodb'
4
4
 
5
- import {
6
- DEF_MIGRATION_POLL, DEF_MIGRATION_WAIT, DEF_MIGRATIONS_COLLECTION, MONGO_DUPLICATE_KEY
7
- } from '../consts.js'
5
+ import { DEF_MIGRATION_POLL, DEF_MIGRATION_WAIT, DEF_MIGRATIONS_COLLECTION, MONGO_DUPLICATE_KEY, DEFAULT_DB_ALIAS } from '../consts.js'
8
6
  import type { MongoDbService, MongoResource, MongoTx } from '../types.js'
9
- import { DEFAULT_DB_ALIAS } from '../consts.js'
10
7
  import { mongoCollectionName } from './name.js'
11
-
12
- interface LedgerRecord {
13
- alias: string
14
- name: string
15
- stage: MigrationStage
16
- checksum: string | null
17
- baseline: boolean
18
- startedAt: Date
19
- /** `null` while a replica is running it — the claim, not the completion. */
20
- completedAt: Date | null
21
- durationMs: number | null
22
- }
8
+ import type { LedgerRecord } from './types.local.js'
23
9
 
24
10
  const isDuplicateKey = (error: unknown): boolean =>
25
11
  (error as { code?: number } | null)?.code === MONGO_DUPLICATE_KEY
package/src/utils/name.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
2
2
  import type { MongoResource } from '../types.js'
3
-
4
- // Collection names must use only alphanumeric characters, underscore, and hyphen — no
5
- // colons or other delimiters. This guard enforces the rule for every collection created
6
- // through makeMongoResource across all projects (common/internal/app), failing fast at
7
- // initialization rather than silently creating a badly-named collection.
8
- const VALID_COLLECTION_NAME = /^[a-zA-Z0-9_-]+$/
3
+ import { VALID_COLLECTION_NAME } from './consts.local.js'
9
4
 
10
5
  export const mongoCollectionName = (config: DbConfig, resource: MongoResource<ResourceRecord>): string => {
11
6
  const name = `${config.resourcePrefix ?? ''}${resource.name ?? resource.alias}`
@@ -0,0 +1,79 @@
1
+ import type { Collection, Document } from 'mongodb'
2
+
3
+ import type { MongoReference } from '../../types.js'
4
+
5
+ /** Declared ObjectId references: converting their values, their criteria and their stored data. */
6
+ export interface MongoRefHelper {
7
+ /** A 24 hex string — the only shape a stored reference is converted from. */
8
+ isObjectIdHex: (value: unknown) => value is string
9
+ /**
10
+ * Write side of a declared reference: the string id a record carries becomes the
11
+ * `ObjectId` the collection stores. Arrays convert elementwise.
12
+ *
13
+ * Strict on purpose — a declared reference holding something that is not a mongo id is
14
+ * either a mis-declared field (should never have been a reference) or a bug at the call
15
+ * site, and storing it as a string would silently reintroduce the mixed type state this
16
+ * mechanism exists to remove.
17
+ *
18
+ * @throws {MisshapedRecord}
19
+ */
20
+ marshalReference: (field: string, value: unknown) => unknown
21
+ /** Read side: `ObjectId` back to the string records carry. Tolerates not yet migrated strings. */
22
+ demarshalReference: (value: unknown) => unknown
23
+ /** Convert every declared reference of a fetched document back to string ids, in place. */
24
+ demarshalRefs: <T extends {}>(record: T, refs: Map<string, MongoReference>) => T
25
+ /**
26
+ * Convert a mongo filter the way records are converted: values addressed at `_id` or at a
27
+ * declared reference become `ObjectId`s, and the `id` alias records actually carry is
28
+ * mapped onto `_id` — documents never store `id`, so before this mapping such criteria
29
+ * silently matched nothing.
30
+ *
31
+ * Tolerant by design: a value that is not 24 hex passes through unconverted. Criteria are
32
+ * matched against the collection, and against an `ObjectId` typed field a stray string
33
+ * matches nothing — which is exactly what it matched before the field was converted.
34
+ *
35
+ * Operators arrive already in mongo's own vocabulary — `criteriaToFilter` translates
36
+ * the portable one first, so this pass only has to convert values.
37
+ */
38
+ marshalCriteria: (criteria: Document | undefined, refs: Map<string, MongoReference>) => Document | undefined
39
+ /**
40
+ * Criteria for addressing a single record by a field — `get`/`load`/`update`/`delete`.
41
+ *
42
+ * `_id` keeps its historical strictness (an invalid id throws through the driver). The
43
+ * `id` alias and declared references convert tolerantly, so a caller probing a reference
44
+ * with a value that is not a mongo id gets "not found" rather than a throw.
45
+ */
46
+ identityCriteria: (field: string, id: string, refs: Map<string, MongoReference>) => Document
47
+ /**
48
+ * Name of the system migration that converts a reference field's stored strings.
49
+ *
50
+ * The `@1` is the body's version: the shared body (`makeRefMigration`) fingerprints identically
51
+ * for every field, so an edit to it would raise `MigrationConflict` against every ledger on
52
+ * the next boot. Any semantic change to {@link MongoRefHelper.convertReferenceField} must bump
53
+ * this suffix instead — the old name stays applied, the new one runs (idempotently) once.
54
+ */
55
+ refMigrationName: (field: string) => string
56
+ /**
57
+ * Convert one reference field's stored string ids to `ObjectId`s — the body of the
58
+ * system migration and of the boot time reconciliation probe alike.
59
+ *
60
+ * Idempotent and interrupt safe: it matches only documents where the field (or one of
61
+ * its elements) is still a string, converts only values that are actually 24 hex, and
62
+ * leaves everything else exactly as it was. Safe to run concurrently from several
63
+ * replicas — a document converts once, the loser's filter no longer matches it.
64
+ *
65
+ * Validation is bypassed deliberately: at `Pre` stage the collection still carries the
66
+ * validator that declares the field a *string*, and after the switch a legacy document
67
+ * may violate the schema in unrelated ways — either would wedge the boot on a write
68
+ * that only makes the data more correct. (Bypassing requires the connection's user to
69
+ * hold the `bypassDocumentValidation` privilege — `dbOwner`/`root` do.)
70
+ */
71
+ convertReferenceField: (collection: Collection, field: string) => Promise<number>
72
+ /**
73
+ * The second half of the double check the reference migration promises: the ledger says
74
+ * whether the migration ran; this probes whether the collection actually holds no
75
+ * convertible strings — and repairs it when the two disagree (a restored backup, a
76
+ * write from a legacy process, a ledger created by hand).
77
+ */
78
+ reconcileReferences: (collection: Collection, refs: MongoReference[], alias: string) => Promise<void>
79
+ }