@prisma/orm-target-mongo 0.16.0-dev.36
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/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/adapter.d.mts +19 -0
- package/dist/adapter.d.mts.map +1 -0
- package/dist/adapter.mjs +12 -0
- package/dist/adapter.mjs.map +1 -0
- package/dist/adapter__codec-ids.d.mts +12 -0
- package/dist/adapter__codec-ids.d.mts.map +1 -0
- package/dist/adapter__codec-ids.mjs +2 -0
- package/dist/adapter__codec-types.d.mts +38 -0
- package/dist/adapter__codec-types.d.mts.map +1 -0
- package/dist/adapter__codec-types.mjs +1 -0
- package/dist/adapter__codecs.d.mts +14 -0
- package/dist/adapter__codecs.d.mts.map +1 -0
- package/dist/adapter__codecs.mjs +2 -0
- package/dist/adapter__control.d.mts +3 -0
- package/dist/adapter__control.mjs +3 -0
- package/dist/adapter__runtime.d.mts +16 -0
- package/dist/adapter__runtime.d.mts.map +1 -0
- package/dist/adapter__runtime.mjs +25 -0
- package/dist/adapter__runtime.mjs.map +1 -0
- package/dist/codec-ids-BennPcpQ-Btzz3ILV.mjs +12 -0
- package/dist/codec-ids-BennPcpQ-Btzz3ILV.mjs.map +1 -0
- package/dist/codec-types-Bx2fHyq_.d.mts +35 -0
- package/dist/codec-types-Bx2fHyq_.d.mts.map +1 -0
- package/dist/codecs-DLWO-Cjc-CWfQj0bY.mjs +170 -0
- package/dist/codecs-DLWO-Cjc-CWfQj0bY.mjs.map +1 -0
- package/dist/control-BOc5lrQg.mjs +437 -0
- package/dist/control-BOc5lrQg.mjs.map +1 -0
- package/dist/control-CEXmcht7.mjs +1563 -0
- package/dist/control-CEXmcht7.mjs.map +1 -0
- package/dist/control-CxXmTy_h.d.mts +312 -0
- package/dist/control-CxXmTy_h.d.mts.map +1 -0
- package/dist/control-rPM2HNxW.d.mts +170 -0
- package/dist/control-rPM2HNxW.d.mts.map +1 -0
- package/dist/descriptor-meta-CBiaTwe5-HX742pZh.mjs +17 -0
- package/dist/descriptor-meta-CBiaTwe5-HX742pZh.mjs.map +1 -0
- package/dist/driver.d.mts +2 -0
- package/dist/driver.mjs +2 -0
- package/dist/driver__control.d.mts +16 -0
- package/dist/driver__control.d.mts.map +1 -0
- package/dist/driver__control.mjs +46 -0
- package/dist/driver__control.mjs.map +1 -0
- package/dist/migration-BkqcE2Uv.d.mts +42 -0
- package/dist/migration-BkqcE2Uv.d.mts.map +1 -0
- package/dist/migration-C-jfTsGU.mjs +4 -0
- package/dist/migration-factories-CCJFG_3e-75oJ8xn2.mjs +201 -0
- package/dist/migration-factories-CCJFG_3e-75oJ8xn2.mjs.map +1 -0
- package/dist/mongo-adapter-BkVYI6jn-ChXldtpg.mjs +807 -0
- package/dist/mongo-adapter-BkVYI6jn-ChXldtpg.mjs.map +1 -0
- package/dist/mongo-adapter-CjJU3S9o-BIJruFYW.d.mts +16 -0
- package/dist/mongo-adapter-CjJU3S9o-BIJruFYW.d.mts.map +1 -0
- package/dist/mongo-driver-C7zThOjZ-BcMrlirh.mjs +161 -0
- package/dist/mongo-driver-C7zThOjZ-BcMrlirh.mjs.map +1 -0
- package/dist/mongo-driver-D1lEf5Kl-KPfGUVXK.d.mts +33 -0
- package/dist/mongo-driver-D1lEf5Kl-KPfGUVXK.d.mts.map +1 -0
- package/dist/op-factory-call-B7EKa4Ri-B2Lb6W68.d.mts +76 -0
- package/dist/op-factory-call-B7EKa4Ri-B2Lb6W68.d.mts.map +1 -0
- package/dist/pack-MK8ZQfrL.d.mts +22 -0
- package/dist/pack-MK8ZQfrL.d.mts.map +1 -0
- package/dist/runtime-B9_-kjBX.d.mts +15 -0
- package/dist/runtime-B9_-kjBX.d.mts.map +1 -0
- package/dist/target.d.mts +8 -0
- package/dist/target.mjs +4 -0
- package/dist/target__codec-types.d.mts +2 -0
- package/dist/target__codec-types.mjs +1 -0
- package/dist/target__control.d.mts +4 -0
- package/dist/target__control.mjs +2 -0
- package/dist/target__migration.d.mts +2 -0
- package/dist/target__migration.mjs +3 -0
- package/dist/target__pack.d.mts +2 -0
- package/dist/target__pack.mjs +7 -0
- package/dist/target__pack.mjs.map +1 -0
- package/dist/target__runtime.d.mts +2 -0
- package/dist/target__runtime.mjs +21 -0
- package/dist/target__runtime.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
import { t as mongoTargetDescriptorMeta } from "./descriptor-meta-CBiaTwe5-HX742pZh.mjs";
|
|
2
|
+
import { a as dropCollection, n as createCollection, o as dropIndex, r as createIndex, t as collMod } from "./migration-factories-CCJFG_3e-75oJ8xn2.mjs";
|
|
3
|
+
import { i as extractDb, r as createMongoRunnerDeps } from "./control-BOc5lrQg.mjs";
|
|
4
|
+
import { n as MongoDriverImpl } from "./mongo-driver-C7zThOjZ-BcMrlirh.mjs";
|
|
5
|
+
import { structuredError } from "@prisma/orm-framework/utils/structured-error";
|
|
6
|
+
import { ifDefined } from "@prisma/orm-framework/utils/defined";
|
|
7
|
+
import { NamespaceBase, UNBOUND_NAMESPACE_ID, elementCoordinates, freezeNode, hydrateNamespaceEntities } from "@prisma/orm-framework/components/ir";
|
|
8
|
+
import { blindCast } from "@prisma/orm-framework/utils/casts";
|
|
9
|
+
import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr } from "@prisma/orm-family-mongo/query-ast/control";
|
|
10
|
+
import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma/orm-family-mongo/query-ast/execution";
|
|
11
|
+
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
12
|
+
import { canonicalize, deepEqual } from "@prisma/orm-family-mongo/schema-ir";
|
|
13
|
+
import { errorRunnerFailed } from "@prisma/orm-framework/errors/execution";
|
|
14
|
+
import { type } from "arktype";
|
|
15
|
+
import { canonicalizeSchemasForVerification, contractToMongoSchemaIR, diffMongoSchemas } from "@prisma/orm-family-mongo/family/control";
|
|
16
|
+
import { notOk, ok } from "@prisma/orm-framework/utils/result";
|
|
17
|
+
import { TsExpression, jsonToTsSource, renderImports } from "@prisma/orm-framework/ts-render";
|
|
18
|
+
import { Migration } from "@prisma/orm-toolchain/migration-tools/migration";
|
|
19
|
+
import { APP_SPACE_ID, contractSnapshotJsonSpecifier, contractSnapshotTypesSpecifier, issueOutcome } from "@prisma/orm-framework/components/control";
|
|
20
|
+
import { resolveRequirementSpecifiers } from "@prisma/orm-framework/components/emission";
|
|
21
|
+
import { detectScaffoldRuntime, shebangLineFor } from "@prisma/orm-toolchain/migration-tools/migration-ts";
|
|
22
|
+
import { verifyMongoSchema } from "@prisma/orm-family-mongo/family/schema-verify";
|
|
23
|
+
import { MongoContractSerializerBase, MongoSchemaVerifierBase } from "@prisma/orm-family-mongo/family/ir";
|
|
24
|
+
import { MongoStorage } from "@prisma/orm-family-mongo/contract";
|
|
25
|
+
import { composeMongoEntityKinds } from "@prisma/orm-family-mongo/contract/entity-kinds";
|
|
26
|
+
//#region ../../../3-mongo-target/1-mongo-target/dist/control.mjs
|
|
27
|
+
/**
|
|
28
|
+
* The single module a scaffolded Mongo `migration.ts` imports from: the
|
|
29
|
+
* operation factories below, plus the `Migration` base and `MigrationCLI`
|
|
30
|
+
* that `render-typescript.ts` adds. The authored name; `render-typescript.ts`
|
|
31
|
+
* maps it to whatever the consuming application's import root calls it,
|
|
32
|
+
* once over the whole assembled import list.
|
|
33
|
+
*/
|
|
34
|
+
const TARGET_MIGRATION_MODULE = "@internal/target-mongo/migration";
|
|
35
|
+
var OpFactoryCallNode = class extends TsExpression {
|
|
36
|
+
importRequirements() {
|
|
37
|
+
return [{
|
|
38
|
+
moduleSpecifier: TARGET_MIGRATION_MODULE,
|
|
39
|
+
symbol: this.factoryName
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
freeze() {
|
|
43
|
+
Object.freeze(this);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function formatKeys(keys) {
|
|
47
|
+
return keys.map((k) => `${k.field}:${k.direction}`).join(", ");
|
|
48
|
+
}
|
|
49
|
+
var CreateIndexCall = class extends OpFactoryCallNode {
|
|
50
|
+
factoryName = "createIndex";
|
|
51
|
+
operationClass = "additive";
|
|
52
|
+
collection;
|
|
53
|
+
keys;
|
|
54
|
+
options;
|
|
55
|
+
label;
|
|
56
|
+
constructor(collection, keys, options) {
|
|
57
|
+
super();
|
|
58
|
+
this.collection = collection;
|
|
59
|
+
this.keys = keys;
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.label = `Create index on ${collection} (${formatKeys(keys)})`;
|
|
62
|
+
this.freeze();
|
|
63
|
+
}
|
|
64
|
+
toOp() {
|
|
65
|
+
return createIndex(this.collection, this.keys, this.options);
|
|
66
|
+
}
|
|
67
|
+
renderTypeScript() {
|
|
68
|
+
return this.options ? `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)}, ${jsonToTsSource(this.options)})` : `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var DropIndexCall = class extends OpFactoryCallNode {
|
|
72
|
+
factoryName = "dropIndex";
|
|
73
|
+
operationClass = "destructive";
|
|
74
|
+
collection;
|
|
75
|
+
keys;
|
|
76
|
+
label;
|
|
77
|
+
constructor(collection, keys) {
|
|
78
|
+
super();
|
|
79
|
+
this.collection = collection;
|
|
80
|
+
this.keys = keys;
|
|
81
|
+
this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
|
|
82
|
+
this.freeze();
|
|
83
|
+
}
|
|
84
|
+
toOp() {
|
|
85
|
+
return dropIndex(this.collection, this.keys);
|
|
86
|
+
}
|
|
87
|
+
renderTypeScript() {
|
|
88
|
+
return `dropIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var CreateCollectionCall = class extends OpFactoryCallNode {
|
|
92
|
+
factoryName = "createCollection";
|
|
93
|
+
operationClass = "additive";
|
|
94
|
+
collection;
|
|
95
|
+
options;
|
|
96
|
+
label;
|
|
97
|
+
constructor(collection, options) {
|
|
98
|
+
super();
|
|
99
|
+
this.collection = collection;
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.label = `Create collection ${collection}`;
|
|
102
|
+
this.freeze();
|
|
103
|
+
}
|
|
104
|
+
toOp() {
|
|
105
|
+
return createCollection(this.collection, this.options);
|
|
106
|
+
}
|
|
107
|
+
renderTypeScript() {
|
|
108
|
+
return this.options ? `createCollection(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})` : `createCollection(${jsonToTsSource(this.collection)})`;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var DropCollectionCall = class extends OpFactoryCallNode {
|
|
112
|
+
factoryName = "dropCollection";
|
|
113
|
+
operationClass = "destructive";
|
|
114
|
+
collection;
|
|
115
|
+
label;
|
|
116
|
+
constructor(collection) {
|
|
117
|
+
super();
|
|
118
|
+
this.collection = collection;
|
|
119
|
+
this.label = `Drop collection ${collection}`;
|
|
120
|
+
this.freeze();
|
|
121
|
+
}
|
|
122
|
+
toOp() {
|
|
123
|
+
return dropCollection(this.collection);
|
|
124
|
+
}
|
|
125
|
+
renderTypeScript() {
|
|
126
|
+
return `dropCollection(${jsonToTsSource(this.collection)})`;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var CollModCall = class extends OpFactoryCallNode {
|
|
130
|
+
factoryName = "collMod";
|
|
131
|
+
collection;
|
|
132
|
+
options;
|
|
133
|
+
meta;
|
|
134
|
+
operationClass;
|
|
135
|
+
label;
|
|
136
|
+
constructor(collection, options, meta) {
|
|
137
|
+
super();
|
|
138
|
+
this.collection = collection;
|
|
139
|
+
this.options = options;
|
|
140
|
+
this.meta = meta;
|
|
141
|
+
this.operationClass = meta?.operationClass ?? "destructive";
|
|
142
|
+
this.label = meta?.label ?? `Modify collection ${collection}`;
|
|
143
|
+
this.freeze();
|
|
144
|
+
}
|
|
145
|
+
toOp() {
|
|
146
|
+
return collMod(this.collection, this.options, this.meta);
|
|
147
|
+
}
|
|
148
|
+
renderTypeScript() {
|
|
149
|
+
return this.meta ? `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)}, ${jsonToTsSource(this.meta)})` : `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function schemaIndexToCreateIndexOptions(index) {
|
|
153
|
+
return {
|
|
154
|
+
...index.unique ? { unique: true } : {},
|
|
155
|
+
...ifDefined("sparse", index.sparse),
|
|
156
|
+
...ifDefined("expireAfterSeconds", index.expireAfterSeconds),
|
|
157
|
+
...ifDefined("partialFilterExpression", index.partialFilterExpression),
|
|
158
|
+
...ifDefined("wildcardProjection", index.wildcardProjection),
|
|
159
|
+
...ifDefined("collation", index.collation),
|
|
160
|
+
...ifDefined("weights", index.weights),
|
|
161
|
+
...ifDefined("default_language", index.default_language),
|
|
162
|
+
...ifDefined("language_override", index.language_override)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function schemaCollectionToCreateCollectionOptions(coll) {
|
|
166
|
+
const opts = coll.options;
|
|
167
|
+
const validator = coll.validator;
|
|
168
|
+
if (!opts && !validator) return void 0;
|
|
169
|
+
return {
|
|
170
|
+
...opts?.capped ? { capped: true } : {},
|
|
171
|
+
...ifDefined("size", opts?.capped?.size),
|
|
172
|
+
...ifDefined("max", opts?.capped?.max),
|
|
173
|
+
...ifDefined("timeseries", opts?.timeseries),
|
|
174
|
+
...ifDefined("collation", opts?.collation),
|
|
175
|
+
...opts?.clusteredIndex ? { clusteredIndex: {
|
|
176
|
+
key: { _id: 1 },
|
|
177
|
+
unique: true,
|
|
178
|
+
...opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}
|
|
179
|
+
} } : {},
|
|
180
|
+
...validator ? { validator: { $jsonSchema: validator.jsonSchema } } : {},
|
|
181
|
+
...ifDefined("validationLevel", validator?.validationLevel),
|
|
182
|
+
...ifDefined("validationAction", validator?.validationAction),
|
|
183
|
+
...ifDefined("changeStreamPreAndPostImages", opts?.changeStreamPreAndPostImages)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function renderOps(calls) {
|
|
187
|
+
return calls.map((call) => call.toOp());
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Always-present base imports for the rendered scaffold, both from the
|
|
191
|
+
* target's own `migration` entry:
|
|
192
|
+
*
|
|
193
|
+
* - `Migration` — the user-facing Mongo `Migration` base, forwarded from
|
|
194
|
+
* `@internal/family-mongo`; subclasses don't need to redeclare
|
|
195
|
+
* `targetId` or thread family/target generics.
|
|
196
|
+
* - `MigrationCLI` — the migration-file CLI entrypoint that loads
|
|
197
|
+
* `prisma-next.config.ts`, assembles a `ControlStack`, and instantiates
|
|
198
|
+
* the migration class. The migration file owns this dependency directly:
|
|
199
|
+
* pulling CLI machinery in at script run time is acceptable because the
|
|
200
|
+
* script's whole purpose is to be invoked from the project that owns the
|
|
201
|
+
* config.
|
|
202
|
+
*
|
|
203
|
+
* Naming one package rather than three is what lets an application that
|
|
204
|
+
* installed only `@prisma/orm-mongo` run a scaffolded migration: emitted
|
|
205
|
+
* code may name only a direct dependency (ADR 242), and the facade carries
|
|
206
|
+
* the target's `migration` entry but neither the family base nor the CLI.
|
|
207
|
+
*/
|
|
208
|
+
const BASE_IMPORTS = [{
|
|
209
|
+
moduleSpecifier: TARGET_MIGRATION_MODULE,
|
|
210
|
+
symbol: "Migration"
|
|
211
|
+
}, {
|
|
212
|
+
moduleSpecifier: TARGET_MIGRATION_MODULE,
|
|
213
|
+
symbol: "MigrationCLI"
|
|
214
|
+
}];
|
|
215
|
+
/**
|
|
216
|
+
* Render a list of Mongo `OpFactoryCall`s as a `migration.ts` source string.
|
|
217
|
+
* The result is shebanged, imports the contract JSON from the shared
|
|
218
|
+
* snapshot store (the destination contract, plus the source contract for a
|
|
219
|
+
* non-baseline migration), extends `Migration<Start, End>` (or
|
|
220
|
+
* `Migration<never, End>` for a baseline) from
|
|
221
|
+
* `@internal/target-mongo/migration`,
|
|
222
|
+
* assigns the JSON to `endContractJson` / `startContractJson`, and implements
|
|
223
|
+
* `operations`. The `Migration` base derives `describe()` from those fields.
|
|
224
|
+
*
|
|
225
|
+
* The walk is polymorphic: each call node contributes its own
|
|
226
|
+
* `renderTypeScript()` expression and declares its own `importRequirements()`.
|
|
227
|
+
* The top-level renderer aggregates imports across all nodes and emits one
|
|
228
|
+
* `import { … } from "…"` line per module. The `Migration` / `MigrationCLI`
|
|
229
|
+
* base imports and the contract-JSON imports are always emitted, independent
|
|
230
|
+
* of the call nodes.
|
|
231
|
+
*/
|
|
232
|
+
function renderCallsToTypeScript(calls, meta) {
|
|
233
|
+
const imports = buildImports(calls, meta);
|
|
234
|
+
const operationsBody = calls.map((c) => c.renderTypeScript()).join(",\n");
|
|
235
|
+
const hasStart = meta.from !== null;
|
|
236
|
+
const startField = hasStart ? [" override readonly startContractJson = startContract;"] : [];
|
|
237
|
+
return [
|
|
238
|
+
shebangLineFor(detectScaffoldRuntime()),
|
|
239
|
+
imports,
|
|
240
|
+
"",
|
|
241
|
+
`class M extends Migration<${hasStart ? "Start" : "never"}, End> {`,
|
|
242
|
+
...startField,
|
|
243
|
+
" override readonly endContractJson = endContract;",
|
|
244
|
+
"",
|
|
245
|
+
" override get operations() {",
|
|
246
|
+
" return [",
|
|
247
|
+
indent(operationsBody, 6),
|
|
248
|
+
" ];",
|
|
249
|
+
" }",
|
|
250
|
+
"}",
|
|
251
|
+
"",
|
|
252
|
+
"export default M;",
|
|
253
|
+
"MigrationCLI.run(import.meta.url, M);",
|
|
254
|
+
""
|
|
255
|
+
].join("\n");
|
|
256
|
+
}
|
|
257
|
+
function buildImports(calls, meta) {
|
|
258
|
+
const requirements = [...BASE_IMPORTS, ...contractImports(meta)];
|
|
259
|
+
for (const call of calls) for (const req of call.importRequirements()) requirements.push(req);
|
|
260
|
+
return renderImports(resolveRequirementSpecifiers(requirements, meta.resolveImportSpecifier));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The committed contract-JSON imports the scaffold reads its from/to identity
|
|
264
|
+
* from, resolved to the deduplicated snapshot store under
|
|
265
|
+
* `meta.snapshotsImportPath`. The end snapshot is always present; the start
|
|
266
|
+
* snapshot is added only for a non-baseline migration (`meta.from !== null`).
|
|
267
|
+
* The matching `Contract` type imports (aliased `Start`/`End`) feed the
|
|
268
|
+
* `Migration<Start, End>` generics. Baseline emits `Migration<never, End>` with
|
|
269
|
+
* no start imports — `never` is the honest "no prior contract" Start.
|
|
270
|
+
*/
|
|
271
|
+
function contractImports(meta) {
|
|
272
|
+
const reqs = [{
|
|
273
|
+
moduleSpecifier: contractSnapshotJsonSpecifier(meta.snapshotsImportPath, meta.to),
|
|
274
|
+
symbol: "endContract",
|
|
275
|
+
kind: "default",
|
|
276
|
+
attributes: { type: "json" }
|
|
277
|
+
}, {
|
|
278
|
+
moduleSpecifier: contractSnapshotTypesSpecifier(meta.snapshotsImportPath, meta.to),
|
|
279
|
+
symbol: "Contract",
|
|
280
|
+
alias: "End",
|
|
281
|
+
typeOnly: true
|
|
282
|
+
}];
|
|
283
|
+
if (meta.from !== null) {
|
|
284
|
+
reqs.push({
|
|
285
|
+
moduleSpecifier: contractSnapshotJsonSpecifier(meta.snapshotsImportPath, meta.from),
|
|
286
|
+
symbol: "startContract",
|
|
287
|
+
kind: "default",
|
|
288
|
+
attributes: { type: "json" }
|
|
289
|
+
});
|
|
290
|
+
reqs.push({
|
|
291
|
+
moduleSpecifier: contractSnapshotTypesSpecifier(meta.snapshotsImportPath, meta.from),
|
|
292
|
+
symbol: "Contract",
|
|
293
|
+
alias: "Start",
|
|
294
|
+
typeOnly: true
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return reqs;
|
|
298
|
+
}
|
|
299
|
+
function indent(text, spaces) {
|
|
300
|
+
const pad = " ".repeat(spaces);
|
|
301
|
+
return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : line).join("\n");
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Planner-produced Mongo migration, returned by `MongoMigrationPlanner.plan(...)`
|
|
305
|
+
* and `MongoMigrationPlanner.emptyMigration(...)`.
|
|
306
|
+
*
|
|
307
|
+
* Unlike user-authored migrations (which extend `MongoMigration` from
|
|
308
|
+
* `@internal/family-mongo`), this class lives inside the target and holds
|
|
309
|
+
* the richer authoring IR (`OpFactoryCall[]`) needed to render itself back to
|
|
310
|
+
* TypeScript source. It implements `MigrationPlanWithAuthoringSurface` so
|
|
311
|
+
* that the CLI can uniformly ask any planner result to serialize itself to a
|
|
312
|
+
* `migration.ts`.
|
|
313
|
+
*
|
|
314
|
+
* It extends the framework `Migration` base directly and declares its own
|
|
315
|
+
* `targetId` rather than inheriting `MongoMigration`, whose added value is
|
|
316
|
+
* the lazily-built contract views a planner result never reads.
|
|
317
|
+
*/
|
|
318
|
+
var PlannerProducedMongoMigration = class extends Migration {
|
|
319
|
+
calls;
|
|
320
|
+
meta;
|
|
321
|
+
snapshotsImportPath;
|
|
322
|
+
targetId = "mongo";
|
|
323
|
+
constructor(calls, meta, snapshotsImportPath) {
|
|
324
|
+
super();
|
|
325
|
+
this.calls = calls;
|
|
326
|
+
this.meta = meta;
|
|
327
|
+
this.snapshotsImportPath = snapshotsImportPath;
|
|
328
|
+
}
|
|
329
|
+
get operations() {
|
|
330
|
+
return renderOps(this.calls);
|
|
331
|
+
}
|
|
332
|
+
describe() {
|
|
333
|
+
return this.meta;
|
|
334
|
+
}
|
|
335
|
+
renderTypeScript(resolveImportSpecifier) {
|
|
336
|
+
return renderCallsToTypeScript(this.calls, {
|
|
337
|
+
from: this.meta.from,
|
|
338
|
+
to: this.meta.to,
|
|
339
|
+
snapshotsImportPath: this.snapshotsImportPath,
|
|
340
|
+
resolveImportSpecifier
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function buildIndexLookupKey(index) {
|
|
345
|
+
const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
|
|
346
|
+
const opts = [
|
|
347
|
+
index.unique ? "unique" : "",
|
|
348
|
+
index.sparse ? "sparse" : "",
|
|
349
|
+
index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : "",
|
|
350
|
+
index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : "",
|
|
351
|
+
index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : "",
|
|
352
|
+
index.collation ? `col:${canonicalize(index.collation)}` : "",
|
|
353
|
+
index.weights ? `wt:${canonicalize(index.weights)}` : "",
|
|
354
|
+
index.default_language ? `dl:${index.default_language}` : "",
|
|
355
|
+
index.language_override ? `lo:${index.language_override}` : ""
|
|
356
|
+
].filter(Boolean).join(";");
|
|
357
|
+
return opts ? `${keys}|${opts}` : keys;
|
|
358
|
+
}
|
|
359
|
+
function validatorsEqual(a, b) {
|
|
360
|
+
if (!a && !b) return true;
|
|
361
|
+
if (!a || !b) return false;
|
|
362
|
+
return a.validationLevel === b.validationLevel && a.validationAction === b.validationAction && canonicalize(a.jsonSchema) === canonicalize(b.jsonSchema);
|
|
363
|
+
}
|
|
364
|
+
function classifyValidatorUpdate(origin, dest) {
|
|
365
|
+
if (origin.validationAction !== dest.validationAction && dest.validationAction === "error") return "destructive";
|
|
366
|
+
if (origin.validationLevel !== dest.validationLevel && dest.validationLevel === "strict") return "destructive";
|
|
367
|
+
if (canonicalize(origin.jsonSchema) === canonicalize(dest.jsonSchema)) return "widening";
|
|
368
|
+
return isWideningSchemaChange(origin.jsonSchema, dest.jsonSchema) ? "widening" : "destructive";
|
|
369
|
+
}
|
|
370
|
+
function isPlainObject(v) {
|
|
371
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Returns true when `dest` is a structural superset of `origin` for the common
|
|
375
|
+
* additive case: adding non-required properties to a top-level object schema.
|
|
376
|
+
* Anything uncertain falls through to the safe `destructive` default.
|
|
377
|
+
*/
|
|
378
|
+
function isWideningSchemaChange(origin, dest) {
|
|
379
|
+
if (origin["bsonType"] !== "object" || dest["bsonType"] !== "object") return false;
|
|
380
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(origin), ...Object.keys(dest)]);
|
|
381
|
+
for (const key of allKeys) {
|
|
382
|
+
if (key === "required" || key === "properties") continue;
|
|
383
|
+
if (canonicalize(origin[key]) !== canonicalize(dest[key])) return false;
|
|
384
|
+
}
|
|
385
|
+
const originRequired = new Set(Array.isArray(origin["required"]) ? origin["required"] : []);
|
|
386
|
+
const destRequired = Array.isArray(dest["required"]) ? dest["required"] : [];
|
|
387
|
+
for (const field of destRequired) if (!originRequired.has(field)) return false;
|
|
388
|
+
const originProps = isPlainObject(origin["properties"]) ? origin["properties"] : {};
|
|
389
|
+
const destProps = isPlainObject(dest["properties"]) ? dest["properties"] : {};
|
|
390
|
+
for (const field of Object.keys(originProps)) {
|
|
391
|
+
if (!Object.hasOwn(destProps, field)) return false;
|
|
392
|
+
if (canonicalize(originProps[field]) !== canonicalize(destProps[field])) return false;
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function hasImmutableOptionChange(origin, dest) {
|
|
397
|
+
if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
|
|
398
|
+
if (canonicalize(origin?.timeseries) !== canonicalize(dest?.timeseries)) return "timeseries";
|
|
399
|
+
if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
|
|
400
|
+
if (canonicalize(origin?.clusteredIndex) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
|
|
401
|
+
}
|
|
402
|
+
function collectionHasOptions(coll) {
|
|
403
|
+
return !!(coll.options || coll.validator);
|
|
404
|
+
}
|
|
405
|
+
var MongoMigrationPlanner = class {
|
|
406
|
+
planCalls(options) {
|
|
407
|
+
const contract = options.contract;
|
|
408
|
+
const originIR = options.schema;
|
|
409
|
+
const destinationIR = contractToMongoSchemaIR(contract);
|
|
410
|
+
const collCreates = [];
|
|
411
|
+
const drops = [];
|
|
412
|
+
const creates = [];
|
|
413
|
+
const validatorOps = [];
|
|
414
|
+
const mutableOptionOps = [];
|
|
415
|
+
const collDrops = [];
|
|
416
|
+
const conflicts = [];
|
|
417
|
+
const allCollectionNames = /* @__PURE__ */ new Set([...originIR.collectionNames, ...destinationIR.collectionNames]);
|
|
418
|
+
for (const collName of [...allCollectionNames].sort()) {
|
|
419
|
+
const originColl = originIR.collection(collName);
|
|
420
|
+
const destColl = destinationIR.collection(collName);
|
|
421
|
+
if (!originColl) {
|
|
422
|
+
if (destColl && (collectionHasOptions(destColl) || destColl.indexes.length === 0)) {
|
|
423
|
+
const opts = collectionHasOptions(destColl) ? schemaCollectionToCreateCollectionOptions(destColl) : void 0;
|
|
424
|
+
collCreates.push(new CreateCollectionCall(collName, opts));
|
|
425
|
+
}
|
|
426
|
+
} else if (!destColl) collDrops.push(new DropCollectionCall(collName));
|
|
427
|
+
else {
|
|
428
|
+
const immutableChange = hasImmutableOptionChange(originColl.options, destColl.options);
|
|
429
|
+
if (immutableChange) conflicts.push({
|
|
430
|
+
kind: "policy-violation",
|
|
431
|
+
summary: `Cannot change immutable collection option '${immutableChange}' on ${collName}`,
|
|
432
|
+
why: `MongoDB does not support modifying the '${immutableChange}' option after collection creation`
|
|
433
|
+
});
|
|
434
|
+
const mutableCall = planMutableOptionsDiffCall(collName, originColl.options, destColl.options);
|
|
435
|
+
if (mutableCall) mutableOptionOps.push(mutableCall);
|
|
436
|
+
const validatorCall = planValidatorDiffCall(collName, originColl.validator, destColl.validator);
|
|
437
|
+
if (validatorCall) validatorOps.push(validatorCall);
|
|
438
|
+
}
|
|
439
|
+
const originLookup = /* @__PURE__ */ new Map();
|
|
440
|
+
if (originColl) for (const idx of originColl.indexes) originLookup.set(buildIndexLookupKey(idx), idx);
|
|
441
|
+
const destLookup = /* @__PURE__ */ new Map();
|
|
442
|
+
if (destColl) for (const idx of destColl.indexes) destLookup.set(buildIndexLookupKey(idx), idx);
|
|
443
|
+
for (const [lookupKey, idx] of originLookup) if (!destLookup.has(lookupKey)) drops.push(new DropIndexCall(collName, idx.keys));
|
|
444
|
+
for (const [lookupKey, idx] of destLookup) if (!originLookup.has(lookupKey)) creates.push(new CreateIndexCall(collName, idx.keys, schemaIndexToCreateIndexOptions(idx)));
|
|
445
|
+
}
|
|
446
|
+
if (conflicts.length > 0) return {
|
|
447
|
+
kind: "failure",
|
|
448
|
+
conflicts
|
|
449
|
+
};
|
|
450
|
+
const allCalls = [
|
|
451
|
+
...collCreates,
|
|
452
|
+
...drops,
|
|
453
|
+
...creates,
|
|
454
|
+
...validatorOps,
|
|
455
|
+
...mutableOptionOps,
|
|
456
|
+
...collDrops
|
|
457
|
+
];
|
|
458
|
+
for (const call of allCalls) if (!options.policy.allowedOperationClasses.includes(call.operationClass)) conflicts.push({
|
|
459
|
+
kind: "policy-violation",
|
|
460
|
+
summary: `${call.operationClass} operation disallowed: ${call.label}`,
|
|
461
|
+
why: `Policy does not allow '${call.operationClass}' operations`
|
|
462
|
+
});
|
|
463
|
+
if (conflicts.length > 0) return {
|
|
464
|
+
kind: "failure",
|
|
465
|
+
conflicts
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
kind: "success",
|
|
469
|
+
calls: allCalls
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
plan(options) {
|
|
473
|
+
const contract = options.contract;
|
|
474
|
+
const result = this.planCalls(options);
|
|
475
|
+
if (result.kind === "failure") return result;
|
|
476
|
+
return {
|
|
477
|
+
kind: "success",
|
|
478
|
+
plan: new PlannerProducedMongoMigration(result.calls, {
|
|
479
|
+
from: options.fromContract?.storage.storageHash ?? null,
|
|
480
|
+
to: contract.storage.storageHash
|
|
481
|
+
}, options.snapshotsImportPath)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Produce an empty `migration.ts` authoring surface for `migration new`.
|
|
486
|
+
*
|
|
487
|
+
* The "empty migration" is a `PlannerProducedMongoMigration` with no
|
|
488
|
+
* operations; `renderTypeScript()` emits a stub class with the correct
|
|
489
|
+
* `from`/`to` metadata that the user then fills in with operations. The
|
|
490
|
+
* contract path on the context is unused — Mongo's emitted source does
|
|
491
|
+
* not import from the generated contract `.d.ts`.
|
|
492
|
+
*/
|
|
493
|
+
emptyMigration(context) {
|
|
494
|
+
return new PlannerProducedMongoMigration([], {
|
|
495
|
+
from: context.fromHash,
|
|
496
|
+
to: context.toHash
|
|
497
|
+
}, context.snapshotsImportPath);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
function planValidatorDiffCall(collName, originValidator, destValidator) {
|
|
501
|
+
if (validatorsEqual(originValidator, destValidator)) return void 0;
|
|
502
|
+
if (destValidator) {
|
|
503
|
+
const operationClass = originValidator ? classifyValidatorUpdate(originValidator, destValidator) : "destructive";
|
|
504
|
+
return new CollModCall(collName, {
|
|
505
|
+
validator: { $jsonSchema: destValidator.jsonSchema },
|
|
506
|
+
validationLevel: destValidator.validationLevel,
|
|
507
|
+
validationAction: destValidator.validationAction
|
|
508
|
+
}, {
|
|
509
|
+
id: `validator.${collName}.${originValidator ? "update" : "add"}`,
|
|
510
|
+
label: `${originValidator ? "Update" : "Add"} validator on ${collName}`,
|
|
511
|
+
operationClass
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return new CollModCall(collName, {
|
|
515
|
+
validator: {},
|
|
516
|
+
validationLevel: "strict",
|
|
517
|
+
validationAction: "error"
|
|
518
|
+
}, {
|
|
519
|
+
id: `validator.${collName}.remove`,
|
|
520
|
+
label: `Remove validator on ${collName}`,
|
|
521
|
+
operationClass: "widening"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function planMutableOptionsDiffCall(collName, origin, dest) {
|
|
525
|
+
const originCSPPI = origin?.changeStreamPreAndPostImages;
|
|
526
|
+
const destCSPPI = dest?.changeStreamPreAndPostImages;
|
|
527
|
+
if (deepEqual(originCSPPI, destCSPPI)) return void 0;
|
|
528
|
+
const desiredCSPPI = destCSPPI ?? { enabled: false };
|
|
529
|
+
return new CollModCall(collName, { changeStreamPreAndPostImages: desiredCSPPI }, {
|
|
530
|
+
id: `options.${collName}.update`,
|
|
531
|
+
label: `Update mutable options on ${collName}`,
|
|
532
|
+
operationClass: desiredCSPPI.enabled ? "widening" : "destructive"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function mongoTargetError(code, message, options) {
|
|
536
|
+
return structuredError(code, message, options);
|
|
537
|
+
}
|
|
538
|
+
function getNestedField(doc, path) {
|
|
539
|
+
const parts = path.split(".");
|
|
540
|
+
let current = doc;
|
|
541
|
+
for (const part of parts) {
|
|
542
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
543
|
+
const record = current;
|
|
544
|
+
if (!Object.hasOwn(record, part)) return;
|
|
545
|
+
current = record[part];
|
|
546
|
+
}
|
|
547
|
+
return current;
|
|
548
|
+
}
|
|
549
|
+
function evaluateFieldOp(op, actual, expected) {
|
|
550
|
+
switch (op) {
|
|
551
|
+
case "$eq": return deepEqual(actual, expected);
|
|
552
|
+
case "$ne": return !deepEqual(actual, expected);
|
|
553
|
+
case "$gt": return typeof actual === typeof expected && actual > expected;
|
|
554
|
+
case "$gte": return typeof actual === typeof expected && actual >= expected;
|
|
555
|
+
case "$lt": return typeof actual === typeof expected && actual < expected;
|
|
556
|
+
case "$lte": return typeof actual === typeof expected && actual <= expected;
|
|
557
|
+
case "$in": return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
|
|
558
|
+
default: throw mongoTargetError("MIGRATION.OPERATION_UNSUPPORTED", `Unsupported filter operator in migration check: ${op}`, { meta: { operator: op } });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
var FilterEvaluator = class {
|
|
562
|
+
doc = {};
|
|
563
|
+
evaluate(filter, doc) {
|
|
564
|
+
this.doc = doc;
|
|
565
|
+
return filter.accept(this);
|
|
566
|
+
}
|
|
567
|
+
field(expr) {
|
|
568
|
+
const value = getNestedField(this.doc, expr.field);
|
|
569
|
+
return evaluateFieldOp(expr.op, value, expr.value);
|
|
570
|
+
}
|
|
571
|
+
and(expr) {
|
|
572
|
+
return expr.exprs.every((child) => child.accept(this));
|
|
573
|
+
}
|
|
574
|
+
or(expr) {
|
|
575
|
+
return expr.exprs.some((child) => child.accept(this));
|
|
576
|
+
}
|
|
577
|
+
not(expr) {
|
|
578
|
+
return !expr.expr.accept(this);
|
|
579
|
+
}
|
|
580
|
+
exists(expr) {
|
|
581
|
+
const has = getNestedField(this.doc, expr.field) !== void 0;
|
|
582
|
+
return expr.exists ? has : !has;
|
|
583
|
+
}
|
|
584
|
+
expr(_expr) {
|
|
585
|
+
throw mongoTargetError("MIGRATION.OPERATION_UNSUPPORTED", "Aggregation expression filters are not supported in migration checks", { meta: { operator: "$expr" } });
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const IndexKeyJson = type({
|
|
589
|
+
field: "string",
|
|
590
|
+
direction: type("1 | -1 | \"text\" | \"2dsphere\" | \"2d\" | \"hashed\"")
|
|
591
|
+
});
|
|
592
|
+
const CollationJson = type({
|
|
593
|
+
locale: "string",
|
|
594
|
+
"caseLevel?": "boolean",
|
|
595
|
+
"caseFirst?": "string",
|
|
596
|
+
"strength?": "number",
|
|
597
|
+
"numericOrdering?": "boolean",
|
|
598
|
+
"alternate?": "string",
|
|
599
|
+
"maxVariable?": "string",
|
|
600
|
+
"backwards?": "boolean",
|
|
601
|
+
"normalization?": "boolean"
|
|
602
|
+
});
|
|
603
|
+
const CreateIndexJson = type({
|
|
604
|
+
kind: "\"createIndex\"",
|
|
605
|
+
collection: "string",
|
|
606
|
+
keys: IndexKeyJson.array().atLeastLength(1),
|
|
607
|
+
"unique?": "boolean",
|
|
608
|
+
"sparse?": "boolean",
|
|
609
|
+
"expireAfterSeconds?": "number",
|
|
610
|
+
"partialFilterExpression?": "Record<string, unknown>",
|
|
611
|
+
"name?": "string",
|
|
612
|
+
"wildcardProjection?": "Record<string, 0 | 1>",
|
|
613
|
+
"collation?": CollationJson,
|
|
614
|
+
"weights?": "Record<string, number>",
|
|
615
|
+
"default_language?": "string",
|
|
616
|
+
"language_override?": "string"
|
|
617
|
+
});
|
|
618
|
+
const DropIndexJson = type({
|
|
619
|
+
kind: "\"dropIndex\"",
|
|
620
|
+
collection: "string",
|
|
621
|
+
name: "string"
|
|
622
|
+
});
|
|
623
|
+
const CreateCollectionJson = type({
|
|
624
|
+
kind: "\"createCollection\"",
|
|
625
|
+
collection: "string",
|
|
626
|
+
"validator?": "Record<string, unknown>",
|
|
627
|
+
"validationLevel?": "\"strict\" | \"moderate\"",
|
|
628
|
+
"validationAction?": "\"error\" | \"warn\"",
|
|
629
|
+
"capped?": "boolean",
|
|
630
|
+
"size?": "number",
|
|
631
|
+
"max?": "number",
|
|
632
|
+
"timeseries?": {
|
|
633
|
+
timeField: "string",
|
|
634
|
+
"metaField?": "string",
|
|
635
|
+
"granularity?": "\"seconds\" | \"minutes\" | \"hours\""
|
|
636
|
+
},
|
|
637
|
+
"collation?": CollationJson,
|
|
638
|
+
"changeStreamPreAndPostImages?": { enabled: "boolean" },
|
|
639
|
+
"clusteredIndex?": {
|
|
640
|
+
key: "Record<string, number>",
|
|
641
|
+
unique: "boolean",
|
|
642
|
+
"name?": "string"
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
const DropCollectionJson = type({
|
|
646
|
+
kind: "\"dropCollection\"",
|
|
647
|
+
collection: "string"
|
|
648
|
+
});
|
|
649
|
+
const CollModJson = type({
|
|
650
|
+
kind: "\"collMod\"",
|
|
651
|
+
collection: "string",
|
|
652
|
+
"validator?": "Record<string, unknown>",
|
|
653
|
+
"validationLevel?": "\"strict\" | \"moderate\"",
|
|
654
|
+
"validationAction?": "\"error\" | \"warn\"",
|
|
655
|
+
"changeStreamPreAndPostImages?": { enabled: "boolean" }
|
|
656
|
+
});
|
|
657
|
+
const ListIndexesJson = type({
|
|
658
|
+
kind: "\"listIndexes\"",
|
|
659
|
+
collection: "string"
|
|
660
|
+
});
|
|
661
|
+
const ListCollectionsJson = type({ kind: "\"listCollections\"" });
|
|
662
|
+
const FieldFilterJson = type({
|
|
663
|
+
kind: "\"field\"",
|
|
664
|
+
field: "string",
|
|
665
|
+
op: "string",
|
|
666
|
+
value: "unknown"
|
|
667
|
+
});
|
|
668
|
+
const ExistsFilterJson = type({
|
|
669
|
+
kind: "\"exists\"",
|
|
670
|
+
field: "string",
|
|
671
|
+
exists: "boolean"
|
|
672
|
+
});
|
|
673
|
+
const RawInsertOneJson = type({
|
|
674
|
+
kind: "\"rawInsertOne\"",
|
|
675
|
+
collection: "string",
|
|
676
|
+
document: "Record<string, unknown>"
|
|
677
|
+
});
|
|
678
|
+
const RawInsertManyJson = type({
|
|
679
|
+
kind: "\"rawInsertMany\"",
|
|
680
|
+
collection: "string",
|
|
681
|
+
documents: "Record<string, unknown>[]"
|
|
682
|
+
});
|
|
683
|
+
const RawUpdateOneJson = type({
|
|
684
|
+
kind: "\"rawUpdateOne\"",
|
|
685
|
+
collection: "string",
|
|
686
|
+
filter: "Record<string, unknown>",
|
|
687
|
+
update: "Record<string, unknown> | Record<string, unknown>[]"
|
|
688
|
+
});
|
|
689
|
+
const RawUpdateManyJson = type({
|
|
690
|
+
kind: "\"rawUpdateMany\"",
|
|
691
|
+
collection: "string",
|
|
692
|
+
filter: "Record<string, unknown>",
|
|
693
|
+
update: "Record<string, unknown> | Record<string, unknown>[]"
|
|
694
|
+
});
|
|
695
|
+
const RawDeleteOneJson = type({
|
|
696
|
+
kind: "\"rawDeleteOne\"",
|
|
697
|
+
collection: "string",
|
|
698
|
+
filter: "Record<string, unknown>"
|
|
699
|
+
});
|
|
700
|
+
const RawDeleteManyJson = type({
|
|
701
|
+
kind: "\"rawDeleteMany\"",
|
|
702
|
+
collection: "string",
|
|
703
|
+
filter: "Record<string, unknown>"
|
|
704
|
+
});
|
|
705
|
+
const RawAggregateJson = type({
|
|
706
|
+
kind: "\"rawAggregate\"",
|
|
707
|
+
collection: "string",
|
|
708
|
+
pipeline: "Record<string, unknown>[]"
|
|
709
|
+
});
|
|
710
|
+
const RawFindOneAndUpdateJson = type({
|
|
711
|
+
kind: "\"rawFindOneAndUpdate\"",
|
|
712
|
+
collection: "string",
|
|
713
|
+
filter: "Record<string, unknown>",
|
|
714
|
+
update: "Record<string, unknown> | Record<string, unknown>[]",
|
|
715
|
+
upsert: "boolean"
|
|
716
|
+
});
|
|
717
|
+
const RawFindOneAndDeleteJson = type({
|
|
718
|
+
kind: "\"rawFindOneAndDelete\"",
|
|
719
|
+
collection: "string",
|
|
720
|
+
filter: "Record<string, unknown>"
|
|
721
|
+
});
|
|
722
|
+
const TypedAggregateJson = type({
|
|
723
|
+
kind: "\"aggregate\"",
|
|
724
|
+
collection: "string",
|
|
725
|
+
pipeline: "Record<string, unknown>[]"
|
|
726
|
+
});
|
|
727
|
+
const QueryPlanJson = type({
|
|
728
|
+
collection: "string",
|
|
729
|
+
command: "Record<string, unknown>",
|
|
730
|
+
meta: type({
|
|
731
|
+
target: "string",
|
|
732
|
+
storageHash: "string",
|
|
733
|
+
lane: "string",
|
|
734
|
+
"targetFamily?": "string",
|
|
735
|
+
"profileHash?": "string",
|
|
736
|
+
"annotations?": "Record<string, unknown>"
|
|
737
|
+
})
|
|
738
|
+
});
|
|
739
|
+
const CheckJson = type({
|
|
740
|
+
description: "string",
|
|
741
|
+
source: "Record<string, unknown>",
|
|
742
|
+
filter: "Record<string, unknown>",
|
|
743
|
+
expect: "\"exists\" | \"notExists\""
|
|
744
|
+
});
|
|
745
|
+
const StepJson = type({
|
|
746
|
+
description: "string",
|
|
747
|
+
command: "Record<string, unknown>"
|
|
748
|
+
});
|
|
749
|
+
const DdlOperationJson = type({
|
|
750
|
+
id: "string",
|
|
751
|
+
label: "string",
|
|
752
|
+
operationClass: "\"additive\" | \"widening\" | \"destructive\"",
|
|
753
|
+
precheck: "Record<string, unknown>[]",
|
|
754
|
+
execute: "Record<string, unknown>[]",
|
|
755
|
+
postcheck: "Record<string, unknown>[]"
|
|
756
|
+
});
|
|
757
|
+
const DataTransformCheckJson = type({
|
|
758
|
+
description: "string",
|
|
759
|
+
source: "Record<string, unknown>",
|
|
760
|
+
filter: "Record<string, unknown>",
|
|
761
|
+
expect: "\"exists\" | \"notExists\""
|
|
762
|
+
});
|
|
763
|
+
const DataTransformOperationJson = type({
|
|
764
|
+
id: "string",
|
|
765
|
+
label: "string",
|
|
766
|
+
operationClass: "\"data\"",
|
|
767
|
+
name: "string",
|
|
768
|
+
precheck: "Record<string, unknown>[]",
|
|
769
|
+
run: "Record<string, unknown>[]",
|
|
770
|
+
postcheck: "Record<string, unknown>[]"
|
|
771
|
+
});
|
|
772
|
+
function validate(schema, data, context) {
|
|
773
|
+
try {
|
|
774
|
+
return schema.assert(stripUndefinedDeep(data));
|
|
775
|
+
} catch (error) {
|
|
776
|
+
/* v8 ignore stop */
|
|
777
|
+
throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Invalid ${context}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
778
|
+
meta: { context },
|
|
779
|
+
cause: error
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Strip `undefined`-valued properties before they reach arktype's optional-key
|
|
785
|
+
* assertions.
|
|
786
|
+
*
|
|
787
|
+
* Op IRs (e.g. `CreateCollectionCommand`) assign every optional field on
|
|
788
|
+
* every instance — fields the caller did not provide land as
|
|
789
|
+
* `undefined`-valued properties. arktype treats `{ foo?: 'boolean' }` as
|
|
790
|
+
* "key may be absent, but if present must be boolean", so the bare instance
|
|
791
|
+
* fails validation when it crosses the deserialize boundary in-process
|
|
792
|
+
* (no JSON round-trip happens between planner → runner). This helper
|
|
793
|
+
* recovers the JSON-round-tripped shape (undefined keys absent) without
|
|
794
|
+
* forcing every caller to round-trip.
|
|
795
|
+
*
|
|
796
|
+
* Returns the original value reference whenever no change is needed.
|
|
797
|
+
* That preserves prototype-bound payload values such as BSON wrappers
|
|
798
|
+
* (`ObjectId`, `Decimal128`, `Binary`, …) which embed no `undefined`
|
|
799
|
+
* own-enumerable properties and therefore never trigger a rebuild.
|
|
800
|
+
* Top-level op IRs (class instances with `undefined` optional fields)
|
|
801
|
+
* still get flattened to plain records as required by arktype.
|
|
802
|
+
*/
|
|
803
|
+
function stripUndefinedDeep(value) {
|
|
804
|
+
if (Array.isArray(value)) {
|
|
805
|
+
let changed = false;
|
|
806
|
+
const next = value.map((item) => {
|
|
807
|
+
const stripped = stripUndefinedDeep(item);
|
|
808
|
+
if (stripped !== item) changed = true;
|
|
809
|
+
return stripped;
|
|
810
|
+
});
|
|
811
|
+
return changed ? next : value;
|
|
812
|
+
}
|
|
813
|
+
if (value === null || typeof value !== "object") return value;
|
|
814
|
+
const entries = Object.entries(value);
|
|
815
|
+
const out = {};
|
|
816
|
+
let changed = false;
|
|
817
|
+
for (const [key, val] of entries) {
|
|
818
|
+
if (val === void 0) {
|
|
819
|
+
changed = true;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const stripped = stripUndefinedDeep(val);
|
|
823
|
+
if (stripped !== val) changed = true;
|
|
824
|
+
out[key] = stripped;
|
|
825
|
+
}
|
|
826
|
+
return changed ? out : value;
|
|
827
|
+
}
|
|
828
|
+
function deserializeFilterExpr(json) {
|
|
829
|
+
const record = json;
|
|
830
|
+
const kind = record["kind"];
|
|
831
|
+
switch (kind) {
|
|
832
|
+
case "field": {
|
|
833
|
+
const data = validate(FieldFilterJson, json, "field filter");
|
|
834
|
+
return MongoFieldFilter.of(data.field, data.op, data.value);
|
|
835
|
+
}
|
|
836
|
+
case "and": {
|
|
837
|
+
const exprs = record["exprs"];
|
|
838
|
+
if (!Array.isArray(exprs)) throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", "Invalid and filter: missing exprs array", { meta: { context: "and filter" } });
|
|
839
|
+
return MongoAndExpr.of(exprs.map(deserializeFilterExpr));
|
|
840
|
+
}
|
|
841
|
+
case "or": {
|
|
842
|
+
const exprs = record["exprs"];
|
|
843
|
+
if (!Array.isArray(exprs)) throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", "Invalid or filter: missing exprs array", { meta: { context: "or filter" } });
|
|
844
|
+
return MongoOrExpr.of(exprs.map(deserializeFilterExpr));
|
|
845
|
+
}
|
|
846
|
+
case "not": {
|
|
847
|
+
const expr = record["expr"];
|
|
848
|
+
if (!expr || typeof expr !== "object") throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", "Invalid not filter: missing expr", { meta: { context: "not filter" } });
|
|
849
|
+
return new MongoNotExpr(deserializeFilterExpr(expr));
|
|
850
|
+
}
|
|
851
|
+
case "exists": {
|
|
852
|
+
const data = validate(ExistsFilterJson, json, "exists filter");
|
|
853
|
+
return new MongoExistsExpr(data.field, data.exists);
|
|
854
|
+
}
|
|
855
|
+
default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown filter expression kind: ${kind}`, { meta: {
|
|
856
|
+
context: "filter expression",
|
|
857
|
+
kind
|
|
858
|
+
} });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function deserializePipelineStage(json) {
|
|
862
|
+
const record = json;
|
|
863
|
+
const kind = record["kind"];
|
|
864
|
+
switch (kind) {
|
|
865
|
+
case "match": return new MongoMatchStage(deserializeFilterExpr(record["filter"]));
|
|
866
|
+
case "limit": return new MongoLimitStage(record["limit"]);
|
|
867
|
+
case "sort": return new MongoSortStage(record["sort"]);
|
|
868
|
+
case "project": return new MongoProjectStage(record["projection"]);
|
|
869
|
+
case "addFields": return new MongoAddFieldsStage(record["fields"]);
|
|
870
|
+
case "lookup": {
|
|
871
|
+
const opts = {
|
|
872
|
+
from: record["from"],
|
|
873
|
+
as: record["as"]
|
|
874
|
+
};
|
|
875
|
+
if (record["localField"] !== void 0) opts.localField = record["localField"];
|
|
876
|
+
if (record["foreignField"] !== void 0) opts.foreignField = record["foreignField"];
|
|
877
|
+
if (record["pipeline"] !== void 0) opts.pipeline = record["pipeline"].map(deserializePipelineStage);
|
|
878
|
+
if (record["let_"] !== void 0) opts.let_ = record["let_"];
|
|
879
|
+
return new MongoLookupStage(opts);
|
|
880
|
+
}
|
|
881
|
+
case "merge": {
|
|
882
|
+
const opts = { into: record["into"] };
|
|
883
|
+
if (record["on"] !== void 0) opts.on = record["on"];
|
|
884
|
+
if (record["whenMatched"] !== void 0) {
|
|
885
|
+
const wm = record["whenMatched"];
|
|
886
|
+
opts.whenMatched = typeof wm === "string" ? wm : wm.map(deserializePipelineStage);
|
|
887
|
+
}
|
|
888
|
+
if (record["whenNotMatched"] !== void 0) opts.whenNotMatched = record["whenNotMatched"];
|
|
889
|
+
return new MongoMergeStage(opts);
|
|
890
|
+
}
|
|
891
|
+
default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown pipeline stage kind: ${kind}`, { meta: {
|
|
892
|
+
context: "pipeline stage",
|
|
893
|
+
kind
|
|
894
|
+
} });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function deserializeDmlCommand(json) {
|
|
898
|
+
const kind = json["kind"];
|
|
899
|
+
switch (kind) {
|
|
900
|
+
case "rawInsertOne": {
|
|
901
|
+
const data = validate(RawInsertOneJson, json, "rawInsertOne command");
|
|
902
|
+
return new RawInsertOneCommand(data.collection, data.document);
|
|
903
|
+
}
|
|
904
|
+
case "rawInsertMany": {
|
|
905
|
+
const data = validate(RawInsertManyJson, json, "rawInsertMany command");
|
|
906
|
+
return new RawInsertManyCommand(data.collection, data.documents);
|
|
907
|
+
}
|
|
908
|
+
case "rawUpdateOne": {
|
|
909
|
+
const data = validate(RawUpdateOneJson, json, "rawUpdateOne command");
|
|
910
|
+
return new RawUpdateOneCommand(data.collection, data.filter, data.update);
|
|
911
|
+
}
|
|
912
|
+
case "rawUpdateMany": {
|
|
913
|
+
const data = validate(RawUpdateManyJson, json, "rawUpdateMany command");
|
|
914
|
+
return new RawUpdateManyCommand(data.collection, data.filter, data.update);
|
|
915
|
+
}
|
|
916
|
+
case "rawDeleteOne": {
|
|
917
|
+
const data = validate(RawDeleteOneJson, json, "rawDeleteOne command");
|
|
918
|
+
return new RawDeleteOneCommand(data.collection, data.filter);
|
|
919
|
+
}
|
|
920
|
+
case "rawDeleteMany": {
|
|
921
|
+
const data = validate(RawDeleteManyJson, json, "rawDeleteMany command");
|
|
922
|
+
return new RawDeleteManyCommand(data.collection, data.filter);
|
|
923
|
+
}
|
|
924
|
+
case "rawAggregate": {
|
|
925
|
+
const data = validate(RawAggregateJson, json, "rawAggregate command");
|
|
926
|
+
return new RawAggregateCommand(data.collection, data.pipeline);
|
|
927
|
+
}
|
|
928
|
+
case "rawFindOneAndUpdate": {
|
|
929
|
+
const data = validate(RawFindOneAndUpdateJson, json, "rawFindOneAndUpdate command");
|
|
930
|
+
return new RawFindOneAndUpdateCommand(data.collection, data.filter, data.update, data.upsert);
|
|
931
|
+
}
|
|
932
|
+
case "rawFindOneAndDelete": {
|
|
933
|
+
const data = validate(RawFindOneAndDeleteJson, json, "rawFindOneAndDelete command");
|
|
934
|
+
return new RawFindOneAndDeleteCommand(data.collection, data.filter);
|
|
935
|
+
}
|
|
936
|
+
case "aggregate": {
|
|
937
|
+
const data = validate(TypedAggregateJson, json, "aggregate command");
|
|
938
|
+
const pipeline = data.pipeline.map(deserializePipelineStage);
|
|
939
|
+
return new AggregateCommand(data.collection, pipeline);
|
|
940
|
+
}
|
|
941
|
+
default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown DML command kind: ${kind}`, { meta: {
|
|
942
|
+
context: "DML command",
|
|
943
|
+
kind
|
|
944
|
+
} });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function deserializeMongoQueryPlan(json) {
|
|
948
|
+
const data = validate(QueryPlanJson, json, "Mongo query plan");
|
|
949
|
+
const command = deserializeDmlCommand(data.command);
|
|
950
|
+
const m = data.meta;
|
|
951
|
+
const meta = {
|
|
952
|
+
target: m.target,
|
|
953
|
+
storageHash: m.storageHash,
|
|
954
|
+
lane: m.lane,
|
|
955
|
+
...ifDefined("targetFamily", m.targetFamily),
|
|
956
|
+
...ifDefined("profileHash", m.profileHash),
|
|
957
|
+
...ifDefined("annotations", m.annotations)
|
|
958
|
+
};
|
|
959
|
+
return {
|
|
960
|
+
collection: data.collection,
|
|
961
|
+
command,
|
|
962
|
+
meta
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function deserializeDdlCommand(json) {
|
|
966
|
+
const kind = json["kind"];
|
|
967
|
+
switch (kind) {
|
|
968
|
+
case "createIndex": {
|
|
969
|
+
const data = validate(CreateIndexJson, json, "createIndex command");
|
|
970
|
+
return new CreateIndexCommand(data.collection, data.keys, {
|
|
971
|
+
...ifDefined("unique", data.unique),
|
|
972
|
+
...ifDefined("sparse", data.sparse),
|
|
973
|
+
...ifDefined("expireAfterSeconds", data.expireAfterSeconds),
|
|
974
|
+
...ifDefined("partialFilterExpression", data.partialFilterExpression),
|
|
975
|
+
...ifDefined("name", data.name),
|
|
976
|
+
...ifDefined("wildcardProjection", data.wildcardProjection),
|
|
977
|
+
...ifDefined("collation", data.collation),
|
|
978
|
+
...ifDefined("weights", data.weights),
|
|
979
|
+
...ifDefined("default_language", data.default_language),
|
|
980
|
+
...ifDefined("language_override", data.language_override)
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
case "dropIndex": {
|
|
984
|
+
const data = validate(DropIndexJson, json, "dropIndex command");
|
|
985
|
+
return new DropIndexCommand(data.collection, data.name);
|
|
986
|
+
}
|
|
987
|
+
case "createCollection": {
|
|
988
|
+
const data = validate(CreateCollectionJson, json, "createCollection command");
|
|
989
|
+
return new CreateCollectionCommand(data.collection, {
|
|
990
|
+
...ifDefined("validator", data.validator),
|
|
991
|
+
...ifDefined("validationLevel", data.validationLevel),
|
|
992
|
+
...ifDefined("validationAction", data.validationAction),
|
|
993
|
+
...ifDefined("capped", data.capped),
|
|
994
|
+
...ifDefined("size", data.size),
|
|
995
|
+
...ifDefined("max", data.max),
|
|
996
|
+
...ifDefined("timeseries", data.timeseries),
|
|
997
|
+
...ifDefined("collation", data.collation),
|
|
998
|
+
...ifDefined("changeStreamPreAndPostImages", data.changeStreamPreAndPostImages),
|
|
999
|
+
...ifDefined("clusteredIndex", data.clusteredIndex)
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
|
|
1003
|
+
case "collMod": {
|
|
1004
|
+
const data = validate(CollModJson, json, "collMod command");
|
|
1005
|
+
return new CollModCommand(data.collection, {
|
|
1006
|
+
...ifDefined("validator", data.validator),
|
|
1007
|
+
...ifDefined("validationLevel", data.validationLevel),
|
|
1008
|
+
...ifDefined("validationAction", data.validationAction),
|
|
1009
|
+
...ifDefined("changeStreamPreAndPostImages", data.changeStreamPreAndPostImages)
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown DDL command kind: ${kind}`, { meta: {
|
|
1013
|
+
context: "DDL command",
|
|
1014
|
+
kind
|
|
1015
|
+
} });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function deserializeInspectionCommand(json) {
|
|
1019
|
+
const kind = json["kind"];
|
|
1020
|
+
switch (kind) {
|
|
1021
|
+
case "listIndexes": return new ListIndexesCommand(validate(ListIndexesJson, json, "listIndexes command").collection);
|
|
1022
|
+
case "listCollections":
|
|
1023
|
+
validate(ListCollectionsJson, json, "listCollections command");
|
|
1024
|
+
return new ListCollectionsCommand();
|
|
1025
|
+
default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown inspection command kind: ${kind}`, { meta: {
|
|
1026
|
+
context: "inspection command",
|
|
1027
|
+
kind
|
|
1028
|
+
} });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function deserializeCheck(json) {
|
|
1032
|
+
const data = validate(CheckJson, json, "migration check");
|
|
1033
|
+
return {
|
|
1034
|
+
description: data.description,
|
|
1035
|
+
source: deserializeInspectionCommand(data.source),
|
|
1036
|
+
filter: deserializeFilterExpr(data.filter),
|
|
1037
|
+
expect: data.expect
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function deserializeStep(json) {
|
|
1041
|
+
const data = validate(StepJson, json, "migration step");
|
|
1042
|
+
return {
|
|
1043
|
+
description: data.description,
|
|
1044
|
+
command: deserializeDdlCommand(data.command)
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function isDataTransformJson(json) {
|
|
1048
|
+
return typeof json === "object" && json !== null && json["operationClass"] === "data";
|
|
1049
|
+
}
|
|
1050
|
+
function deserializeDdlOp(json) {
|
|
1051
|
+
const data = validate(DdlOperationJson, json, "migration operation");
|
|
1052
|
+
return {
|
|
1053
|
+
id: data.id,
|
|
1054
|
+
label: data.label,
|
|
1055
|
+
operationClass: data.operationClass,
|
|
1056
|
+
precheck: data.precheck.map(deserializeCheck),
|
|
1057
|
+
execute: data.execute.map(deserializeStep),
|
|
1058
|
+
postcheck: data.postcheck.map(deserializeCheck)
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function deserializeDataTransformCheck(json) {
|
|
1062
|
+
const data = validate(DataTransformCheckJson, json, "data transform check");
|
|
1063
|
+
return {
|
|
1064
|
+
description: data.description,
|
|
1065
|
+
source: deserializeMongoQueryPlan(data.source),
|
|
1066
|
+
filter: deserializeFilterExpr(data.filter),
|
|
1067
|
+
expect: data.expect
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function deserializeDataTransformOp(json) {
|
|
1071
|
+
const data = validate(DataTransformOperationJson, json, "data transform operation");
|
|
1072
|
+
return {
|
|
1073
|
+
id: data.id,
|
|
1074
|
+
label: data.label,
|
|
1075
|
+
operationClass: "data",
|
|
1076
|
+
name: data.name,
|
|
1077
|
+
precheck: data.precheck.map(deserializeDataTransformCheck),
|
|
1078
|
+
run: data.run.map(deserializeMongoQueryPlan),
|
|
1079
|
+
postcheck: data.postcheck.map(deserializeDataTransformCheck)
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function deserializeMongoOp(json) {
|
|
1083
|
+
if (isDataTransformJson(json)) return deserializeDataTransformOp(json);
|
|
1084
|
+
return deserializeDdlOp(json);
|
|
1085
|
+
}
|
|
1086
|
+
function deserializeMongoOps(json) {
|
|
1087
|
+
return json.map(deserializeMongoOp);
|
|
1088
|
+
}
|
|
1089
|
+
function serializeMongoOps(ops) {
|
|
1090
|
+
return JSON.stringify(ops, null, 2);
|
|
1091
|
+
}
|
|
1092
|
+
const READ_ONLY_CHECK_COMMAND_KINDS = /* @__PURE__ */ new Set(["aggregate", "rawAggregate"]);
|
|
1093
|
+
function runnerFailure(code, summary, opts) {
|
|
1094
|
+
return notOk({
|
|
1095
|
+
code,
|
|
1096
|
+
summary,
|
|
1097
|
+
...opts
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
var MongoMigrationRunner = class {
|
|
1101
|
+
deps;
|
|
1102
|
+
constructor(deps) {
|
|
1103
|
+
this.deps = deps;
|
|
1104
|
+
}
|
|
1105
|
+
async execute(options) {
|
|
1106
|
+
const { inspectionExecutor, adapter, driver, executeDdl, markerOps } = this.deps;
|
|
1107
|
+
const operations = deserializeMongoOps(options.plan.operations);
|
|
1108
|
+
const space = options.plan.spaceId ?? APP_SPACE_ID;
|
|
1109
|
+
const policyCheck = this.enforcePolicyCompatibility(options.policy, operations);
|
|
1110
|
+
if (policyCheck) return policyCheck;
|
|
1111
|
+
const existingMarker = await markerOps.readMarker(space);
|
|
1112
|
+
const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
|
|
1113
|
+
if (markerCheck) return markerCheck;
|
|
1114
|
+
const checks = options.executionChecks;
|
|
1115
|
+
const runPrechecks = checks?.prechecks !== false;
|
|
1116
|
+
const runPostchecks = checks?.postchecks !== false;
|
|
1117
|
+
const runIdempotency = checks?.idempotencyChecks !== false;
|
|
1118
|
+
const filterEvaluator = new FilterEvaluator();
|
|
1119
|
+
let operationsExecuted = 0;
|
|
1120
|
+
for (const operation of operations) {
|
|
1121
|
+
options.callbacks?.onOperationStart?.(operation);
|
|
1122
|
+
try {
|
|
1123
|
+
if (operation.operationClass === "data") {
|
|
1124
|
+
const result = await this.executeDataTransform(operation, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks);
|
|
1125
|
+
if (result.failure) return result.failure;
|
|
1126
|
+
if (result.executed) operationsExecuted += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const ddlOp = operation;
|
|
1130
|
+
if (runPostchecks && runIdempotency) {
|
|
1131
|
+
if (await this.allChecksSatisfied(ddlOp.postcheck, inspectionExecutor, filterEvaluator)) continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (runPrechecks) {
|
|
1134
|
+
if (!await this.evaluateChecks(ddlOp.precheck, inspectionExecutor, filterEvaluator)) return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${operation.id} failed during precheck`, { meta: { operationId: operation.id } });
|
|
1135
|
+
}
|
|
1136
|
+
for (const step of ddlOp.execute) await executeDdl(step.command);
|
|
1137
|
+
if (runPostchecks) {
|
|
1138
|
+
if (!await this.evaluateChecks(ddlOp.postcheck, inspectionExecutor, filterEvaluator)) return runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${operation.id} failed during postcheck`, { meta: { operationId: operation.id } });
|
|
1139
|
+
}
|
|
1140
|
+
operationsExecuted += 1;
|
|
1141
|
+
} finally {
|
|
1142
|
+
options.callbacks?.onOperationComplete?.(operation);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const destination = options.plan.destination;
|
|
1146
|
+
const profileHash = options.destinationContract.profileHash ?? destination.storageHash;
|
|
1147
|
+
const incomingInvariants = options.plan.providedInvariants ?? [];
|
|
1148
|
+
const existingInvariantSet = new Set(existingMarker?.invariants ?? []);
|
|
1149
|
+
const incomingIsSubsetOfExisting = incomingInvariants.every((id) => existingInvariantSet.has(id));
|
|
1150
|
+
const markerAlreadyAtDestination = existingMarker !== null && existingMarker.storageHash === destination.storageHash && existingMarker.profileHash === profileHash;
|
|
1151
|
+
if (!(operationsExecuted === 0 && markerAlreadyAtDestination && incomingIsSubsetOfExisting)) {
|
|
1152
|
+
const liveSchema = await this.deps.introspectSchema();
|
|
1153
|
+
const rawVerifyResult = verifyMongoSchema({
|
|
1154
|
+
contract: options.destinationContract,
|
|
1155
|
+
schema: liveSchema,
|
|
1156
|
+
strict: options.strictVerification ?? true,
|
|
1157
|
+
frameworkComponents: options.frameworkComponents,
|
|
1158
|
+
...options.context ? { context: options.context } : {}
|
|
1159
|
+
});
|
|
1160
|
+
const verifyResult = options.scopeVerifyResult ? options.scopeVerifyResult(rawVerifyResult) : rawVerifyResult;
|
|
1161
|
+
if (!verifyResult.ok) return runnerFailure("MIGRATION.SCHEMA_VERIFY_FAILED", verifyResult.summary, {
|
|
1162
|
+
why: "The resulting database schema does not satisfy the destination contract.",
|
|
1163
|
+
meta: { issues: verifyResult.schema.issues }
|
|
1164
|
+
});
|
|
1165
|
+
if (existingMarker) {
|
|
1166
|
+
if (!await markerOps.updateMarker(space, existingMarker.storageHash, {
|
|
1167
|
+
storageHash: destination.storageHash,
|
|
1168
|
+
profileHash,
|
|
1169
|
+
invariants: incomingInvariants
|
|
1170
|
+
})) return runnerFailure("MIGRATION.MARKER_CAS_FAILURE", "Marker was modified by another process during migration execution.", { meta: {
|
|
1171
|
+
space,
|
|
1172
|
+
expectedStorageHash: existingMarker.storageHash,
|
|
1173
|
+
destinationStorageHash: destination.storageHash
|
|
1174
|
+
} });
|
|
1175
|
+
} else await markerOps.initMarker(space, {
|
|
1176
|
+
storageHash: destination.storageHash,
|
|
1177
|
+
profileHash,
|
|
1178
|
+
invariants: incomingInvariants
|
|
1179
|
+
});
|
|
1180
|
+
await this.recordLedgerEntries(markerOps, space, options);
|
|
1181
|
+
}
|
|
1182
|
+
return ok({
|
|
1183
|
+
operationsPlanned: operations.length,
|
|
1184
|
+
operationsExecuted
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
async recordLedgerEntries(markerOps, space, options) {
|
|
1188
|
+
const plan = options.plan;
|
|
1189
|
+
const edges = options.migrationEdges;
|
|
1190
|
+
const totalEdgeOps = edges.reduce((sum, edge) => sum + edge.operationCount, 0);
|
|
1191
|
+
if (totalEdgeOps !== plan.operations.length) throw new InternalError(`Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`);
|
|
1192
|
+
let offset = 0;
|
|
1193
|
+
for (const edge of edges) {
|
|
1194
|
+
const edgeOps = plan.operations.slice(offset, offset + edge.operationCount);
|
|
1195
|
+
offset += edge.operationCount;
|
|
1196
|
+
await markerOps.writeLedgerEntry(space, {
|
|
1197
|
+
edgeId: `${edge.from}->${edge.to}`,
|
|
1198
|
+
from: edge.from,
|
|
1199
|
+
to: edge.to,
|
|
1200
|
+
migrationName: edge.dirName,
|
|
1201
|
+
migrationHash: edge.migrationHash,
|
|
1202
|
+
operations: edgeOps
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async executeDataTransform(op, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks) {
|
|
1207
|
+
if (runPostchecks && runIdempotency && op.postcheck.length > 0) {
|
|
1208
|
+
if (await this.evaluateDataTransformChecks(op.postcheck, adapter, driver, filterEvaluator)) return { executed: false };
|
|
1209
|
+
}
|
|
1210
|
+
if (runPrechecks && op.precheck.length > 0) {
|
|
1211
|
+
if (!await this.evaluateDataTransformChecks(op.precheck, adapter, driver, filterEvaluator)) return {
|
|
1212
|
+
executed: false,
|
|
1213
|
+
failure: runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} failed during precheck`, { meta: {
|
|
1214
|
+
operationId: op.id,
|
|
1215
|
+
name: op.name
|
|
1216
|
+
} })
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
for (const plan of op.run) {
|
|
1220
|
+
const wireCommand = await adapter.lower(plan, {});
|
|
1221
|
+
for await (const _ of driver.execute(wireCommand));
|
|
1222
|
+
}
|
|
1223
|
+
if (runPostchecks && op.postcheck.length > 0) {
|
|
1224
|
+
if (!await this.evaluateDataTransformChecks(op.postcheck, adapter, driver, filterEvaluator)) return {
|
|
1225
|
+
executed: false,
|
|
1226
|
+
failure: runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${op.id} failed during postcheck`, { meta: {
|
|
1227
|
+
operationId: op.id,
|
|
1228
|
+
name: op.name
|
|
1229
|
+
} })
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return { executed: true };
|
|
1233
|
+
}
|
|
1234
|
+
async evaluateDataTransformChecks(checks, adapter, driver, filterEvaluator) {
|
|
1235
|
+
for (const check of checks) {
|
|
1236
|
+
const commandKind = check.source.command.kind;
|
|
1237
|
+
if (!READ_ONLY_CHECK_COMMAND_KINDS.has(commandKind)) throw errorRunnerFailed(`Data-transform check rejected: command kind "${commandKind}" is not read-only`, {
|
|
1238
|
+
why: "Data-transform checks must use aggregate or rawAggregate commands so the pre/postcheck path cannot mutate the database.",
|
|
1239
|
+
fix: "Author the check.source as an aggregate pipeline (or rawAggregate) rather than a DML write command.",
|
|
1240
|
+
meta: {
|
|
1241
|
+
checkDescription: check.description,
|
|
1242
|
+
commandKind,
|
|
1243
|
+
collection: check.source.collection
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
const wireCommand = await adapter.lower(check.source, {});
|
|
1247
|
+
let matchFound = false;
|
|
1248
|
+
for await (const row of driver.execute(wireCommand)) if (filterEvaluator.evaluate(check.filter, row)) {
|
|
1249
|
+
matchFound = true;
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
if (!(check.expect === "exists" ? matchFound : !matchFound)) return false;
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
async evaluateChecks(checks, inspectionExecutor, filterEvaluator) {
|
|
1257
|
+
for (const check of checks) {
|
|
1258
|
+
const matchFound = (await check.source.accept(inspectionExecutor)).some((doc) => filterEvaluator.evaluate(check.filter, doc));
|
|
1259
|
+
if (!(check.expect === "exists" ? matchFound : !matchFound)) return false;
|
|
1260
|
+
}
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
async allChecksSatisfied(checks, inspectionExecutor, filterEvaluator) {
|
|
1264
|
+
if (checks.length === 0) return false;
|
|
1265
|
+
return this.evaluateChecks(checks, inspectionExecutor, filterEvaluator);
|
|
1266
|
+
}
|
|
1267
|
+
enforcePolicyCompatibility(policy, operations) {
|
|
1268
|
+
const allowedClasses = new Set(policy.allowedOperationClasses);
|
|
1269
|
+
for (const operation of operations) if (!allowedClasses.has(operation.operationClass)) return runnerFailure("MIGRATION.POLICY_VIOLATION", `Operation ${operation.id} has class "${operation.operationClass}" which is not allowed by policy.`, {
|
|
1270
|
+
why: `Policy only allows: ${[...allowedClasses].join(", ")}.`,
|
|
1271
|
+
meta: {
|
|
1272
|
+
operationId: operation.id,
|
|
1273
|
+
operationClass: operation.operationClass
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
ensureMarkerCompatibility(marker, plan) {
|
|
1278
|
+
const origin = plan.origin ?? null;
|
|
1279
|
+
if (!origin) return;
|
|
1280
|
+
if (!marker) return runnerFailure("MIGRATION.MARKER_ORIGIN_MISMATCH", `Missing contract marker: expected origin storage hash ${origin.storageHash}.`, { meta: { expectedOriginStorageHash: origin.storageHash } });
|
|
1281
|
+
if (marker.storageHash !== origin.storageHash) return runnerFailure("MIGRATION.MARKER_ORIGIN_MISMATCH", `Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`, { meta: {
|
|
1282
|
+
markerStorageHash: marker.storageHash,
|
|
1283
|
+
expectedOriginStorageHash: origin.storageHash
|
|
1284
|
+
} });
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
/**
|
|
1288
|
+
* Mongo target `Namespace` concretion. In Mongo the "namespace" concept
|
|
1289
|
+
* binds to the connection's `db` field — a `MongoTargetDatabase` instance
|
|
1290
|
+
* names the database the collections live under. `entries['collection']`
|
|
1291
|
+
* holds collection IR. The unbound singleton (below) is the late-bound
|
|
1292
|
+
* namespace whose binding the connection's `db` resolves at runtime rather
|
|
1293
|
+
* than at authoring time.
|
|
1294
|
+
*
|
|
1295
|
+
* Qualifier emission is the rendering seam: query / DDL emission asks the
|
|
1296
|
+
* namespace for its qualifier (e.g. `"<db>.<collection>"`) and consumes
|
|
1297
|
+
* the result polymorphically. The unbound singleton overrides these
|
|
1298
|
+
* methods to elide the prefix entirely — call sites stay polymorphic and
|
|
1299
|
+
* never branch on `id === UNBOUND_NAMESPACE_ID`.
|
|
1300
|
+
*/
|
|
1301
|
+
var MongoTargetDatabase = class extends NamespaceBase {
|
|
1302
|
+
id;
|
|
1303
|
+
entries;
|
|
1304
|
+
constructor(input) {
|
|
1305
|
+
super();
|
|
1306
|
+
this.id = input.id;
|
|
1307
|
+
const rawEntries = {
|
|
1308
|
+
collection: {},
|
|
1309
|
+
...input.entries
|
|
1310
|
+
};
|
|
1311
|
+
this.entries = Object.freeze(blindCast(hydrateNamespaceEntities(rawEntries, composeMongoEntityKinds(), "carry")));
|
|
1312
|
+
Object.defineProperty(this, "kind", {
|
|
1313
|
+
value: "database",
|
|
1314
|
+
writable: false,
|
|
1315
|
+
enumerable: false,
|
|
1316
|
+
configurable: true
|
|
1317
|
+
});
|
|
1318
|
+
freezeNode(this);
|
|
1319
|
+
}
|
|
1320
|
+
get collection() {
|
|
1321
|
+
return this.entries.collection ?? Object.freeze({});
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* The bare qualifier as it would appear in a rendered string. The
|
|
1325
|
+
* unbound-database singleton overrides this to return `''`.
|
|
1326
|
+
*/
|
|
1327
|
+
qualifier() {
|
|
1328
|
+
return this.id;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Qualify a collection name with the database prefix. The
|
|
1332
|
+
* unbound-database singleton overrides this to emit just the
|
|
1333
|
+
* collection name. Used by emission/introspection paths that need a
|
|
1334
|
+
* fully-qualified reference.
|
|
1335
|
+
*/
|
|
1336
|
+
qualifyCollection(collectionName) {
|
|
1337
|
+
return `${this.id}.${collectionName}`;
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
/**
|
|
1341
|
+
* Singleton subclass for the reserved sentinel namespace id
|
|
1342
|
+
* (`UNBOUND_NAMESPACE_ID`) — the late-bound namespace whose binding the
|
|
1343
|
+
* connection's `db` resolves at runtime. Overrides qualifier emission
|
|
1344
|
+
* to elide the database prefix; call sites that consume `qualifier()`
|
|
1345
|
+
* / `qualifyCollection()` get unqualified output without branching on
|
|
1346
|
+
* the namespace id.
|
|
1347
|
+
*
|
|
1348
|
+
* This is the target-side materialization of "the framework provides
|
|
1349
|
+
* affordances; targets implement specifics": the framework names the
|
|
1350
|
+
* sentinel; Mongo decides what late-bound means here (the collection
|
|
1351
|
+
* name, naked — the database is supplied by the live connection).
|
|
1352
|
+
*/
|
|
1353
|
+
var MongoTargetUnboundDatabase = class MongoTargetUnboundDatabase extends MongoTargetDatabase {
|
|
1354
|
+
static instance = new MongoTargetUnboundDatabase();
|
|
1355
|
+
constructor() {
|
|
1356
|
+
super({ id: UNBOUND_NAMESPACE_ID });
|
|
1357
|
+
}
|
|
1358
|
+
qualifier() {
|
|
1359
|
+
return "";
|
|
1360
|
+
}
|
|
1361
|
+
qualifyCollection(collectionName) {
|
|
1362
|
+
return collectionName;
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var MongoTargetContractSerializer = class extends MongoContractSerializerBase {
|
|
1366
|
+
constructTargetContract(validated) {
|
|
1367
|
+
const { storage, ...rest } = validated;
|
|
1368
|
+
const namespaces = Object.fromEntries(Object.entries(storage.namespaces).map(([nsId, nsData]) => {
|
|
1369
|
+
const collectionCount = Object.keys(nsData.entries.collection ?? {}).length;
|
|
1370
|
+
const valueSetCount = Object.keys(nsData.entries["valueSet"] ?? {}).length;
|
|
1371
|
+
if (nsId === UNBOUND_NAMESPACE_ID && collectionCount === 0 && valueSetCount === 0) return [nsId, MongoTargetUnboundDatabase.instance];
|
|
1372
|
+
const dbInput = { id: nsData.id };
|
|
1373
|
+
if (nsData.entries["collection"] !== void 0 || nsData.entries["valueSet"] !== void 0) dbInput.entries = {
|
|
1374
|
+
collection: blindCast(nsData.entries["collection"] ?? {}),
|
|
1375
|
+
...ifDefined("valueSet", blindCast(nsData.entries["valueSet"]))
|
|
1376
|
+
};
|
|
1377
|
+
return [nsId, new MongoTargetDatabase(dbInput)];
|
|
1378
|
+
}));
|
|
1379
|
+
const targetStorage = new MongoStorage({
|
|
1380
|
+
storageHash: storage.storageHash,
|
|
1381
|
+
namespaces
|
|
1382
|
+
});
|
|
1383
|
+
return {
|
|
1384
|
+
...rest,
|
|
1385
|
+
storage: targetStorage
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
serializeContract(contract) {
|
|
1389
|
+
const { storage, ...rest } = contract;
|
|
1390
|
+
const namespacesJson = {};
|
|
1391
|
+
for (const [nsId, ns] of Object.entries(storage.namespaces)) {
|
|
1392
|
+
const collectionsOut = {};
|
|
1393
|
+
for (const [collName, coll] of Object.entries(ns.entries.collection ?? {})) collectionsOut[collName] = JSON.parse(JSON.stringify(coll));
|
|
1394
|
+
const valueSetOut = {};
|
|
1395
|
+
for (const [vsName, vs] of Object.entries(ns.entries.valueSet ?? {})) valueSetOut[vsName] = blindCast(JSON.parse(JSON.stringify(vs)));
|
|
1396
|
+
namespacesJson[nsId] = {
|
|
1397
|
+
id: ns.id,
|
|
1398
|
+
kind: "mongo-database",
|
|
1399
|
+
entries: {
|
|
1400
|
+
collection: collectionsOut,
|
|
1401
|
+
...ifDefined("valueSet", Object.keys(valueSetOut).length > 0 ? valueSetOut : void 0)
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return blindCast({
|
|
1406
|
+
...rest,
|
|
1407
|
+
storage: {
|
|
1408
|
+
storageHash: String(storage.storageHash),
|
|
1409
|
+
namespaces: namespacesJson
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
/**
|
|
1415
|
+
* Mongo target `SchemaVerifier` concretion. Extends the family base's
|
|
1416
|
+
* namespace-walk scaffolding and contributes the per-namespace diff via
|
|
1417
|
+
* `verifyNamespace`; the diff body reuses the existing target-side
|
|
1418
|
+
* helpers (`contractToMongoSchemaIR`, `canonicalizeSchemasForVerification`,
|
|
1419
|
+
* `diffMongoSchemas`) so production verification behaviour is unchanged.
|
|
1420
|
+
*
|
|
1421
|
+
* Today's invariant: every Mongo contract carries exactly one
|
|
1422
|
+
* namespace (the unbound singleton, materialised as
|
|
1423
|
+
* `MongoTargetUnboundDatabase`), so the family-base namespace walk
|
|
1424
|
+
* dispatches exactly once and the per-namespace body runs the existing
|
|
1425
|
+
* whole-schema diff. Future per-collection namespace assignment will
|
|
1426
|
+
* have this hook project the diff to the namespace's owned collections.
|
|
1427
|
+
*
|
|
1428
|
+
* `verifyTargetExtensions` returns the empty list — Mongo has no
|
|
1429
|
+
* target-only kinds today.
|
|
1430
|
+
*
|
|
1431
|
+
* Strict diff mode is `false` for SPI-routed calls; production
|
|
1432
|
+
* verification today still goes through `verifyMongoSchema` which
|
|
1433
|
+
* receives strict from the CLI.
|
|
1434
|
+
*/
|
|
1435
|
+
var MongoTargetSchemaVerifier = class extends MongoSchemaVerifierBase {
|
|
1436
|
+
verifyNamespace(options) {
|
|
1437
|
+
const expectedIR = contractToMongoSchemaIR(options.contract);
|
|
1438
|
+
const { live, expected } = canonicalizeSchemasForVerification(options.schema, expectedIR);
|
|
1439
|
+
const collectionControlPolicy = (name) => this.collectionControlPolicyForName(options.contract, name);
|
|
1440
|
+
const { failures, warnings } = diffMongoSchemas(live, expected, false, collectionControlPolicy);
|
|
1441
|
+
return [...failures, ...warnings];
|
|
1442
|
+
}
|
|
1443
|
+
verifyTargetExtensions(_options) {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
/**
|
|
1448
|
+
* The bare entity names the given contracts declare, unioned. The Mongo runner
|
|
1449
|
+
* asks this of every OTHER contract space in a multi-space apply, so each
|
|
1450
|
+
* space's post-apply verify can drop the extras those siblings claim.
|
|
1451
|
+
*/
|
|
1452
|
+
function entityNamesDeclaredBy(contracts) {
|
|
1453
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1454
|
+
for (const contract of contracts) for (const { entityName } of elementCoordinates(contract.storage)) owned.add(entityName);
|
|
1455
|
+
return owned;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* True when an issue reports a whole collection present in the database but
|
|
1459
|
+
* declared by no contract (an extra) — its path is exactly the collection
|
|
1460
|
+
* name, never a deeper index/validator/options auxiliary.
|
|
1461
|
+
*/
|
|
1462
|
+
function extraCollectionName(issue) {
|
|
1463
|
+
if (issueOutcome(issue) !== "not-expected" || issue.path.length !== 1) return void 0;
|
|
1464
|
+
return issue.path[0];
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Scope a per-space post-apply verify result to the contract space's own
|
|
1468
|
+
* elements: drop the `extra` findings for collections another contract space
|
|
1469
|
+
* claims. The runner verifies the destination contract against the full live
|
|
1470
|
+
* database, which holds sibling spaces' collections — without the scoping a
|
|
1471
|
+
* multi-space apply could never pass strict verify. Extras claimed by NO space
|
|
1472
|
+
* survive, so genuine drift still fails the runner's verdict.
|
|
1473
|
+
*
|
|
1474
|
+
* The result is issue-based, so the verdict recomputes directly from the
|
|
1475
|
+
* surviving list: `ok` holds exactly when it is empty.
|
|
1476
|
+
*/
|
|
1477
|
+
function scopeVerifyResultToSpace(result, ownedByOtherSpaces) {
|
|
1478
|
+
if (ownedByOtherSpaces.size === 0) return result;
|
|
1479
|
+
const issues = result.schema.issues.filter((issue) => {
|
|
1480
|
+
const name = extraCollectionName(issue);
|
|
1481
|
+
return name === void 0 || !ownedByOtherSpaces.has(name);
|
|
1482
|
+
});
|
|
1483
|
+
if (issues.length === result.schema.issues.length) return result;
|
|
1484
|
+
const ok = issues.length === 0;
|
|
1485
|
+
const { code: staleCode, ...envelope } = result;
|
|
1486
|
+
return {
|
|
1487
|
+
...envelope,
|
|
1488
|
+
ok,
|
|
1489
|
+
...ok ? {} : { code: result.code ?? "CONTRACT.MARKER_REQUIRED" },
|
|
1490
|
+
summary: ok ? "Database schema satisfies contract" : result.summary,
|
|
1491
|
+
schema: {
|
|
1492
|
+
...result.schema,
|
|
1493
|
+
issues
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* `migration.ts` default-exports a `Migration` subclass whose `operations`
|
|
1499
|
+
* getter returns the ordered list of operations and whose `describe()`
|
|
1500
|
+
* returns the manifest identity metadata. `MongoMigrationPlanner.plan()`
|
|
1501
|
+
* returns a `MigrationPlanWithAuthoringSurface` that knows how to render
|
|
1502
|
+
* itself back to such a file; `MongoMigrationPlanner.emptyMigration()`
|
|
1503
|
+
* returns the same shape for `migration new`. Users run the scaffolded
|
|
1504
|
+
* `migration.ts` directly (via `node migration.ts`) to self-emit
|
|
1505
|
+
* `ops.json` and attest the `migrationHash`.
|
|
1506
|
+
*/
|
|
1507
|
+
const mongoTargetDescriptor = {
|
|
1508
|
+
...mongoTargetDescriptorMeta,
|
|
1509
|
+
contractSerializer: new MongoTargetContractSerializer(),
|
|
1510
|
+
schemaVerifier: new MongoTargetSchemaVerifier(),
|
|
1511
|
+
migrations: {
|
|
1512
|
+
createPlanner(_adapter) {
|
|
1513
|
+
return new MongoMigrationPlanner();
|
|
1514
|
+
},
|
|
1515
|
+
createRunner(family) {
|
|
1516
|
+
let cachedDeps;
|
|
1517
|
+
const runMongo = async (driver, runnerOptions) => {
|
|
1518
|
+
cachedDeps ??= createMongoRunnerDeps(driver, MongoDriverImpl.fromDb(extractDb(driver)), family);
|
|
1519
|
+
return new MongoMigrationRunner(cachedDeps).execute({
|
|
1520
|
+
...runnerOptions,
|
|
1521
|
+
destinationContract: blindCast(runnerOptions.destinationContract)
|
|
1522
|
+
});
|
|
1523
|
+
};
|
|
1524
|
+
return { async execute({ driver, perSpaceOptions }) {
|
|
1525
|
+
const contracts = perSpaceOptions.map((opts) => blindCast(opts.destinationContract));
|
|
1526
|
+
const perSpaceResults = [];
|
|
1527
|
+
for (let i = 0; i < perSpaceOptions.length; i++) {
|
|
1528
|
+
const spaceOptions = perSpaceOptions[i];
|
|
1529
|
+
if (!spaceOptions) continue;
|
|
1530
|
+
const ownedByOtherSpaces = entityNamesDeclaredBy(contracts.filter((_, j) => j !== i));
|
|
1531
|
+
const scopeVerifyResult = (result) => scopeVerifyResultToSpace(result, ownedByOtherSpaces);
|
|
1532
|
+
const { space, ...runnerOptions } = spaceOptions;
|
|
1533
|
+
const result = await runMongo(driver, {
|
|
1534
|
+
...runnerOptions,
|
|
1535
|
+
scopeVerifyResult
|
|
1536
|
+
});
|
|
1537
|
+
if (!result.ok) return notOk({
|
|
1538
|
+
...result.failure,
|
|
1539
|
+
failingSpace: space
|
|
1540
|
+
});
|
|
1541
|
+
perSpaceResults.push({
|
|
1542
|
+
space,
|
|
1543
|
+
value: result.value
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
return ok({ perSpaceResults });
|
|
1547
|
+
} };
|
|
1548
|
+
},
|
|
1549
|
+
contractToSchema(contract) {
|
|
1550
|
+
return contractToMongoSchemaIR(blindCast(contract));
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
create() {
|
|
1554
|
+
return {
|
|
1555
|
+
familyId: "mongo",
|
|
1556
|
+
targetId: "mongo"
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
//#endregion
|
|
1561
|
+
export { renderCallsToTypeScript as _, DropIndexCall as a, schemaIndexToCreateIndexOptions as b, MongoMigrationRunner as c, MongoTargetSchemaVerifier as d, MongoTargetUnboundDatabase as f, mongoTargetDescriptor as g, deserializeMongoOps as h, DropCollectionCall as i, MongoTargetContractSerializer as l, deserializeMongoOp as m, CreateCollectionCall as n, FilterEvaluator as o, PlannerProducedMongoMigration as p, CreateIndexCall as r, MongoMigrationPlanner as s, CollModCall as t, MongoTargetDatabase as u, renderOps as v, serializeMongoOps as x, schemaCollectionToCreateCollectionOptions as y };
|
|
1562
|
+
|
|
1563
|
+
//# sourceMappingURL=control-CEXmcht7.mjs.map
|