@prisma-next/target-mongo 0.7.0 → 0.8.0-dev.1

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 (31) hide show
  1. package/dist/control.d.mts +168 -101
  2. package/dist/control.d.mts.map +1 -1
  3. package/dist/control.mjs +1194 -1138
  4. package/dist/control.mjs.map +1 -1
  5. package/dist/{migration-factories-ZBsWqXt-.mjs → migration-factories-BRBKKZia.mjs} +1 -1
  6. package/dist/{migration-factories-ZBsWqXt-.mjs.map → migration-factories-BRBKKZia.mjs.map} +1 -1
  7. package/dist/migration.d.mts +2 -2
  8. package/dist/migration.mjs +1 -1
  9. package/dist/{op-factory-call-9z5D19cP.d.mts → op-factory-call-BC-llGKt.d.mts} +2 -2
  10. package/dist/{op-factory-call-9z5D19cP.d.mts.map → op-factory-call-BC-llGKt.d.mts.map} +1 -1
  11. package/package.json +23 -21
  12. package/src/core/control-target.ts +185 -0
  13. package/src/core/mongo-planner.ts +1 -1
  14. package/src/core/mongo-runner.ts +3 -45
  15. package/src/core/mongo-target-contract-serializer.ts +73 -0
  16. package/src/core/mongo-target-contract.ts +15 -0
  17. package/src/core/mongo-target-database.ts +82 -0
  18. package/src/core/mongo-target-schema-verifier.ts +54 -0
  19. package/src/exports/control.ts +8 -9
  20. package/dist/schema-verify.d.mts +0 -22
  21. package/dist/schema-verify.d.mts.map +0 -1
  22. package/dist/schema-verify.mjs +0 -2
  23. package/dist/verify-mongo-schema-DlPXaotB.mjs +0 -578
  24. package/dist/verify-mongo-schema-DlPXaotB.mjs.map +0 -1
  25. package/src/core/contract-to-schema.ts +0 -63
  26. package/src/core/ddl-formatter.ts +0 -112
  27. package/src/core/marker-ledger.ts +0 -232
  28. package/src/core/schema-diff.ts +0 -402
  29. package/src/core/schema-verify/canonicalize-introspection.ts +0 -389
  30. package/src/core/schema-verify/verify-mongo-schema.ts +0 -60
  31. package/src/exports/schema-verify.ts +0 -2
package/dist/control.mjs CHANGED
@@ -1,1214 +1,1000 @@
1
- import { n as contractToMongoSchemaIR, t as verifyMongoSchema } from "./verify-mongo-schema-DlPXaotB.mjs";
2
- import { a as dropCollection, n as createCollection, o as dropIndex, r as createIndex, t as collMod } from "./migration-factories-ZBsWqXt-.mjs";
3
- import { canonicalize, deepEqual } from "@prisma-next/mongo-schema-ir";
4
- import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma-next/mongo-query-ast/execution";
5
- import { type } from "arktype";
1
+ import { t as mongoTargetDescriptorMeta } from "./descriptor-meta-DdXFJeK1.mjs";
2
+ import { a as dropCollection, n as createCollection, o as dropIndex, r as createIndex, t as collMod } from "./migration-factories-BRBKKZia.mjs";
3
+ import { createMongoRunnerDeps, extractDb } from "@prisma-next/adapter-mongo/control";
4
+ import { MongoDriverImpl } from "@prisma-next/driver-mongo";
5
+ import { canonicalizeSchemasForVerification, contractToMongoSchemaIR, diffMongoSchemas } from "@prisma-next/family-mongo/control";
6
+ import { projectSchemaToSpace } from "@prisma-next/migration-tools/aggregate";
7
+ import { MongoSchemaIR, canonicalize, deepEqual } from "@prisma-next/mongo-schema-ir";
8
+ import { notOk, ok } from "@prisma-next/utils/result";
9
+ import { TsExpression, jsonToTsSource, renderImports } from "@prisma-next/ts-render";
6
10
  import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr } from "@prisma-next/mongo-query-ast/control";
7
11
  import { ifDefined } from "@prisma-next/utils/defined";
8
- import { TsExpression, jsonToTsSource, renderImports } from "@prisma-next/ts-render";
9
12
  import { Migration } from "@prisma-next/migration-tools/migration";
10
13
  import { detectScaffoldRuntime, shebangLineFor } from "@prisma-next/migration-tools/migration-ts";
11
14
  import { errorRunnerFailed } from "@prisma-next/errors/execution";
15
+ import { verifyMongoSchema } from "@prisma-next/family-mongo/schema-verify";
12
16
  import { APP_SPACE_ID } from "@prisma-next/framework-components/control";
13
- import { notOk, ok } from "@prisma-next/utils/result";
14
- //#region src/core/ddl-formatter.ts
15
- function formatKeySpec(keys) {
16
- return `{ ${keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`).join(", ")} }`;
17
- }
18
- function formatOptions(cmd) {
19
- const parts = [];
20
- if (cmd.unique) parts.push("unique: true");
21
- if (cmd.sparse) parts.push("sparse: true");
22
- if (cmd.expireAfterSeconds !== void 0) parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
23
- if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
24
- if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
25
- if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
26
- if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
27
- if (cmd.language_override) parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
28
- if (cmd.wildcardProjection) parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
29
- if (cmd.partialFilterExpression) parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
30
- if (parts.length === 0) return void 0;
31
- return `{ ${parts.join(", ")} }`;
32
- }
33
- function formatCreateCollectionOptions(cmd) {
34
- const parts = [];
35
- if (cmd.capped) parts.push("capped: true");
36
- if (cmd.size !== void 0) parts.push(`size: ${cmd.size}`);
37
- if (cmd.max !== void 0) parts.push(`max: ${cmd.max}`);
38
- if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
39
- if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
40
- if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
41
- if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
42
- if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
43
- if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
44
- if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
45
- if (parts.length === 0) return void 0;
46
- return `{ ${parts.join(", ")} }`;
47
- }
48
- var MongoDdlCommandFormatter = class {
49
- createIndex(cmd) {
50
- const keySpec = formatKeySpec(cmd.keys);
51
- const opts = formatOptions(cmd);
52
- return opts ? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})` : `db.${cmd.collection}.createIndex(${keySpec})`;
17
+ import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma-next/mongo-query-ast/execution";
18
+ import { type } from "arktype";
19
+ import { MongoContractSerializerBase, MongoSchemaVerifierBase } from "@prisma-next/family-mongo/ir";
20
+ import { NamespaceBase, UNSPECIFIED_NAMESPACE_ID, freezeNode } from "@prisma-next/framework-components/ir";
21
+ import { MongoStorage } from "@prisma-next/mongo-contract";
22
+ //#region src/core/op-factory-call.ts
23
+ const TARGET_MIGRATION_MODULE = "@prisma-next/target-mongo/migration";
24
+ var OpFactoryCallNode = class extends TsExpression {
25
+ importRequirements() {
26
+ return [{
27
+ moduleSpecifier: TARGET_MIGRATION_MODULE,
28
+ symbol: this.factoryName
29
+ }];
53
30
  }
54
- dropIndex(cmd) {
55
- return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
31
+ freeze() {
32
+ Object.freeze(this);
56
33
  }
57
- createCollection(cmd) {
58
- const opts = formatCreateCollectionOptions(cmd);
59
- return opts ? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})` : `db.createCollection(${JSON.stringify(cmd.collection)})`;
34
+ };
35
+ function formatKeys(keys) {
36
+ return keys.map((k) => `${k.field}:${k.direction}`).join(", ");
37
+ }
38
+ var CreateIndexCall = class extends OpFactoryCallNode {
39
+ factoryName = "createIndex";
40
+ operationClass = "additive";
41
+ collection;
42
+ keys;
43
+ options;
44
+ label;
45
+ constructor(collection, keys, options) {
46
+ super();
47
+ this.collection = collection;
48
+ this.keys = keys;
49
+ this.options = options;
50
+ this.label = `Create index on ${collection} (${formatKeys(keys)})`;
51
+ this.freeze();
60
52
  }
61
- dropCollection(cmd) {
62
- return `db.${cmd.collection}.drop()`;
53
+ toOp() {
54
+ return createIndex(this.collection, this.keys, this.options);
63
55
  }
64
- collMod(cmd) {
65
- const parts = [`collMod: ${JSON.stringify(cmd.collection)}`];
66
- if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
67
- if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
68
- if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
69
- if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
70
- return `db.runCommand({ ${parts.join(", ")} })`;
56
+ renderTypeScript() {
57
+ return this.options ? `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)}, ${jsonToTsSource(this.options)})` : `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
71
58
  }
72
59
  };
73
- const formatter = new MongoDdlCommandFormatter();
74
- function formatMongoOperations(operations) {
75
- const statements = [];
76
- for (const operation of operations) {
77
- const candidate = operation;
78
- if (!("execute" in candidate) || !Array.isArray(candidate["execute"])) continue;
79
- for (const step of candidate["execute"]) if (step.command && typeof step.command.accept === "function") statements.push(step.command.accept(formatter));
60
+ var DropIndexCall = class extends OpFactoryCallNode {
61
+ factoryName = "dropIndex";
62
+ operationClass = "destructive";
63
+ collection;
64
+ keys;
65
+ label;
66
+ constructor(collection, keys) {
67
+ super();
68
+ this.collection = collection;
69
+ this.keys = keys;
70
+ this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
71
+ this.freeze();
80
72
  }
81
- return statements;
82
- }
83
- //#endregion
84
- //#region src/core/filter-evaluator.ts
85
- function getNestedField(doc, path) {
86
- const parts = path.split(".");
87
- let current = doc;
88
- for (const part of parts) {
89
- if (current === null || current === void 0 || typeof current !== "object") return;
90
- const record = current;
91
- if (!Object.hasOwn(record, part)) return;
92
- current = record[part];
73
+ toOp() {
74
+ return dropIndex(this.collection, this.keys);
93
75
  }
94
- return current;
95
- }
96
- function evaluateFieldOp(op, actual, expected) {
97
- switch (op) {
98
- case "$eq": return deepEqual(actual, expected);
99
- case "$ne": return !deepEqual(actual, expected);
100
- case "$gt": return typeof actual === typeof expected && actual > expected;
101
- case "$gte": return typeof actual === typeof expected && actual >= expected;
102
- case "$lt": return typeof actual === typeof expected && actual < expected;
103
- case "$lte": return typeof actual === typeof expected && actual <= expected;
104
- case "$in": return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
105
- default: throw new Error(`Unsupported filter operator in migration check: ${op}`);
76
+ renderTypeScript() {
77
+ return `dropIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
106
78
  }
107
- }
108
- var FilterEvaluator = class {
109
- doc = {};
110
- evaluate(filter, doc) {
111
- this.doc = doc;
112
- return filter.accept(this);
79
+ };
80
+ var CreateCollectionCall = class extends OpFactoryCallNode {
81
+ factoryName = "createCollection";
82
+ operationClass = "additive";
83
+ collection;
84
+ options;
85
+ label;
86
+ constructor(collection, options) {
87
+ super();
88
+ this.collection = collection;
89
+ this.options = options;
90
+ this.label = `Create collection ${collection}`;
91
+ this.freeze();
113
92
  }
114
- field(expr) {
115
- const value = getNestedField(this.doc, expr.field);
116
- return evaluateFieldOp(expr.op, value, expr.value);
93
+ toOp() {
94
+ return createCollection(this.collection, this.options);
117
95
  }
118
- and(expr) {
119
- return expr.exprs.every((child) => child.accept(this));
96
+ renderTypeScript() {
97
+ return this.options ? `createCollection(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})` : `createCollection(${jsonToTsSource(this.collection)})`;
120
98
  }
121
- or(expr) {
122
- return expr.exprs.some((child) => child.accept(this));
99
+ };
100
+ var DropCollectionCall = class extends OpFactoryCallNode {
101
+ factoryName = "dropCollection";
102
+ operationClass = "destructive";
103
+ collection;
104
+ label;
105
+ constructor(collection) {
106
+ super();
107
+ this.collection = collection;
108
+ this.label = `Drop collection ${collection}`;
109
+ this.freeze();
123
110
  }
124
- not(expr) {
125
- return !expr.expr.accept(this);
111
+ toOp() {
112
+ return dropCollection(this.collection);
126
113
  }
127
- exists(expr) {
128
- const has = getNestedField(this.doc, expr.field) !== void 0;
129
- return expr.exists ? has : !has;
114
+ renderTypeScript() {
115
+ return `dropCollection(${jsonToTsSource(this.collection)})`;
130
116
  }
131
- expr(_expr) {
132
- throw new Error("Aggregation expression filters are not supported in migration checks");
117
+ };
118
+ var CollModCall = class extends OpFactoryCallNode {
119
+ factoryName = "collMod";
120
+ collection;
121
+ options;
122
+ meta;
123
+ operationClass;
124
+ label;
125
+ constructor(collection, options, meta) {
126
+ super();
127
+ this.collection = collection;
128
+ this.options = options;
129
+ this.meta = meta;
130
+ this.operationClass = meta?.operationClass ?? "destructive";
131
+ this.label = meta?.label ?? `Modify collection ${collection}`;
132
+ this.freeze();
133
+ }
134
+ toOp() {
135
+ return collMod(this.collection, this.options, this.meta);
136
+ }
137
+ renderTypeScript() {
138
+ return this.meta ? `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)}, ${jsonToTsSource(this.meta)})` : `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})`;
133
139
  }
134
140
  };
135
- //#endregion
136
- //#region src/core/marker-ledger.ts
137
- const COLLECTION = "_prisma_migrations";
138
- /**
139
- * Marker doc shape.
140
- *
141
- * Same fields as the SQL marker row but camelCase + Mongo-native types:
142
- * `Date` is BSON-hydrated, `meta` is a native object (not JSON-stringified),
143
- * `_id` and any extension fields are tolerated. `invariants?` is optional —
144
- * absent reads as `[]` (schemaless default); present-but-malformed throws.
145
- *
146
- * `space` is required: every marker doc is keyed by its space id (`_id`)
147
- * and stamped with a matching `space` field for partitioned reads.
148
- */
149
- const MongoMarkerDocSchema = type({
150
- space: "string",
151
- storageHash: "string",
152
- profileHash: "string",
153
- "contractJson?": "unknown | null",
154
- "canonicalVersion?": "number | null",
155
- "updatedAt?": "Date",
156
- "appTag?": "string | null",
157
- "meta?": type({ "[string]": "unknown" }).or("null"),
158
- "invariants?": type("string").array(),
159
- "+": "delete"
160
- });
161
- function parseMongoMarkerDoc(doc) {
162
- const result = MongoMarkerDocSchema(doc);
163
- if (result instanceof type.errors) throw new Error(`Invalid marker doc on ${COLLECTION}: ${result.summary}`);
141
+ function schemaIndexToCreateIndexOptions(index) {
164
142
  return {
165
- storageHash: result.storageHash,
166
- profileHash: result.profileHash,
167
- contractJson: result.contractJson ?? null,
168
- canonicalVersion: result.canonicalVersion ?? null,
169
- updatedAt: result.updatedAt ?? /* @__PURE__ */ new Date(),
170
- appTag: result.appTag ?? null,
171
- meta: result.meta ?? {},
172
- invariants: result.invariants ?? []
143
+ unique: index.unique || void 0,
144
+ sparse: index.sparse,
145
+ expireAfterSeconds: index.expireAfterSeconds,
146
+ partialFilterExpression: index.partialFilterExpression,
147
+ wildcardProjection: index.wildcardProjection,
148
+ collation: index.collation,
149
+ weights: index.weights,
150
+ default_language: index.default_language,
151
+ language_override: index.language_override
173
152
  };
174
153
  }
175
- async function executeAggregate(db, cmd) {
176
- return db.collection(cmd.collection).aggregate(cmd.pipeline).toArray();
177
- }
178
- async function executeInsertOne(db, cmd) {
179
- await db.collection(cmd.collection).insertOne(cmd.document);
180
- }
181
- async function executeFindOneAndUpdate(db, cmd) {
182
- return db.collection(cmd.collection).findOneAndUpdate(cmd.filter, cmd.update, { upsert: cmd.upsert });
183
- }
184
- /**
185
- * Reads the marker document for the given contract space, or returns
186
- * `null` if no marker has been written for that space yet. Each space
187
- * owns one row keyed by `_id: <space>` — see ADR 212 for the per-space
188
- * mechanism this enables.
189
- */
190
- async function readMarker(db, space) {
191
- const doc = (await executeAggregate(db, new RawAggregateCommand(COLLECTION, [{ $match: {
192
- _id: space,
193
- space
194
- } }, { $limit: 1 }])))[0];
195
- if (!doc) return null;
196
- return parseMongoMarkerDoc(doc);
197
- }
198
- /**
199
- * Reads every marker doc in the collection (one per contract space)
200
- * and returns them keyed by `space`. Used by the per-space verifier
201
- * to detect marker-vs-on-disk drift and orphan marker rows. Returns
202
- * an empty map when no marker docs have been written yet.
203
- *
204
- * Marker docs are keyed by `_id: <space>` (string); ledger entries
205
- * live in the same collection but use a driver-generated `ObjectId`
206
- * `_id` plus `type: 'ledger'`. The filter selects string-keyed docs
207
- * with a `space` field, which excludes ledger entries by construction.
208
- */
209
- async function readAllMarkers(db) {
210
- const docs = await executeAggregate(db, new RawAggregateCommand(COLLECTION, [{ $match: {
211
- _id: { $type: "string" },
212
- space: { $type: "string" },
213
- $expr: { $eq: ["$_id", "$space"] }
214
- } }]));
215
- const out = /* @__PURE__ */ new Map();
216
- for (const doc of docs) {
217
- const space = doc["space"];
218
- /* v8 ignore next -- @preserve type-narrowing guard: the $match stage above filters on `space: { $type: 'string' }`, so this branch is unreachable at runtime. The check exists so the `out.set(space, ...)` call below can accept `string`. */
219
- if (typeof space !== "string") continue;
220
- out.set(space, parseMongoMarkerDoc(doc));
221
- }
222
- return out;
154
+ function schemaCollectionToCreateCollectionOptions(coll) {
155
+ const opts = coll.options;
156
+ const validator = coll.validator;
157
+ if (!opts && !validator) return void 0;
158
+ return {
159
+ capped: opts?.capped ? true : void 0,
160
+ size: opts?.capped?.size,
161
+ max: opts?.capped?.max,
162
+ timeseries: opts?.timeseries,
163
+ collation: opts?.collation,
164
+ clusteredIndex: opts?.clusteredIndex ? {
165
+ key: { _id: 1 },
166
+ unique: true,
167
+ ...opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}
168
+ } : void 0,
169
+ validator: validator ? { $jsonSchema: validator.jsonSchema } : void 0,
170
+ validationLevel: validator?.validationLevel,
171
+ validationAction: validator?.validationAction,
172
+ changeStreamPreAndPostImages: opts?.changeStreamPreAndPostImages
173
+ };
223
174
  }
224
- async function initMarker(db, space, destination) {
225
- await executeInsertOne(db, new RawInsertOneCommand(COLLECTION, {
226
- _id: space,
227
- space,
228
- storageHash: destination.storageHash,
229
- profileHash: destination.profileHash,
230
- contractJson: null,
231
- canonicalVersion: null,
232
- updatedAt: /* @__PURE__ */ new Date(),
233
- appTag: null,
234
- meta: {},
235
- invariants: destination.invariants ?? []
236
- }));
175
+ //#endregion
176
+ //#region src/core/render-ops.ts
177
+ function renderOps(calls) {
178
+ return calls.map((call) => call.toOp());
237
179
  }
180
+ //#endregion
181
+ //#region src/core/render-typescript.ts
238
182
  /**
239
- * Updates the marker doc for the given space atomically (CAS on
240
- * `expectedFrom`).
183
+ * Always-present base imports for the rendered scaffold:
241
184
  *
242
- * `destination.invariants`:
243
- * - `undefined` existing field left untouched.
244
- * - explicit value merged into the existing field server-side via an
245
- * aggregation pipeline (`$setUnion + $sortArray`), atomic at the
246
- * document level. `[]` is a no-op merge.
185
+ * - `Migration` from `@prisma-next/family-mongo/migration` — the
186
+ * user-facing Mongo `Migration` base; subclasses don't need to
187
+ * redeclare `targetId` or thread family/target generics.
188
+ * - `MigrationCLI` from `@prisma-next/cli/migration-cli` the
189
+ * migration-file CLI entrypoint that loads `prisma-next.config.ts`,
190
+ * assembles a `ControlStack`, and instantiates the migration class.
191
+ * The migration file owns this dependency directly: pulling CLI
192
+ * machinery in at script run time is acceptable because the script's
193
+ * whole purpose is to be invoked from the project that owns the
194
+ * config. (Mirrors the postgres facade pattern; pulling `MigrationCLI`
195
+ * into `@prisma-next/family-mongo/migration` so a Mongo migration only
196
+ * needs one import is tracked separately as a follow-up.)
247
197
  */
248
- async function updateMarker(db, space, expectedFrom, destination) {
249
- const setBase = {
250
- storageHash: destination.storageHash,
251
- profileHash: destination.profileHash,
252
- updatedAt: /* @__PURE__ */ new Date()
253
- };
254
- const update = destination.invariants === void 0 ? { $set: setBase } : [{ $set: {
255
- ...setBase,
256
- invariants: { $sortArray: {
257
- input: { $setUnion: [{ $ifNull: ["$invariants", []] }, destination.invariants] },
258
- sortBy: 1
259
- } }
260
- } }];
261
- return await executeFindOneAndUpdate(db, new RawFindOneAndUpdateCommand(COLLECTION, {
262
- _id: space,
263
- space,
264
- storageHash: expectedFrom
265
- }, update, false)) !== null;
266
- }
198
+ const BASE_IMPORTS = [{
199
+ moduleSpecifier: "@prisma-next/family-mongo/migration",
200
+ symbol: "Migration"
201
+ }, {
202
+ moduleSpecifier: "@prisma-next/cli/migration-cli",
203
+ symbol: "MigrationCLI"
204
+ }];
267
205
  /**
268
- * Appends a ledger entry for the given space. Ledger entries co-exist
269
- * with marker docs in the same collection; marker docs use `_id: <space>`
270
- * (string), ledger entries use `type: 'ledger'` plus a driver-generated
271
- * ObjectId. Reads partition the two by filter shape.
206
+ * Render a list of Mongo `OpFactoryCall`s as a `migration.ts`
207
+ * source string. The result is shebanged, extends the user-facing
208
+ * `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and
209
+ * implements the abstract `operations` and `describe` members. `meta` is
210
+ * always rendered — `describe()` is part of the `Migration` contract, so
211
+ * even an empty stub must satisfy it; callers pass `from: null` for a
212
+ * baseline `migration-new` scaffold (and a real `to` hash either way).
272
213
  *
273
- * The same `edgeId` may legitimately recur across different spaces (e.g.
274
- * a synthetic ∅→head edge on first apply), so the ledger key is
275
- * `(space, edgeId)` the doc carries `space` for partitioned reads.
214
+ * The walk is polymorphic: each call node contributes its own
215
+ * `renderTypeScript()` expression and declares its own
216
+ * `importRequirements()`. The top-level renderer aggregates imports
217
+ * across all nodes and emits one `import { … } from "…"` line per module.
218
+ * The `Migration` and `MigrationCLI` imports are always emitted — they're
219
+ * structural to the rendered scaffold (extends `Migration`, calls
220
+ * `MigrationCLI.run`), not driven by any node.
276
221
  */
277
- async function writeLedgerEntry(db, space, entry) {
278
- await executeInsertOne(db, new RawInsertOneCommand(COLLECTION, {
279
- type: "ledger",
280
- space,
281
- edgeId: entry.edgeId,
282
- from: entry.from,
283
- to: entry.to,
284
- appliedAt: /* @__PURE__ */ new Date()
285
- }));
286
- }
287
- //#endregion
288
- //#region src/core/mongo-ops-serializer.ts
289
- const CreateIndexJson = type({
290
- kind: "\"createIndex\"",
291
- collection: "string",
292
- keys: type({
293
- field: "string",
294
- direction: type("1 | -1 | \"text\" | \"2dsphere\" | \"2d\" | \"hashed\"")
295
- }).array().atLeastLength(1),
296
- "unique?": "boolean",
297
- "sparse?": "boolean",
298
- "expireAfterSeconds?": "number",
299
- "partialFilterExpression?": "Record<string, unknown>",
300
- "name?": "string",
301
- "wildcardProjection?": "Record<string, unknown>",
302
- "collation?": "Record<string, unknown>",
303
- "weights?": "Record<string, unknown>",
304
- "default_language?": "string",
305
- "language_override?": "string"
306
- });
307
- const DropIndexJson = type({
308
- kind: "\"dropIndex\"",
309
- collection: "string",
310
- name: "string"
311
- });
312
- const CreateCollectionJson = type({
313
- kind: "\"createCollection\"",
314
- collection: "string",
315
- "validator?": "Record<string, unknown>",
316
- "validationLevel?": "\"strict\" | \"moderate\"",
317
- "validationAction?": "\"error\" | \"warn\"",
318
- "capped?": "boolean",
319
- "size?": "number",
320
- "max?": "number",
321
- "timeseries?": "Record<string, unknown>",
322
- "collation?": "Record<string, unknown>",
323
- "changeStreamPreAndPostImages?": "Record<string, unknown>",
324
- "clusteredIndex?": "Record<string, unknown>"
325
- });
326
- const DropCollectionJson = type({
327
- kind: "\"dropCollection\"",
328
- collection: "string"
329
- });
330
- const CollModJson = type({
331
- kind: "\"collMod\"",
332
- collection: "string",
333
- "validator?": "Record<string, unknown>",
334
- "validationLevel?": "\"strict\" | \"moderate\"",
335
- "validationAction?": "\"error\" | \"warn\"",
336
- "changeStreamPreAndPostImages?": "Record<string, unknown>"
337
- });
338
- const ListIndexesJson = type({
339
- kind: "\"listIndexes\"",
340
- collection: "string"
341
- });
342
- const ListCollectionsJson = type({ kind: "\"listCollections\"" });
343
- const FieldFilterJson = type({
344
- kind: "\"field\"",
345
- field: "string",
346
- op: "string",
347
- value: "unknown"
348
- });
349
- const ExistsFilterJson = type({
350
- kind: "\"exists\"",
351
- field: "string",
352
- exists: "boolean"
353
- });
354
- const RawInsertOneJson = type({
355
- kind: "\"rawInsertOne\"",
356
- collection: "string",
357
- document: "Record<string, unknown>"
358
- });
359
- const RawInsertManyJson = type({
360
- kind: "\"rawInsertMany\"",
361
- collection: "string",
362
- documents: "Record<string, unknown>[]"
363
- });
364
- const RawUpdateOneJson = type({
365
- kind: "\"rawUpdateOne\"",
366
- collection: "string",
367
- filter: "Record<string, unknown>",
368
- update: "Record<string, unknown> | Record<string, unknown>[]"
369
- });
370
- const RawUpdateManyJson = type({
371
- kind: "\"rawUpdateMany\"",
372
- collection: "string",
373
- filter: "Record<string, unknown>",
374
- update: "Record<string, unknown> | Record<string, unknown>[]"
375
- });
376
- const RawDeleteOneJson = type({
377
- kind: "\"rawDeleteOne\"",
378
- collection: "string",
379
- filter: "Record<string, unknown>"
380
- });
381
- const RawDeleteManyJson = type({
382
- kind: "\"rawDeleteMany\"",
383
- collection: "string",
384
- filter: "Record<string, unknown>"
385
- });
386
- const RawAggregateJson = type({
387
- kind: "\"rawAggregate\"",
388
- collection: "string",
389
- pipeline: "Record<string, unknown>[]"
390
- });
391
- const RawFindOneAndUpdateJson = type({
392
- kind: "\"rawFindOneAndUpdate\"",
393
- collection: "string",
394
- filter: "Record<string, unknown>",
395
- update: "Record<string, unknown> | Record<string, unknown>[]",
396
- upsert: "boolean"
397
- });
398
- const RawFindOneAndDeleteJson = type({
399
- kind: "\"rawFindOneAndDelete\"",
400
- collection: "string",
401
- filter: "Record<string, unknown>"
402
- });
403
- const TypedAggregateJson = type({
404
- kind: "\"aggregate\"",
405
- collection: "string",
406
- pipeline: "Record<string, unknown>[]"
407
- });
408
- const QueryPlanJson = type({
409
- collection: "string",
410
- command: "Record<string, unknown>",
411
- meta: type({
412
- target: "string",
413
- storageHash: "string",
414
- lane: "string",
415
- "targetFamily?": "string",
416
- "profileHash?": "string",
417
- "annotations?": "Record<string, unknown>"
418
- })
419
- });
420
- const CheckJson = type({
421
- description: "string",
422
- source: "Record<string, unknown>",
423
- filter: "Record<string, unknown>",
424
- expect: "\"exists\" | \"notExists\""
425
- });
426
- const StepJson = type({
427
- description: "string",
428
- command: "Record<string, unknown>"
429
- });
430
- const DdlOperationJson = type({
431
- id: "string",
432
- label: "string",
433
- operationClass: "\"additive\" | \"widening\" | \"destructive\"",
434
- precheck: "Record<string, unknown>[]",
435
- execute: "Record<string, unknown>[]",
436
- postcheck: "Record<string, unknown>[]"
437
- });
438
- const DataTransformCheckJson = type({
439
- description: "string",
440
- source: "Record<string, unknown>",
441
- filter: "Record<string, unknown>",
442
- expect: "\"exists\" | \"notExists\""
443
- });
444
- const DataTransformOperationJson = type({
445
- id: "string",
446
- label: "string",
447
- operationClass: "\"data\"",
448
- name: "string",
449
- precheck: "Record<string, unknown>[]",
450
- run: "Record<string, unknown>[]",
451
- postcheck: "Record<string, unknown>[]"
452
- });
453
- function validate(schema, data, context) {
454
- try {
455
- return schema.assert(stripUndefinedDeep(data));
456
- } catch (error) {
457
- /* v8 ignore start -- assertion libraries always throw Error instances */
458
- const message = error instanceof Error ? error.message : String(error);
459
- /* v8 ignore stop */
460
- throw new Error(`Invalid ${context}: ${message}`);
461
- }
222
+ function renderCallsToTypeScript(calls, meta) {
223
+ const imports = buildImports(calls);
224
+ const operationsBody = calls.map((c) => c.renderTypeScript()).join(",\n");
225
+ return [
226
+ shebangLineFor(detectScaffoldRuntime()),
227
+ imports,
228
+ "",
229
+ "class M extends Migration {",
230
+ buildDescribeMethod(meta),
231
+ " override get operations() {",
232
+ " return [",
233
+ indent(operationsBody, 6),
234
+ " ];",
235
+ " }",
236
+ "}",
237
+ "",
238
+ "export default M;",
239
+ "MigrationCLI.run(import.meta.url, M);",
240
+ ""
241
+ ].join("\n");
242
+ }
243
+ function buildImports(calls) {
244
+ const requirements = [...BASE_IMPORTS];
245
+ for (const call of calls) for (const req of call.importRequirements()) requirements.push(req);
246
+ return renderImports(requirements);
247
+ }
248
+ function buildDescribeMethod(meta) {
249
+ const lines = [];
250
+ lines.push(" override describe() {");
251
+ lines.push(" return {");
252
+ lines.push(` from: ${JSON.stringify(meta.from)},`);
253
+ lines.push(` to: ${JSON.stringify(meta.to)},`);
254
+ if (meta.labels && meta.labels.length > 0) lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
255
+ lines.push(" };");
256
+ lines.push(" }");
257
+ lines.push("");
258
+ return lines.join("\n");
259
+ }
260
+ function indent(text, spaces) {
261
+ const pad = " ".repeat(spaces);
262
+ return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : line).join("\n");
462
263
  }
264
+ //#endregion
265
+ //#region src/core/planner-produced-migration.ts
463
266
  /**
464
- * Strip `undefined`-valued properties before they reach arktype's optional-key
465
- * assertions.
267
+ * Planner-produced Mongo migration, returned by `MongoMigrationPlanner.plan(...)`
268
+ * and `MongoMigrationPlanner.emptyMigration(...)`.
466
269
  *
467
- * Op IRs (e.g. `CreateCollectionCommand`) assign every optional field on
468
- * every instance fields the caller did not provide land as
469
- * `undefined`-valued properties. arktype treats `{ foo?: 'boolean' }` as
470
- * "key may be absent, but if present must be boolean", so the bare instance
471
- * fails validation when it crosses the deserialize boundary in-process
472
- * (no JSON round-trip happens between planner runner). This helper
473
- * recovers the JSON-round-tripped shape (undefined keys absent) without
474
- * forcing every caller to round-trip.
270
+ * Unlike user-authored migrations (which extend `MongoMigration` from
271
+ * `@prisma-next/family-mongo/migration`), this class lives inside the target
272
+ * and holds the richer authoring IR (`OpFactoryCall[]`) needed to render
273
+ * itself back to TypeScript source. It implements
274
+ * `MigrationPlanWithAuthoringSurface` so that the CLI can uniformly ask any
275
+ * planner result to serialize itself to a `migration.ts`.
475
276
  *
476
- * Returns the original value reference whenever no change is needed.
477
- * That preserves prototype-bound payload values such as BSON wrappers
478
- * (`ObjectId`, `Decimal128`, `Binary`, …) which embed no `undefined`
479
- * own-enumerable properties and therefore never trigger a rebuild.
480
- * Top-level op IRs (class instances with `undefined` optional fields)
481
- * still get flattened to plain records as required by arktype.
277
+ * Extends the framework `Migration` base class directly (not
278
+ * `MongoMigration`) because `MongoMigration` lives in `@prisma-next/family-mongo`,
279
+ * which depends on this package extending it here would create a dependency
280
+ * cycle.
482
281
  */
483
- function stripUndefinedDeep(value) {
484
- if (Array.isArray(value)) {
485
- let changed = false;
486
- const next = value.map((item) => {
487
- const stripped = stripUndefinedDeep(item);
488
- if (stripped !== item) changed = true;
489
- return stripped;
490
- });
491
- return changed ? next : value;
282
+ var PlannerProducedMongoMigration = class extends Migration {
283
+ calls;
284
+ meta;
285
+ targetId = "mongo";
286
+ constructor(calls, meta) {
287
+ super();
288
+ this.calls = calls;
289
+ this.meta = meta;
492
290
  }
493
- if (value === null || typeof value !== "object") return value;
494
- const entries = Object.entries(value);
495
- const out = {};
496
- let changed = false;
497
- for (const [key, val] of entries) {
498
- if (val === void 0) {
499
- changed = true;
500
- continue;
501
- }
502
- const stripped = stripUndefinedDeep(val);
503
- if (stripped !== val) changed = true;
504
- out[key] = stripped;
505
- }
506
- return changed ? out : value;
507
- }
508
- function deserializeFilterExpr(json) {
509
- const record = json;
510
- const kind = record["kind"];
511
- switch (kind) {
512
- case "field": {
513
- const data = validate(FieldFilterJson, json, "field filter");
514
- return MongoFieldFilter.of(data.field, data.op, data.value);
515
- }
516
- case "and": {
517
- const exprs = record["exprs"];
518
- if (!Array.isArray(exprs)) throw new Error("Invalid and filter: missing exprs array");
519
- return MongoAndExpr.of(exprs.map(deserializeFilterExpr));
520
- }
521
- case "or": {
522
- const exprs = record["exprs"];
523
- if (!Array.isArray(exprs)) throw new Error("Invalid or filter: missing exprs array");
524
- return MongoOrExpr.of(exprs.map(deserializeFilterExpr));
525
- }
526
- case "not": {
527
- const expr = record["expr"];
528
- if (!expr || typeof expr !== "object") throw new Error("Invalid not filter: missing expr");
529
- return new MongoNotExpr(deserializeFilterExpr(expr));
530
- }
531
- case "exists": {
532
- const data = validate(ExistsFilterJson, json, "exists filter");
533
- return new MongoExistsExpr(data.field, data.exists);
534
- }
535
- default: throw new Error(`Unknown filter expression kind: ${kind}`);
291
+ get operations() {
292
+ return renderOps(this.calls);
536
293
  }
537
- }
538
- function deserializePipelineStage(json) {
539
- const record = json;
540
- const kind = record["kind"];
541
- switch (kind) {
542
- case "match": return new MongoMatchStage(deserializeFilterExpr(record["filter"]));
543
- case "limit": return new MongoLimitStage(record["limit"]);
544
- case "sort": return new MongoSortStage(record["sort"]);
545
- case "project": return new MongoProjectStage(record["projection"]);
546
- case "addFields": return new MongoAddFieldsStage(record["fields"]);
547
- case "lookup": {
548
- const opts = {
549
- from: record["from"],
550
- as: record["as"]
551
- };
552
- if (record["localField"] !== void 0) opts.localField = record["localField"];
553
- if (record["foreignField"] !== void 0) opts.foreignField = record["foreignField"];
554
- if (record["pipeline"] !== void 0) opts.pipeline = record["pipeline"].map(deserializePipelineStage);
555
- if (record["let_"] !== void 0) opts.let_ = record["let_"];
556
- return new MongoLookupStage(opts);
557
- }
558
- case "merge": {
559
- const opts = { into: record["into"] };
560
- if (record["on"] !== void 0) opts.on = record["on"];
561
- if (record["whenMatched"] !== void 0) {
562
- const wm = record["whenMatched"];
563
- opts.whenMatched = typeof wm === "string" ? wm : wm.map(deserializePipelineStage);
564
- }
565
- if (record["whenNotMatched"] !== void 0) opts.whenNotMatched = record["whenNotMatched"];
566
- return new MongoMergeStage(opts);
567
- }
568
- default: throw new Error(`Unknown pipeline stage kind: ${kind}`);
294
+ describe() {
295
+ return this.meta;
569
296
  }
570
- }
571
- function deserializeDmlCommand(json) {
572
- const kind = json["kind"];
573
- switch (kind) {
574
- case "rawInsertOne": {
575
- const data = validate(RawInsertOneJson, json, "rawInsertOne command");
576
- return new RawInsertOneCommand(data.collection, data.document);
577
- }
578
- case "rawInsertMany": {
579
- const data = validate(RawInsertManyJson, json, "rawInsertMany command");
580
- return new RawInsertManyCommand(data.collection, data.documents);
581
- }
582
- case "rawUpdateOne": {
583
- const data = validate(RawUpdateOneJson, json, "rawUpdateOne command");
584
- return new RawUpdateOneCommand(data.collection, data.filter, data.update);
585
- }
586
- case "rawUpdateMany": {
587
- const data = validate(RawUpdateManyJson, json, "rawUpdateMany command");
588
- return new RawUpdateManyCommand(data.collection, data.filter, data.update);
589
- }
590
- case "rawDeleteOne": {
591
- const data = validate(RawDeleteOneJson, json, "rawDeleteOne command");
592
- return new RawDeleteOneCommand(data.collection, data.filter);
593
- }
594
- case "rawDeleteMany": {
595
- const data = validate(RawDeleteManyJson, json, "rawDeleteMany command");
596
- return new RawDeleteManyCommand(data.collection, data.filter);
597
- }
598
- case "rawAggregate": {
599
- const data = validate(RawAggregateJson, json, "rawAggregate command");
600
- return new RawAggregateCommand(data.collection, data.pipeline);
601
- }
602
- case "rawFindOneAndUpdate": {
603
- const data = validate(RawFindOneAndUpdateJson, json, "rawFindOneAndUpdate command");
604
- return new RawFindOneAndUpdateCommand(data.collection, data.filter, data.update, data.upsert);
605
- }
606
- case "rawFindOneAndDelete": {
607
- const data = validate(RawFindOneAndDeleteJson, json, "rawFindOneAndDelete command");
608
- return new RawFindOneAndDeleteCommand(data.collection, data.filter);
609
- }
610
- case "aggregate": {
611
- const data = validate(TypedAggregateJson, json, "aggregate command");
612
- const pipeline = data.pipeline.map(deserializePipelineStage);
613
- return new AggregateCommand(data.collection, pipeline);
614
- }
615
- default: throw new Error(`Unknown DML command kind: ${kind}`);
297
+ renderTypeScript() {
298
+ return renderCallsToTypeScript(this.calls, {
299
+ from: this.meta.from,
300
+ to: this.meta.to,
301
+ ...ifDefined("labels", this.meta.labels)
302
+ });
616
303
  }
304
+ };
305
+ //#endregion
306
+ //#region src/core/mongo-planner.ts
307
+ function buildIndexLookupKey(index) {
308
+ const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
309
+ const opts = [
310
+ index.unique ? "unique" : "",
311
+ index.sparse ? "sparse" : "",
312
+ index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : "",
313
+ index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : "",
314
+ index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : "",
315
+ index.collation ? `col:${canonicalize(index.collation)}` : "",
316
+ index.weights ? `wt:${canonicalize(index.weights)}` : "",
317
+ index.default_language ? `dl:${index.default_language}` : "",
318
+ index.language_override ? `lo:${index.language_override}` : ""
319
+ ].filter(Boolean).join(";");
320
+ return opts ? `${keys}|${opts}` : keys;
617
321
  }
618
- function deserializeMongoQueryPlan(json) {
619
- const data = validate(QueryPlanJson, json, "Mongo query plan");
620
- const command = deserializeDmlCommand(data.command);
621
- const m = data.meta;
622
- const meta = {
623
- target: m.target,
624
- storageHash: m.storageHash,
625
- lane: m.lane,
626
- ...ifDefined("targetFamily", m.targetFamily),
627
- ...ifDefined("profileHash", m.profileHash),
628
- ...ifDefined("annotations", m.annotations)
629
- };
630
- return {
631
- collection: data.collection,
632
- command,
633
- meta
634
- };
322
+ function validatorsEqual(a, b) {
323
+ if (!a && !b) return true;
324
+ if (!a || !b) return false;
325
+ return a.validationLevel === b.validationLevel && a.validationAction === b.validationAction && canonicalize(a.jsonSchema) === canonicalize(b.jsonSchema);
635
326
  }
636
- function deserializeDdlCommand(json) {
637
- const kind = json["kind"];
638
- switch (kind) {
639
- case "createIndex": {
640
- const data = validate(CreateIndexJson, json, "createIndex command");
641
- return new CreateIndexCommand(data.collection, data.keys, {
642
- unique: data.unique,
643
- sparse: data.sparse,
644
- expireAfterSeconds: data.expireAfterSeconds,
645
- partialFilterExpression: data.partialFilterExpression,
646
- name: data.name,
647
- wildcardProjection: data.wildcardProjection,
648
- collation: data.collation,
649
- weights: data.weights,
650
- default_language: data.default_language,
651
- language_override: data.language_override
652
- });
653
- }
654
- case "dropIndex": {
655
- const data = validate(DropIndexJson, json, "dropIndex command");
656
- return new DropIndexCommand(data.collection, data.name);
657
- }
658
- case "createCollection": {
659
- const data = validate(CreateCollectionJson, json, "createCollection command");
660
- return new CreateCollectionCommand(data.collection, {
661
- validator: data.validator,
662
- validationLevel: data.validationLevel,
663
- validationAction: data.validationAction,
664
- capped: data.capped,
665
- size: data.size,
666
- max: data.max,
667
- timeseries: data.timeseries,
668
- collation: data.collation,
669
- changeStreamPreAndPostImages: data.changeStreamPreAndPostImages,
670
- clusteredIndex: data.clusteredIndex
671
- });
672
- }
673
- case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
674
- case "collMod": {
675
- const data = validate(CollModJson, json, "collMod command");
676
- return new CollModCommand(data.collection, {
677
- validator: data.validator,
678
- validationLevel: data.validationLevel,
679
- validationAction: data.validationAction,
680
- changeStreamPreAndPostImages: data.changeStreamPreAndPostImages
681
- });
682
- }
683
- default: throw new Error(`Unknown DDL command kind: ${kind}`);
327
+ function classifyValidatorUpdate(origin, dest) {
328
+ let hasDestructive = false;
329
+ if (canonicalize(origin.jsonSchema) !== canonicalize(dest.jsonSchema)) hasDestructive = true;
330
+ if (origin.validationAction !== dest.validationAction) {
331
+ if (dest.validationAction === "error") hasDestructive = true;
684
332
  }
685
- }
686
- function deserializeInspectionCommand(json) {
687
- const kind = json["kind"];
688
- switch (kind) {
689
- case "listIndexes": return new ListIndexesCommand(validate(ListIndexesJson, json, "listIndexes command").collection);
690
- case "listCollections":
691
- validate(ListCollectionsJson, json, "listCollections command");
692
- return new ListCollectionsCommand();
693
- default: throw new Error(`Unknown inspection command kind: ${kind}`);
333
+ if (origin.validationLevel !== dest.validationLevel) {
334
+ if (dest.validationLevel === "strict") hasDestructive = true;
694
335
  }
336
+ return hasDestructive ? "destructive" : "widening";
695
337
  }
696
- function deserializeCheck(json) {
697
- const data = validate(CheckJson, json, "migration check");
698
- return {
699
- description: data.description,
700
- source: deserializeInspectionCommand(data.source),
701
- filter: deserializeFilterExpr(data.filter),
702
- expect: data.expect
703
- };
704
- }
705
- function deserializeStep(json) {
706
- const data = validate(StepJson, json, "migration step");
707
- return {
708
- description: data.description,
709
- command: deserializeDdlCommand(data.command)
710
- };
338
+ function hasImmutableOptionChange(origin, dest) {
339
+ if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
340
+ if (canonicalize(origin?.timeseries) !== canonicalize(dest?.timeseries)) return "timeseries";
341
+ if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
342
+ if (canonicalize(origin?.clusteredIndex) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
711
343
  }
712
- function isDataTransformJson(json) {
713
- return typeof json === "object" && json !== null && json["operationClass"] === "data";
344
+ function collectionHasOptions(coll) {
345
+ return !!(coll.options || coll.validator);
714
346
  }
715
- function deserializeDdlOp(json) {
716
- const data = validate(DdlOperationJson, json, "migration operation");
717
- return {
718
- id: data.id,
719
- label: data.label,
720
- operationClass: data.operationClass,
721
- precheck: data.precheck.map(deserializeCheck),
722
- execute: data.execute.map(deserializeStep),
723
- postcheck: data.postcheck.map(deserializeCheck)
724
- };
725
- }
726
- function deserializeDataTransformCheck(json) {
727
- const data = validate(DataTransformCheckJson, json, "data transform check");
728
- return {
729
- description: data.description,
730
- source: deserializeMongoQueryPlan(data.source),
731
- filter: deserializeFilterExpr(data.filter),
732
- expect: data.expect
733
- };
734
- }
735
- function deserializeDataTransformOp(json) {
736
- const data = validate(DataTransformOperationJson, json, "data transform operation");
737
- return {
738
- id: data.id,
739
- label: data.label,
740
- operationClass: "data",
741
- name: data.name,
742
- precheck: data.precheck.map(deserializeDataTransformCheck),
743
- run: data.run.map(deserializeMongoQueryPlan),
744
- postcheck: data.postcheck.map(deserializeDataTransformCheck)
745
- };
347
+ var MongoMigrationPlanner = class {
348
+ planCalls(options) {
349
+ const contract = options.contract;
350
+ const originIR = options.schema;
351
+ const destinationIR = contractToMongoSchemaIR(contract);
352
+ const collCreates = [];
353
+ const drops = [];
354
+ const creates = [];
355
+ const validatorOps = [];
356
+ const mutableOptionOps = [];
357
+ const collDrops = [];
358
+ const conflicts = [];
359
+ const allCollectionNames = new Set([...originIR.collectionNames, ...destinationIR.collectionNames]);
360
+ for (const collName of [...allCollectionNames].sort()) {
361
+ const originColl = originIR.collection(collName);
362
+ const destColl = destinationIR.collection(collName);
363
+ if (!originColl) {
364
+ if (destColl && (collectionHasOptions(destColl) || destColl.indexes.length === 0)) {
365
+ const opts = collectionHasOptions(destColl) ? schemaCollectionToCreateCollectionOptions(destColl) : void 0;
366
+ collCreates.push(new CreateCollectionCall(collName, opts));
367
+ }
368
+ } else if (!destColl) collDrops.push(new DropCollectionCall(collName));
369
+ else {
370
+ const immutableChange = hasImmutableOptionChange(originColl.options, destColl.options);
371
+ if (immutableChange) conflicts.push({
372
+ kind: "policy-violation",
373
+ summary: `Cannot change immutable collection option '${immutableChange}' on ${collName}`,
374
+ why: `MongoDB does not support modifying the '${immutableChange}' option after collection creation`
375
+ });
376
+ const mutableCall = planMutableOptionsDiffCall(collName, originColl.options, destColl.options);
377
+ if (mutableCall) mutableOptionOps.push(mutableCall);
378
+ const validatorCall = planValidatorDiffCall(collName, originColl.validator, destColl.validator);
379
+ if (validatorCall) validatorOps.push(validatorCall);
380
+ }
381
+ const originLookup = /* @__PURE__ */ new Map();
382
+ if (originColl) for (const idx of originColl.indexes) originLookup.set(buildIndexLookupKey(idx), idx);
383
+ const destLookup = /* @__PURE__ */ new Map();
384
+ if (destColl) for (const idx of destColl.indexes) destLookup.set(buildIndexLookupKey(idx), idx);
385
+ for (const [lookupKey, idx] of originLookup) if (!destLookup.has(lookupKey)) drops.push(new DropIndexCall(collName, idx.keys));
386
+ for (const [lookupKey, idx] of destLookup) if (!originLookup.has(lookupKey)) creates.push(new CreateIndexCall(collName, idx.keys, schemaIndexToCreateIndexOptions(idx)));
387
+ }
388
+ if (conflicts.length > 0) return {
389
+ kind: "failure",
390
+ conflicts
391
+ };
392
+ const allCalls = [
393
+ ...collCreates,
394
+ ...drops,
395
+ ...creates,
396
+ ...validatorOps,
397
+ ...mutableOptionOps,
398
+ ...collDrops
399
+ ];
400
+ for (const call of allCalls) if (!options.policy.allowedOperationClasses.includes(call.operationClass)) conflicts.push({
401
+ kind: "policy-violation",
402
+ summary: `${call.operationClass} operation disallowed: ${call.label}`,
403
+ why: `Policy does not allow '${call.operationClass}' operations`
404
+ });
405
+ if (conflicts.length > 0) return {
406
+ kind: "failure",
407
+ conflicts
408
+ };
409
+ return {
410
+ kind: "success",
411
+ calls: allCalls
412
+ };
413
+ }
414
+ plan(options) {
415
+ const contract = options.contract;
416
+ const result = this.planCalls(options);
417
+ if (result.kind === "failure") return result;
418
+ return {
419
+ kind: "success",
420
+ plan: new PlannerProducedMongoMigration(result.calls, {
421
+ from: options.fromContract?.storage.storageHash ?? null,
422
+ to: contract.storage.storageHash
423
+ })
424
+ };
425
+ }
426
+ /**
427
+ * Produce an empty `migration.ts` authoring surface for `migration new`.
428
+ *
429
+ * The "empty migration" is a `PlannerProducedMongoMigration` with no
430
+ * operations; `renderTypeScript()` emits a stub class with the correct
431
+ * `from`/`to` metadata that the user then fills in with operations. The
432
+ * contract path on the context is unused — Mongo's emitted source does
433
+ * not import from the generated contract `.d.ts`.
434
+ */
435
+ emptyMigration(context) {
436
+ return new PlannerProducedMongoMigration([], {
437
+ from: context.fromHash,
438
+ to: context.toHash
439
+ });
440
+ }
441
+ };
442
+ function planValidatorDiffCall(collName, originValidator, destValidator) {
443
+ if (validatorsEqual(originValidator, destValidator)) return void 0;
444
+ if (destValidator) {
445
+ const operationClass = originValidator ? classifyValidatorUpdate(originValidator, destValidator) : "destructive";
446
+ return new CollModCall(collName, {
447
+ validator: { $jsonSchema: destValidator.jsonSchema },
448
+ validationLevel: destValidator.validationLevel,
449
+ validationAction: destValidator.validationAction
450
+ }, {
451
+ id: `validator.${collName}.${originValidator ? "update" : "add"}`,
452
+ label: `${originValidator ? "Update" : "Add"} validator on ${collName}`,
453
+ operationClass
454
+ });
455
+ }
456
+ return new CollModCall(collName, {
457
+ validator: {},
458
+ validationLevel: "strict",
459
+ validationAction: "error"
460
+ }, {
461
+ id: `validator.${collName}.remove`,
462
+ label: `Remove validator on ${collName}`,
463
+ operationClass: "widening"
464
+ });
746
465
  }
747
- function deserializeMongoOp(json) {
748
- if (isDataTransformJson(json)) return deserializeDataTransformOp(json);
749
- return deserializeDdlOp(json);
466
+ function planMutableOptionsDiffCall(collName, origin, dest) {
467
+ const originCSPPI = origin?.changeStreamPreAndPostImages;
468
+ const destCSPPI = dest?.changeStreamPreAndPostImages;
469
+ if (deepEqual(originCSPPI, destCSPPI)) return void 0;
470
+ const desiredCSPPI = destCSPPI ?? { enabled: false };
471
+ return new CollModCall(collName, { changeStreamPreAndPostImages: desiredCSPPI }, {
472
+ id: `options.${collName}.update`,
473
+ label: `Update mutable options on ${collName}`,
474
+ operationClass: desiredCSPPI.enabled ? "widening" : "destructive"
475
+ });
750
476
  }
751
- function deserializeMongoOps(json) {
752
- return json.map(deserializeMongoOp);
477
+ //#endregion
478
+ //#region src/core/filter-evaluator.ts
479
+ function getNestedField(doc, path) {
480
+ const parts = path.split(".");
481
+ let current = doc;
482
+ for (const part of parts) {
483
+ if (current === null || current === void 0 || typeof current !== "object") return;
484
+ const record = current;
485
+ if (!Object.hasOwn(record, part)) return;
486
+ current = record[part];
487
+ }
488
+ return current;
753
489
  }
754
- function serializeMongoOps(ops) {
755
- return JSON.stringify(ops, null, 2);
490
+ function evaluateFieldOp(op, actual, expected) {
491
+ switch (op) {
492
+ case "$eq": return deepEqual(actual, expected);
493
+ case "$ne": return !deepEqual(actual, expected);
494
+ case "$gt": return typeof actual === typeof expected && actual > expected;
495
+ case "$gte": return typeof actual === typeof expected && actual >= expected;
496
+ case "$lt": return typeof actual === typeof expected && actual < expected;
497
+ case "$lte": return typeof actual === typeof expected && actual <= expected;
498
+ case "$in": return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
499
+ default: throw new Error(`Unsupported filter operator in migration check: ${op}`);
500
+ }
756
501
  }
757
- //#endregion
758
- //#region src/core/op-factory-call.ts
759
- const TARGET_MIGRATION_MODULE = "@prisma-next/target-mongo/migration";
760
- var OpFactoryCallNode = class extends TsExpression {
761
- importRequirements() {
762
- return [{
763
- moduleSpecifier: TARGET_MIGRATION_MODULE,
764
- symbol: this.factoryName
765
- }];
502
+ var FilterEvaluator = class {
503
+ doc = {};
504
+ evaluate(filter, doc) {
505
+ this.doc = doc;
506
+ return filter.accept(this);
766
507
  }
767
- freeze() {
768
- Object.freeze(this);
508
+ field(expr) {
509
+ const value = getNestedField(this.doc, expr.field);
510
+ return evaluateFieldOp(expr.op, value, expr.value);
511
+ }
512
+ and(expr) {
513
+ return expr.exprs.every((child) => child.accept(this));
514
+ }
515
+ or(expr) {
516
+ return expr.exprs.some((child) => child.accept(this));
517
+ }
518
+ not(expr) {
519
+ return !expr.expr.accept(this);
520
+ }
521
+ exists(expr) {
522
+ const has = getNestedField(this.doc, expr.field) !== void 0;
523
+ return expr.exists ? has : !has;
524
+ }
525
+ expr(_expr) {
526
+ throw new Error("Aggregation expression filters are not supported in migration checks");
527
+ }
528
+ };
529
+ //#endregion
530
+ //#region src/core/mongo-ops-serializer.ts
531
+ const CreateIndexJson = type({
532
+ kind: "\"createIndex\"",
533
+ collection: "string",
534
+ keys: type({
535
+ field: "string",
536
+ direction: type("1 | -1 | \"text\" | \"2dsphere\" | \"2d\" | \"hashed\"")
537
+ }).array().atLeastLength(1),
538
+ "unique?": "boolean",
539
+ "sparse?": "boolean",
540
+ "expireAfterSeconds?": "number",
541
+ "partialFilterExpression?": "Record<string, unknown>",
542
+ "name?": "string",
543
+ "wildcardProjection?": "Record<string, unknown>",
544
+ "collation?": "Record<string, unknown>",
545
+ "weights?": "Record<string, unknown>",
546
+ "default_language?": "string",
547
+ "language_override?": "string"
548
+ });
549
+ const DropIndexJson = type({
550
+ kind: "\"dropIndex\"",
551
+ collection: "string",
552
+ name: "string"
553
+ });
554
+ const CreateCollectionJson = type({
555
+ kind: "\"createCollection\"",
556
+ collection: "string",
557
+ "validator?": "Record<string, unknown>",
558
+ "validationLevel?": "\"strict\" | \"moderate\"",
559
+ "validationAction?": "\"error\" | \"warn\"",
560
+ "capped?": "boolean",
561
+ "size?": "number",
562
+ "max?": "number",
563
+ "timeseries?": "Record<string, unknown>",
564
+ "collation?": "Record<string, unknown>",
565
+ "changeStreamPreAndPostImages?": "Record<string, unknown>",
566
+ "clusteredIndex?": "Record<string, unknown>"
567
+ });
568
+ const DropCollectionJson = type({
569
+ kind: "\"dropCollection\"",
570
+ collection: "string"
571
+ });
572
+ const CollModJson = type({
573
+ kind: "\"collMod\"",
574
+ collection: "string",
575
+ "validator?": "Record<string, unknown>",
576
+ "validationLevel?": "\"strict\" | \"moderate\"",
577
+ "validationAction?": "\"error\" | \"warn\"",
578
+ "changeStreamPreAndPostImages?": "Record<string, unknown>"
579
+ });
580
+ const ListIndexesJson = type({
581
+ kind: "\"listIndexes\"",
582
+ collection: "string"
583
+ });
584
+ const ListCollectionsJson = type({ kind: "\"listCollections\"" });
585
+ const FieldFilterJson = type({
586
+ kind: "\"field\"",
587
+ field: "string",
588
+ op: "string",
589
+ value: "unknown"
590
+ });
591
+ const ExistsFilterJson = type({
592
+ kind: "\"exists\"",
593
+ field: "string",
594
+ exists: "boolean"
595
+ });
596
+ const RawInsertOneJson = type({
597
+ kind: "\"rawInsertOne\"",
598
+ collection: "string",
599
+ document: "Record<string, unknown>"
600
+ });
601
+ const RawInsertManyJson = type({
602
+ kind: "\"rawInsertMany\"",
603
+ collection: "string",
604
+ documents: "Record<string, unknown>[]"
605
+ });
606
+ const RawUpdateOneJson = type({
607
+ kind: "\"rawUpdateOne\"",
608
+ collection: "string",
609
+ filter: "Record<string, unknown>",
610
+ update: "Record<string, unknown> | Record<string, unknown>[]"
611
+ });
612
+ const RawUpdateManyJson = type({
613
+ kind: "\"rawUpdateMany\"",
614
+ collection: "string",
615
+ filter: "Record<string, unknown>",
616
+ update: "Record<string, unknown> | Record<string, unknown>[]"
617
+ });
618
+ const RawDeleteOneJson = type({
619
+ kind: "\"rawDeleteOne\"",
620
+ collection: "string",
621
+ filter: "Record<string, unknown>"
622
+ });
623
+ const RawDeleteManyJson = type({
624
+ kind: "\"rawDeleteMany\"",
625
+ collection: "string",
626
+ filter: "Record<string, unknown>"
627
+ });
628
+ const RawAggregateJson = type({
629
+ kind: "\"rawAggregate\"",
630
+ collection: "string",
631
+ pipeline: "Record<string, unknown>[]"
632
+ });
633
+ const RawFindOneAndUpdateJson = type({
634
+ kind: "\"rawFindOneAndUpdate\"",
635
+ collection: "string",
636
+ filter: "Record<string, unknown>",
637
+ update: "Record<string, unknown> | Record<string, unknown>[]",
638
+ upsert: "boolean"
639
+ });
640
+ const RawFindOneAndDeleteJson = type({
641
+ kind: "\"rawFindOneAndDelete\"",
642
+ collection: "string",
643
+ filter: "Record<string, unknown>"
644
+ });
645
+ const TypedAggregateJson = type({
646
+ kind: "\"aggregate\"",
647
+ collection: "string",
648
+ pipeline: "Record<string, unknown>[]"
649
+ });
650
+ const QueryPlanJson = type({
651
+ collection: "string",
652
+ command: "Record<string, unknown>",
653
+ meta: type({
654
+ target: "string",
655
+ storageHash: "string",
656
+ lane: "string",
657
+ "targetFamily?": "string",
658
+ "profileHash?": "string",
659
+ "annotations?": "Record<string, unknown>"
660
+ })
661
+ });
662
+ const CheckJson = type({
663
+ description: "string",
664
+ source: "Record<string, unknown>",
665
+ filter: "Record<string, unknown>",
666
+ expect: "\"exists\" | \"notExists\""
667
+ });
668
+ const StepJson = type({
669
+ description: "string",
670
+ command: "Record<string, unknown>"
671
+ });
672
+ const DdlOperationJson = type({
673
+ id: "string",
674
+ label: "string",
675
+ operationClass: "\"additive\" | \"widening\" | \"destructive\"",
676
+ precheck: "Record<string, unknown>[]",
677
+ execute: "Record<string, unknown>[]",
678
+ postcheck: "Record<string, unknown>[]"
679
+ });
680
+ const DataTransformCheckJson = type({
681
+ description: "string",
682
+ source: "Record<string, unknown>",
683
+ filter: "Record<string, unknown>",
684
+ expect: "\"exists\" | \"notExists\""
685
+ });
686
+ const DataTransformOperationJson = type({
687
+ id: "string",
688
+ label: "string",
689
+ operationClass: "\"data\"",
690
+ name: "string",
691
+ precheck: "Record<string, unknown>[]",
692
+ run: "Record<string, unknown>[]",
693
+ postcheck: "Record<string, unknown>[]"
694
+ });
695
+ function validate(schema, data, context) {
696
+ try {
697
+ return schema.assert(stripUndefinedDeep(data));
698
+ } catch (error) {
699
+ /* v8 ignore start -- assertion libraries always throw Error instances */
700
+ const message = error instanceof Error ? error.message : String(error);
701
+ /* v8 ignore stop */
702
+ throw new Error(`Invalid ${context}: ${message}`);
769
703
  }
770
- };
771
- function formatKeys(keys) {
772
- return keys.map((k) => `${k.field}:${k.direction}`).join(", ");
773
704
  }
774
- var CreateIndexCall = class extends OpFactoryCallNode {
775
- factoryName = "createIndex";
776
- operationClass = "additive";
777
- collection;
778
- keys;
779
- options;
780
- label;
781
- constructor(collection, keys, options) {
782
- super();
783
- this.collection = collection;
784
- this.keys = keys;
785
- this.options = options;
786
- this.label = `Create index on ${collection} (${formatKeys(keys)})`;
787
- this.freeze();
788
- }
789
- toOp() {
790
- return createIndex(this.collection, this.keys, this.options);
791
- }
792
- renderTypeScript() {
793
- return this.options ? `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)}, ${jsonToTsSource(this.options)})` : `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
794
- }
795
- };
796
- var DropIndexCall = class extends OpFactoryCallNode {
797
- factoryName = "dropIndex";
798
- operationClass = "destructive";
799
- collection;
800
- keys;
801
- label;
802
- constructor(collection, keys) {
803
- super();
804
- this.collection = collection;
805
- this.keys = keys;
806
- this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
807
- this.freeze();
808
- }
809
- toOp() {
810
- return dropIndex(this.collection, this.keys);
811
- }
812
- renderTypeScript() {
813
- return `dropIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
814
- }
815
- };
816
- var CreateCollectionCall = class extends OpFactoryCallNode {
817
- factoryName = "createCollection";
818
- operationClass = "additive";
819
- collection;
820
- options;
821
- label;
822
- constructor(collection, options) {
823
- super();
824
- this.collection = collection;
825
- this.options = options;
826
- this.label = `Create collection ${collection}`;
827
- this.freeze();
828
- }
829
- toOp() {
830
- return createCollection(this.collection, this.options);
831
- }
832
- renderTypeScript() {
833
- return this.options ? `createCollection(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})` : `createCollection(${jsonToTsSource(this.collection)})`;
834
- }
835
- };
836
- var DropCollectionCall = class extends OpFactoryCallNode {
837
- factoryName = "dropCollection";
838
- operationClass = "destructive";
839
- collection;
840
- label;
841
- constructor(collection) {
842
- super();
843
- this.collection = collection;
844
- this.label = `Drop collection ${collection}`;
845
- this.freeze();
846
- }
847
- toOp() {
848
- return dropCollection(this.collection);
849
- }
850
- renderTypeScript() {
851
- return `dropCollection(${jsonToTsSource(this.collection)})`;
705
+ /**
706
+ * Strip `undefined`-valued properties before they reach arktype's optional-key
707
+ * assertions.
708
+ *
709
+ * Op IRs (e.g. `CreateCollectionCommand`) assign every optional field on
710
+ * every instance — fields the caller did not provide land as
711
+ * `undefined`-valued properties. arktype treats `{ foo?: 'boolean' }` as
712
+ * "key may be absent, but if present must be boolean", so the bare instance
713
+ * fails validation when it crosses the deserialize boundary in-process
714
+ * (no JSON round-trip happens between planner → runner). This helper
715
+ * recovers the JSON-round-tripped shape (undefined keys absent) without
716
+ * forcing every caller to round-trip.
717
+ *
718
+ * Returns the original value reference whenever no change is needed.
719
+ * That preserves prototype-bound payload values such as BSON wrappers
720
+ * (`ObjectId`, `Decimal128`, `Binary`, …) which embed no `undefined`
721
+ * own-enumerable properties and therefore never trigger a rebuild.
722
+ * Top-level op IRs (class instances with `undefined` optional fields)
723
+ * still get flattened to plain records as required by arktype.
724
+ */
725
+ function stripUndefinedDeep(value) {
726
+ if (Array.isArray(value)) {
727
+ let changed = false;
728
+ const next = value.map((item) => {
729
+ const stripped = stripUndefinedDeep(item);
730
+ if (stripped !== item) changed = true;
731
+ return stripped;
732
+ });
733
+ return changed ? next : value;
852
734
  }
853
- };
854
- var CollModCall = class extends OpFactoryCallNode {
855
- factoryName = "collMod";
856
- collection;
857
- options;
858
- meta;
859
- operationClass;
860
- label;
861
- constructor(collection, options, meta) {
862
- super();
863
- this.collection = collection;
864
- this.options = options;
865
- this.meta = meta;
866
- this.operationClass = meta?.operationClass ?? "destructive";
867
- this.label = meta?.label ?? `Modify collection ${collection}`;
868
- this.freeze();
735
+ if (value === null || typeof value !== "object") return value;
736
+ const entries = Object.entries(value);
737
+ const out = {};
738
+ let changed = false;
739
+ for (const [key, val] of entries) {
740
+ if (val === void 0) {
741
+ changed = true;
742
+ continue;
743
+ }
744
+ const stripped = stripUndefinedDeep(val);
745
+ if (stripped !== val) changed = true;
746
+ out[key] = stripped;
869
747
  }
870
- toOp() {
871
- return collMod(this.collection, this.options, this.meta);
748
+ return changed ? out : value;
749
+ }
750
+ function deserializeFilterExpr(json) {
751
+ const record = json;
752
+ const kind = record["kind"];
753
+ switch (kind) {
754
+ case "field": {
755
+ const data = validate(FieldFilterJson, json, "field filter");
756
+ return MongoFieldFilter.of(data.field, data.op, data.value);
757
+ }
758
+ case "and": {
759
+ const exprs = record["exprs"];
760
+ if (!Array.isArray(exprs)) throw new Error("Invalid and filter: missing exprs array");
761
+ return MongoAndExpr.of(exprs.map(deserializeFilterExpr));
762
+ }
763
+ case "or": {
764
+ const exprs = record["exprs"];
765
+ if (!Array.isArray(exprs)) throw new Error("Invalid or filter: missing exprs array");
766
+ return MongoOrExpr.of(exprs.map(deserializeFilterExpr));
767
+ }
768
+ case "not": {
769
+ const expr = record["expr"];
770
+ if (!expr || typeof expr !== "object") throw new Error("Invalid not filter: missing expr");
771
+ return new MongoNotExpr(deserializeFilterExpr(expr));
772
+ }
773
+ case "exists": {
774
+ const data = validate(ExistsFilterJson, json, "exists filter");
775
+ return new MongoExistsExpr(data.field, data.exists);
776
+ }
777
+ default: throw new Error(`Unknown filter expression kind: ${kind}`);
872
778
  }
873
- renderTypeScript() {
874
- return this.meta ? `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)}, ${jsonToTsSource(this.meta)})` : `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})`;
779
+ }
780
+ function deserializePipelineStage(json) {
781
+ const record = json;
782
+ const kind = record["kind"];
783
+ switch (kind) {
784
+ case "match": return new MongoMatchStage(deserializeFilterExpr(record["filter"]));
785
+ case "limit": return new MongoLimitStage(record["limit"]);
786
+ case "sort": return new MongoSortStage(record["sort"]);
787
+ case "project": return new MongoProjectStage(record["projection"]);
788
+ case "addFields": return new MongoAddFieldsStage(record["fields"]);
789
+ case "lookup": {
790
+ const opts = {
791
+ from: record["from"],
792
+ as: record["as"]
793
+ };
794
+ if (record["localField"] !== void 0) opts.localField = record["localField"];
795
+ if (record["foreignField"] !== void 0) opts.foreignField = record["foreignField"];
796
+ if (record["pipeline"] !== void 0) opts.pipeline = record["pipeline"].map(deserializePipelineStage);
797
+ if (record["let_"] !== void 0) opts.let_ = record["let_"];
798
+ return new MongoLookupStage(opts);
799
+ }
800
+ case "merge": {
801
+ const opts = { into: record["into"] };
802
+ if (record["on"] !== void 0) opts.on = record["on"];
803
+ if (record["whenMatched"] !== void 0) {
804
+ const wm = record["whenMatched"];
805
+ opts.whenMatched = typeof wm === "string" ? wm : wm.map(deserializePipelineStage);
806
+ }
807
+ if (record["whenNotMatched"] !== void 0) opts.whenNotMatched = record["whenNotMatched"];
808
+ return new MongoMergeStage(opts);
809
+ }
810
+ default: throw new Error(`Unknown pipeline stage kind: ${kind}`);
811
+ }
812
+ }
813
+ function deserializeDmlCommand(json) {
814
+ const kind = json["kind"];
815
+ switch (kind) {
816
+ case "rawInsertOne": {
817
+ const data = validate(RawInsertOneJson, json, "rawInsertOne command");
818
+ return new RawInsertOneCommand(data.collection, data.document);
819
+ }
820
+ case "rawInsertMany": {
821
+ const data = validate(RawInsertManyJson, json, "rawInsertMany command");
822
+ return new RawInsertManyCommand(data.collection, data.documents);
823
+ }
824
+ case "rawUpdateOne": {
825
+ const data = validate(RawUpdateOneJson, json, "rawUpdateOne command");
826
+ return new RawUpdateOneCommand(data.collection, data.filter, data.update);
827
+ }
828
+ case "rawUpdateMany": {
829
+ const data = validate(RawUpdateManyJson, json, "rawUpdateMany command");
830
+ return new RawUpdateManyCommand(data.collection, data.filter, data.update);
831
+ }
832
+ case "rawDeleteOne": {
833
+ const data = validate(RawDeleteOneJson, json, "rawDeleteOne command");
834
+ return new RawDeleteOneCommand(data.collection, data.filter);
835
+ }
836
+ case "rawDeleteMany": {
837
+ const data = validate(RawDeleteManyJson, json, "rawDeleteMany command");
838
+ return new RawDeleteManyCommand(data.collection, data.filter);
839
+ }
840
+ case "rawAggregate": {
841
+ const data = validate(RawAggregateJson, json, "rawAggregate command");
842
+ return new RawAggregateCommand(data.collection, data.pipeline);
843
+ }
844
+ case "rawFindOneAndUpdate": {
845
+ const data = validate(RawFindOneAndUpdateJson, json, "rawFindOneAndUpdate command");
846
+ return new RawFindOneAndUpdateCommand(data.collection, data.filter, data.update, data.upsert);
847
+ }
848
+ case "rawFindOneAndDelete": {
849
+ const data = validate(RawFindOneAndDeleteJson, json, "rawFindOneAndDelete command");
850
+ return new RawFindOneAndDeleteCommand(data.collection, data.filter);
851
+ }
852
+ case "aggregate": {
853
+ const data = validate(TypedAggregateJson, json, "aggregate command");
854
+ const pipeline = data.pipeline.map(deserializePipelineStage);
855
+ return new AggregateCommand(data.collection, pipeline);
856
+ }
857
+ default: throw new Error(`Unknown DML command kind: ${kind}`);
875
858
  }
876
- };
877
- function schemaIndexToCreateIndexOptions(index) {
878
- return {
879
- unique: index.unique || void 0,
880
- sparse: index.sparse,
881
- expireAfterSeconds: index.expireAfterSeconds,
882
- partialFilterExpression: index.partialFilterExpression,
883
- wildcardProjection: index.wildcardProjection,
884
- collation: index.collation,
885
- weights: index.weights,
886
- default_language: index.default_language,
887
- language_override: index.language_override
888
- };
889
859
  }
890
- function schemaCollectionToCreateCollectionOptions(coll) {
891
- const opts = coll.options;
892
- const validator = coll.validator;
893
- if (!opts && !validator) return void 0;
860
+ function deserializeMongoQueryPlan(json) {
861
+ const data = validate(QueryPlanJson, json, "Mongo query plan");
862
+ const command = deserializeDmlCommand(data.command);
863
+ const m = data.meta;
864
+ const meta = {
865
+ target: m.target,
866
+ storageHash: m.storageHash,
867
+ lane: m.lane,
868
+ ...ifDefined("targetFamily", m.targetFamily),
869
+ ...ifDefined("profileHash", m.profileHash),
870
+ ...ifDefined("annotations", m.annotations)
871
+ };
894
872
  return {
895
- capped: opts?.capped ? true : void 0,
896
- size: opts?.capped?.size,
897
- max: opts?.capped?.max,
898
- timeseries: opts?.timeseries,
899
- collation: opts?.collation,
900
- clusteredIndex: opts?.clusteredIndex ? {
901
- key: { _id: 1 },
902
- unique: true,
903
- ...opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}
904
- } : void 0,
905
- validator: validator ? { $jsonSchema: validator.jsonSchema } : void 0,
906
- validationLevel: validator?.validationLevel,
907
- validationAction: validator?.validationAction,
908
- changeStreamPreAndPostImages: opts?.changeStreamPreAndPostImages
873
+ collection: data.collection,
874
+ command,
875
+ meta
909
876
  };
910
877
  }
911
- //#endregion
912
- //#region src/core/render-ops.ts
913
- function renderOps(calls) {
914
- return calls.map((call) => call.toOp());
915
- }
916
- //#endregion
917
- //#region src/core/render-typescript.ts
918
- /**
919
- * Always-present base imports for the rendered scaffold:
920
- *
921
- * - `Migration` from `@prisma-next/family-mongo/migration` — the
922
- * user-facing Mongo `Migration` base; subclasses don't need to
923
- * redeclare `targetId` or thread family/target generics.
924
- * - `MigrationCLI` from `@prisma-next/cli/migration-cli` — the
925
- * migration-file CLI entrypoint that loads `prisma-next.config.ts`,
926
- * assembles a `ControlStack`, and instantiates the migration class.
927
- * The migration file owns this dependency directly: pulling CLI
928
- * machinery in at script run time is acceptable because the script's
929
- * whole purpose is to be invoked from the project that owns the
930
- * config. (Mirrors the postgres facade pattern; pulling `MigrationCLI`
931
- * into `@prisma-next/family-mongo/migration` so a Mongo migration only
932
- * needs one import is tracked separately as a follow-up.)
933
- */
934
- const BASE_IMPORTS = [{
935
- moduleSpecifier: "@prisma-next/family-mongo/migration",
936
- symbol: "Migration"
937
- }, {
938
- moduleSpecifier: "@prisma-next/cli/migration-cli",
939
- symbol: "MigrationCLI"
940
- }];
941
- /**
942
- * Render a list of Mongo `OpFactoryCall`s as a `migration.ts`
943
- * source string. The result is shebanged, extends the user-facing
944
- * `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and
945
- * implements the abstract `operations` and `describe` members. `meta` is
946
- * always rendered — `describe()` is part of the `Migration` contract, so
947
- * even an empty stub must satisfy it; callers pass `from: null` for a
948
- * baseline `migration-new` scaffold (and a real `to` hash either way).
949
- *
950
- * The walk is polymorphic: each call node contributes its own
951
- * `renderTypeScript()` expression and declares its own
952
- * `importRequirements()`. The top-level renderer aggregates imports
953
- * across all nodes and emits one `import { … } from "…"` line per module.
954
- * The `Migration` and `MigrationCLI` imports are always emitted — they're
955
- * structural to the rendered scaffold (extends `Migration`, calls
956
- * `MigrationCLI.run`), not driven by any node.
957
- */
958
- function renderCallsToTypeScript(calls, meta) {
959
- const imports = buildImports(calls);
960
- const operationsBody = calls.map((c) => c.renderTypeScript()).join(",\n");
961
- return [
962
- shebangLineFor(detectScaffoldRuntime()),
963
- imports,
964
- "",
965
- "class M extends Migration {",
966
- buildDescribeMethod(meta),
967
- " override get operations() {",
968
- " return [",
969
- indent(operationsBody, 6),
970
- " ];",
971
- " }",
972
- "}",
973
- "",
974
- "export default M;",
975
- "MigrationCLI.run(import.meta.url, M);",
976
- ""
977
- ].join("\n");
978
- }
979
- function buildImports(calls) {
980
- const requirements = [...BASE_IMPORTS];
981
- for (const call of calls) for (const req of call.importRequirements()) requirements.push(req);
982
- return renderImports(requirements);
878
+ function deserializeDdlCommand(json) {
879
+ const kind = json["kind"];
880
+ switch (kind) {
881
+ case "createIndex": {
882
+ const data = validate(CreateIndexJson, json, "createIndex command");
883
+ return new CreateIndexCommand(data.collection, data.keys, {
884
+ unique: data.unique,
885
+ sparse: data.sparse,
886
+ expireAfterSeconds: data.expireAfterSeconds,
887
+ partialFilterExpression: data.partialFilterExpression,
888
+ name: data.name,
889
+ wildcardProjection: data.wildcardProjection,
890
+ collation: data.collation,
891
+ weights: data.weights,
892
+ default_language: data.default_language,
893
+ language_override: data.language_override
894
+ });
895
+ }
896
+ case "dropIndex": {
897
+ const data = validate(DropIndexJson, json, "dropIndex command");
898
+ return new DropIndexCommand(data.collection, data.name);
899
+ }
900
+ case "createCollection": {
901
+ const data = validate(CreateCollectionJson, json, "createCollection command");
902
+ return new CreateCollectionCommand(data.collection, {
903
+ validator: data.validator,
904
+ validationLevel: data.validationLevel,
905
+ validationAction: data.validationAction,
906
+ capped: data.capped,
907
+ size: data.size,
908
+ max: data.max,
909
+ timeseries: data.timeseries,
910
+ collation: data.collation,
911
+ changeStreamPreAndPostImages: data.changeStreamPreAndPostImages,
912
+ clusteredIndex: data.clusteredIndex
913
+ });
914
+ }
915
+ case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
916
+ case "collMod": {
917
+ const data = validate(CollModJson, json, "collMod command");
918
+ return new CollModCommand(data.collection, {
919
+ validator: data.validator,
920
+ validationLevel: data.validationLevel,
921
+ validationAction: data.validationAction,
922
+ changeStreamPreAndPostImages: data.changeStreamPreAndPostImages
923
+ });
924
+ }
925
+ default: throw new Error(`Unknown DDL command kind: ${kind}`);
926
+ }
983
927
  }
984
- function buildDescribeMethod(meta) {
985
- const lines = [];
986
- lines.push(" override describe() {");
987
- lines.push(" return {");
988
- lines.push(` from: ${JSON.stringify(meta.from)},`);
989
- lines.push(` to: ${JSON.stringify(meta.to)},`);
990
- if (meta.labels && meta.labels.length > 0) lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
991
- lines.push(" };");
992
- lines.push(" }");
993
- lines.push("");
994
- return lines.join("\n");
928
+ function deserializeInspectionCommand(json) {
929
+ const kind = json["kind"];
930
+ switch (kind) {
931
+ case "listIndexes": return new ListIndexesCommand(validate(ListIndexesJson, json, "listIndexes command").collection);
932
+ case "listCollections":
933
+ validate(ListCollectionsJson, json, "listCollections command");
934
+ return new ListCollectionsCommand();
935
+ default: throw new Error(`Unknown inspection command kind: ${kind}`);
936
+ }
995
937
  }
996
- function indent(text, spaces) {
997
- const pad = " ".repeat(spaces);
998
- return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : line).join("\n");
938
+ function deserializeCheck(json) {
939
+ const data = validate(CheckJson, json, "migration check");
940
+ return {
941
+ description: data.description,
942
+ source: deserializeInspectionCommand(data.source),
943
+ filter: deserializeFilterExpr(data.filter),
944
+ expect: data.expect
945
+ };
999
946
  }
1000
- //#endregion
1001
- //#region src/core/planner-produced-migration.ts
1002
- /**
1003
- * Planner-produced Mongo migration, returned by `MongoMigrationPlanner.plan(...)`
1004
- * and `MongoMigrationPlanner.emptyMigration(...)`.
1005
- *
1006
- * Unlike user-authored migrations (which extend `MongoMigration` from
1007
- * `@prisma-next/family-mongo/migration`), this class lives inside the target
1008
- * and holds the richer authoring IR (`OpFactoryCall[]`) needed to render
1009
- * itself back to TypeScript source. It implements
1010
- * `MigrationPlanWithAuthoringSurface` so that the CLI can uniformly ask any
1011
- * planner result to serialize itself to a `migration.ts`.
1012
- *
1013
- * Extends the framework `Migration` base class directly (not
1014
- * `MongoMigration`) because `MongoMigration` lives in `@prisma-next/family-mongo`,
1015
- * which depends on this package — extending it here would create a dependency
1016
- * cycle.
1017
- */
1018
- var PlannerProducedMongoMigration = class extends Migration {
1019
- calls;
1020
- meta;
1021
- targetId = "mongo";
1022
- constructor(calls, meta) {
1023
- super();
1024
- this.calls = calls;
1025
- this.meta = meta;
1026
- }
1027
- get operations() {
1028
- return renderOps(this.calls);
1029
- }
1030
- describe() {
1031
- return this.meta;
1032
- }
1033
- renderTypeScript() {
1034
- return renderCallsToTypeScript(this.calls, {
1035
- from: this.meta.from,
1036
- to: this.meta.to,
1037
- ...ifDefined("labels", this.meta.labels)
1038
- });
1039
- }
1040
- };
1041
- //#endregion
1042
- //#region src/core/mongo-planner.ts
1043
- function buildIndexLookupKey(index) {
1044
- const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
1045
- const opts = [
1046
- index.unique ? "unique" : "",
1047
- index.sparse ? "sparse" : "",
1048
- index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : "",
1049
- index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : "",
1050
- index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : "",
1051
- index.collation ? `col:${canonicalize(index.collation)}` : "",
1052
- index.weights ? `wt:${canonicalize(index.weights)}` : "",
1053
- index.default_language ? `dl:${index.default_language}` : "",
1054
- index.language_override ? `lo:${index.language_override}` : ""
1055
- ].filter(Boolean).join(";");
1056
- return opts ? `${keys}|${opts}` : keys;
947
+ function deserializeStep(json) {
948
+ const data = validate(StepJson, json, "migration step");
949
+ return {
950
+ description: data.description,
951
+ command: deserializeDdlCommand(data.command)
952
+ };
1057
953
  }
1058
- function validatorsEqual(a, b) {
1059
- if (!a && !b) return true;
1060
- if (!a || !b) return false;
1061
- return a.validationLevel === b.validationLevel && a.validationAction === b.validationAction && canonicalize(a.jsonSchema) === canonicalize(b.jsonSchema);
954
+ function isDataTransformJson(json) {
955
+ return typeof json === "object" && json !== null && json["operationClass"] === "data";
1062
956
  }
1063
- function classifyValidatorUpdate(origin, dest) {
1064
- let hasDestructive = false;
1065
- if (canonicalize(origin.jsonSchema) !== canonicalize(dest.jsonSchema)) hasDestructive = true;
1066
- if (origin.validationAction !== dest.validationAction) {
1067
- if (dest.validationAction === "error") hasDestructive = true;
1068
- }
1069
- if (origin.validationLevel !== dest.validationLevel) {
1070
- if (dest.validationLevel === "strict") hasDestructive = true;
1071
- }
1072
- return hasDestructive ? "destructive" : "widening";
957
+ function deserializeDdlOp(json) {
958
+ const data = validate(DdlOperationJson, json, "migration operation");
959
+ return {
960
+ id: data.id,
961
+ label: data.label,
962
+ operationClass: data.operationClass,
963
+ precheck: data.precheck.map(deserializeCheck),
964
+ execute: data.execute.map(deserializeStep),
965
+ postcheck: data.postcheck.map(deserializeCheck)
966
+ };
1073
967
  }
1074
- function hasImmutableOptionChange(origin, dest) {
1075
- if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
1076
- if (canonicalize(origin?.timeseries) !== canonicalize(dest?.timeseries)) return "timeseries";
1077
- if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
1078
- if (canonicalize(origin?.clusteredIndex) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
968
+ function deserializeDataTransformCheck(json) {
969
+ const data = validate(DataTransformCheckJson, json, "data transform check");
970
+ return {
971
+ description: data.description,
972
+ source: deserializeMongoQueryPlan(data.source),
973
+ filter: deserializeFilterExpr(data.filter),
974
+ expect: data.expect
975
+ };
1079
976
  }
1080
- function collectionHasOptions(coll) {
1081
- return !!(coll.options || coll.validator);
977
+ function deserializeDataTransformOp(json) {
978
+ const data = validate(DataTransformOperationJson, json, "data transform operation");
979
+ return {
980
+ id: data.id,
981
+ label: data.label,
982
+ operationClass: "data",
983
+ name: data.name,
984
+ precheck: data.precheck.map(deserializeDataTransformCheck),
985
+ run: data.run.map(deserializeMongoQueryPlan),
986
+ postcheck: data.postcheck.map(deserializeDataTransformCheck)
987
+ };
1082
988
  }
1083
- var MongoMigrationPlanner = class {
1084
- planCalls(options) {
1085
- const contract = options.contract;
1086
- const originIR = options.schema;
1087
- const destinationIR = contractToMongoSchemaIR(contract);
1088
- const collCreates = [];
1089
- const drops = [];
1090
- const creates = [];
1091
- const validatorOps = [];
1092
- const mutableOptionOps = [];
1093
- const collDrops = [];
1094
- const conflicts = [];
1095
- const allCollectionNames = new Set([...originIR.collectionNames, ...destinationIR.collectionNames]);
1096
- for (const collName of [...allCollectionNames].sort()) {
1097
- const originColl = originIR.collection(collName);
1098
- const destColl = destinationIR.collection(collName);
1099
- if (!originColl) {
1100
- if (destColl && (collectionHasOptions(destColl) || destColl.indexes.length === 0)) {
1101
- const opts = collectionHasOptions(destColl) ? schemaCollectionToCreateCollectionOptions(destColl) : void 0;
1102
- collCreates.push(new CreateCollectionCall(collName, opts));
1103
- }
1104
- } else if (!destColl) collDrops.push(new DropCollectionCall(collName));
1105
- else {
1106
- const immutableChange = hasImmutableOptionChange(originColl.options, destColl.options);
1107
- if (immutableChange) conflicts.push({
1108
- kind: "policy-violation",
1109
- summary: `Cannot change immutable collection option '${immutableChange}' on ${collName}`,
1110
- why: `MongoDB does not support modifying the '${immutableChange}' option after collection creation`
1111
- });
1112
- const mutableCall = planMutableOptionsDiffCall(collName, originColl.options, destColl.options);
1113
- if (mutableCall) mutableOptionOps.push(mutableCall);
1114
- const validatorCall = planValidatorDiffCall(collName, originColl.validator, destColl.validator);
1115
- if (validatorCall) validatorOps.push(validatorCall);
1116
- }
1117
- const originLookup = /* @__PURE__ */ new Map();
1118
- if (originColl) for (const idx of originColl.indexes) originLookup.set(buildIndexLookupKey(idx), idx);
1119
- const destLookup = /* @__PURE__ */ new Map();
1120
- if (destColl) for (const idx of destColl.indexes) destLookup.set(buildIndexLookupKey(idx), idx);
1121
- for (const [lookupKey, idx] of originLookup) if (!destLookup.has(lookupKey)) drops.push(new DropIndexCall(collName, idx.keys));
1122
- for (const [lookupKey, idx] of destLookup) if (!originLookup.has(lookupKey)) creates.push(new CreateIndexCall(collName, idx.keys, schemaIndexToCreateIndexOptions(idx)));
1123
- }
1124
- if (conflicts.length > 0) return {
1125
- kind: "failure",
1126
- conflicts
1127
- };
1128
- const allCalls = [
1129
- ...collCreates,
1130
- ...drops,
1131
- ...creates,
1132
- ...validatorOps,
1133
- ...mutableOptionOps,
1134
- ...collDrops
1135
- ];
1136
- for (const call of allCalls) if (!options.policy.allowedOperationClasses.includes(call.operationClass)) conflicts.push({
1137
- kind: "policy-violation",
1138
- summary: `${call.operationClass} operation disallowed: ${call.label}`,
1139
- why: `Policy does not allow '${call.operationClass}' operations`
1140
- });
1141
- if (conflicts.length > 0) return {
1142
- kind: "failure",
1143
- conflicts
1144
- };
1145
- return {
1146
- kind: "success",
1147
- calls: allCalls
1148
- };
1149
- }
1150
- plan(options) {
1151
- const contract = options.contract;
1152
- const result = this.planCalls(options);
1153
- if (result.kind === "failure") return result;
1154
- return {
1155
- kind: "success",
1156
- plan: new PlannerProducedMongoMigration(result.calls, {
1157
- from: options.fromContract?.storage.storageHash ?? null,
1158
- to: contract.storage.storageHash
1159
- })
1160
- };
1161
- }
1162
- /**
1163
- * Produce an empty `migration.ts` authoring surface for `migration new`.
1164
- *
1165
- * The "empty migration" is a `PlannerProducedMongoMigration` with no
1166
- * operations; `renderTypeScript()` emits a stub class with the correct
1167
- * `from`/`to` metadata that the user then fills in with operations. The
1168
- * contract path on the context is unused — Mongo's emitted source does
1169
- * not import from the generated contract `.d.ts`.
1170
- */
1171
- emptyMigration(context) {
1172
- return new PlannerProducedMongoMigration([], {
1173
- from: context.fromHash,
1174
- to: context.toHash
1175
- });
1176
- }
1177
- };
1178
- function planValidatorDiffCall(collName, originValidator, destValidator) {
1179
- if (validatorsEqual(originValidator, destValidator)) return void 0;
1180
- if (destValidator) {
1181
- const operationClass = originValidator ? classifyValidatorUpdate(originValidator, destValidator) : "destructive";
1182
- return new CollModCall(collName, {
1183
- validator: { $jsonSchema: destValidator.jsonSchema },
1184
- validationLevel: destValidator.validationLevel,
1185
- validationAction: destValidator.validationAction
1186
- }, {
1187
- id: `validator.${collName}.${originValidator ? "update" : "add"}`,
1188
- label: `${originValidator ? "Update" : "Add"} validator on ${collName}`,
1189
- operationClass
1190
- });
1191
- }
1192
- return new CollModCall(collName, {
1193
- validator: {},
1194
- validationLevel: "strict",
1195
- validationAction: "error"
1196
- }, {
1197
- id: `validator.${collName}.remove`,
1198
- label: `Remove validator on ${collName}`,
1199
- operationClass: "widening"
1200
- });
989
+ function deserializeMongoOp(json) {
990
+ if (isDataTransformJson(json)) return deserializeDataTransformOp(json);
991
+ return deserializeDdlOp(json);
1201
992
  }
1202
- function planMutableOptionsDiffCall(collName, origin, dest) {
1203
- const originCSPPI = origin?.changeStreamPreAndPostImages;
1204
- const destCSPPI = dest?.changeStreamPreAndPostImages;
1205
- if (deepEqual(originCSPPI, destCSPPI)) return void 0;
1206
- const desiredCSPPI = destCSPPI ?? { enabled: false };
1207
- return new CollModCall(collName, { changeStreamPreAndPostImages: desiredCSPPI }, {
1208
- id: `options.${collName}.update`,
1209
- label: `Update mutable options on ${collName}`,
1210
- operationClass: desiredCSPPI.enabled ? "widening" : "destructive"
1211
- });
993
+ function deserializeMongoOps(json) {
994
+ return json.map(deserializeMongoOp);
995
+ }
996
+ function serializeMongoOps(ops) {
997
+ return JSON.stringify(ops, null, 2);
1212
998
  }
1213
999
  //#endregion
1214
1000
  //#region src/core/mongo-runner.ts
@@ -1394,6 +1180,276 @@ var MongoMigrationRunner = class {
1394
1180
  }
1395
1181
  };
1396
1182
  //#endregion
1397
- export { CollModCall, CreateCollectionCall, CreateIndexCall, DropCollectionCall, DropIndexCall, FilterEvaluator, MongoMigrationPlanner, MongoMigrationRunner, PlannerProducedMongoMigration, contractToMongoSchemaIR, deserializeMongoOp, deserializeMongoOps, formatMongoOperations, initMarker, readAllMarkers, readMarker, renderCallsToTypeScript, renderOps, schemaCollectionToCreateCollectionOptions, schemaIndexToCreateIndexOptions, serializeMongoOps, updateMarker, writeLedgerEntry };
1183
+ //#region src/core/mongo-target-database.ts
1184
+ /**
1185
+ * Mongo target `Namespace` concretion. In Mongo the "namespace" concept
1186
+ * binds to the connection's `db` field — a `MongoTargetDatabase` instance
1187
+ * names the database the collections live under.
1188
+ *
1189
+ * Qualifier emission is the rendering seam: query / DDL emission asks the
1190
+ * namespace for its qualifier (e.g. `"<db>.<collection>"`) and consumes
1191
+ * the result polymorphically. The unspecified singleton overrides these
1192
+ * methods to elide the prefix entirely — call sites stay polymorphic and
1193
+ * never branch on `id === UNSPECIFIED_NAMESPACE_ID`.
1194
+ *
1195
+ * **Freeze-trap warning.** The constructor calls `freezeNode(this)` at
1196
+ * the end. Direct subclasses MUST NOT add instance fields — the freeze
1197
+ * runs in this base constructor and any subclass field assignment will
1198
+ * silently fail in non-strict mode or throw in strict mode. The
1199
+ * `MongoTargetUnspecifiedDatabase` singleton below is intentionally
1200
+ * field-free for this reason; if a future subclass needs to carry
1201
+ * additional fields, lift this `freezeNode` to the leaf-class
1202
+ * constructors (or to a `seal()` hook each leaf calls explicitly).
1203
+ */
1204
+ var MongoTargetDatabase = class extends NamespaceBase {
1205
+ kind = "database";
1206
+ id;
1207
+ constructor(id) {
1208
+ super();
1209
+ this.id = id;
1210
+ freezeNode(this);
1211
+ }
1212
+ /**
1213
+ * The bare qualifier as it would appear in a rendered string. The
1214
+ * unspecified-database singleton overrides this to return `''`.
1215
+ */
1216
+ qualifier() {
1217
+ return this.id;
1218
+ }
1219
+ /**
1220
+ * Qualify a collection name with the database prefix. The
1221
+ * unspecified-database singleton overrides this to emit just the
1222
+ * collection name. Used by emission/introspection paths that need a
1223
+ * fully-qualified reference.
1224
+ */
1225
+ qualifyCollection(collectionName) {
1226
+ return `${this.id}.${collectionName}`;
1227
+ }
1228
+ };
1229
+ /**
1230
+ * Singleton subclass for the reserved sentinel namespace id
1231
+ * (`UNSPECIFIED_NAMESPACE_ID`). Overrides qualifier emission to elide
1232
+ * the database prefix — call sites that consume `qualifier()` /
1233
+ * `qualifyCollection()` get unqualified output without branching on the
1234
+ * namespace id.
1235
+ *
1236
+ * This is the target-side materialization of "the framework provides
1237
+ * affordances; targets implement specifics": the framework names the
1238
+ * sentinel; Mongo decides what no-database-bound means here (the
1239
+ * collection name, naked).
1240
+ */
1241
+ var MongoTargetUnspecifiedDatabase = class MongoTargetUnspecifiedDatabase extends MongoTargetDatabase {
1242
+ static instance = new MongoTargetUnspecifiedDatabase();
1243
+ constructor() {
1244
+ super(UNSPECIFIED_NAMESPACE_ID);
1245
+ }
1246
+ qualifier() {
1247
+ return "";
1248
+ }
1249
+ qualifyCollection(collectionName) {
1250
+ return collectionName;
1251
+ }
1252
+ };
1253
+ //#endregion
1254
+ //#region src/core/mongo-target-contract-serializer.ts
1255
+ /**
1256
+ * Mongo target `ContractSerializer` concretion. Plugs into the
1257
+ * family-shared deserialization pipeline at `constructTargetContract`,
1258
+ * wrapping the validated flat-data shape in a `MongoStorage` class
1259
+ * instance and providing the target's default namespace map.
1260
+ *
1261
+ * Default namespaces is
1262
+ * `{ [UNSPECIFIED_NAMESPACE_ID]: MongoTargetUnspecifiedDatabase.instance }`
1263
+ * — supplied at this target-layer call site because the family-layer
1264
+ * `MongoStorage` class is target-agnostic (it cannot import the
1265
+ * Mongo-target's namespace concretion). Contracts authored before
1266
+ * multi-namespace support bind to the unspecified singleton without the
1267
+ * call site declaring anything.
1268
+ *
1269
+ * `validated.storage.collections` already carries `MongoCollection` IR
1270
+ * class instances by the time this method runs — the family-base
1271
+ * `hydrateMongoContract` walks the arktype-validated tree and
1272
+ * constructs class instances before validation. The target serializer
1273
+ * just wraps the envelope.
1274
+ */
1275
+ var MongoTargetContractSerializer = class extends MongoContractSerializerBase {
1276
+ constructTargetContract(validated) {
1277
+ const { storage, ...rest } = validated;
1278
+ const targetStorage = new MongoStorage({
1279
+ storageHash: storage.storageHash,
1280
+ collections: storage.collections,
1281
+ namespaces: { [UNSPECIFIED_NAMESPACE_ID]: MongoTargetUnspecifiedDatabase.instance }
1282
+ });
1283
+ return {
1284
+ ...rest,
1285
+ storage: targetStorage
1286
+ };
1287
+ }
1288
+ /**
1289
+ * Produce the canonical on-disk JSON shape from an in-memory Mongo
1290
+ * contract. Strips runtime-only fields the storage class carries
1291
+ * for its live-instance API but that don't belong in the persisted
1292
+ * envelope: `MongoStorage.namespaces` is a Namespace-class map the
1293
+ * verifier and runtime walk; the persisted shape omits it (today's
1294
+ * contracts have a single implicit unspecified namespace; future
1295
+ * explicit per-collection assignment will surface in JSON via a
1296
+ * different field).
1297
+ *
1298
+ * Constructing the JsonObject here — rather than relying on
1299
+ * non-enumerable property tricks at the storage class — keeps the
1300
+ * "what's on disk" decision in the SPI implementer, where it
1301
+ * belongs.
1302
+ */
1303
+ serializeContract(contract) {
1304
+ const { storage, ...rest } = contract;
1305
+ return {
1306
+ ...rest,
1307
+ storage: {
1308
+ storageHash: storage.storageHash,
1309
+ collections: storage.collections
1310
+ }
1311
+ };
1312
+ }
1313
+ };
1314
+ //#endregion
1315
+ //#region src/core/mongo-target-schema-verifier.ts
1316
+ /**
1317
+ * Mongo target `SchemaVerifier` concretion. Extends the family base's
1318
+ * namespace-walk scaffolding and contributes the per-namespace diff via
1319
+ * `verifyNamespace`; the diff body reuses the existing target-side
1320
+ * helpers (`contractToMongoSchemaIR`, `canonicalizeSchemasForVerification`,
1321
+ * `diffMongoSchemas`) so production verification behaviour is unchanged.
1322
+ *
1323
+ * Today's invariant: every Mongo contract carries exactly one
1324
+ * namespace (the unspecified singleton, materialised as
1325
+ * `MongoTargetUnspecifiedDatabase`), so the family-base namespace walk
1326
+ * dispatches exactly once and the per-namespace body runs the existing
1327
+ * whole-schema diff. Future per-collection namespace assignment will
1328
+ * have this hook project the diff to the namespace's owned collections.
1329
+ *
1330
+ * `verifyTargetExtensions` returns the empty list — Mongo has no
1331
+ * target-only kinds today.
1332
+ *
1333
+ * Strict diff mode is `false` for SPI-routed calls; production
1334
+ * verification today still goes through `verifyMongoSchema` which
1335
+ * receives strict from the CLI.
1336
+ */
1337
+ var MongoTargetSchemaVerifier = class extends MongoSchemaVerifierBase {
1338
+ verifyNamespace(options) {
1339
+ const expectedIR = contractToMongoSchemaIR(options.contract);
1340
+ const { live, expected } = canonicalizeSchemasForVerification(options.schema, expectedIR);
1341
+ const { issues } = diffMongoSchemas(live, expected, false);
1342
+ return issues;
1343
+ }
1344
+ verifyTargetExtensions(_options) {
1345
+ return [];
1346
+ }
1347
+ };
1348
+ //#endregion
1349
+ //#region src/core/control-target.ts
1350
+ /**
1351
+ * `migration.ts` default-exports a `Migration` subclass whose `operations`
1352
+ * getter returns the ordered list of operations and whose `describe()`
1353
+ * returns the manifest identity metadata. `MongoMigrationPlanner.plan()`
1354
+ * returns a `MigrationPlanWithAuthoringSurface` that knows how to render
1355
+ * itself back to such a file; `MongoMigrationPlanner.emptyMigration()`
1356
+ * returns the same shape for `migration new`. Users run the scaffolded
1357
+ * `migration.ts` directly (via `node migration.ts`) to self-emit
1358
+ * `ops.json` and attest the `migrationHash`.
1359
+ */
1360
+ const mongoTargetDescriptor = {
1361
+ ...mongoTargetDescriptorMeta,
1362
+ contractSerializer: new MongoTargetContractSerializer(),
1363
+ schemaVerifier: new MongoTargetSchemaVerifier(),
1364
+ migrations: {
1365
+ createPlanner(_family) {
1366
+ return new MongoMigrationPlanner();
1367
+ },
1368
+ createRunner(family) {
1369
+ let cachedDeps;
1370
+ const runMongo = async (driver, runnerOptions) => {
1371
+ cachedDeps ??= createMongoRunnerDeps(driver, MongoDriverImpl.fromDb(extractDb(driver)), family);
1372
+ return new MongoMigrationRunner(cachedDeps).execute({
1373
+ ...runnerOptions,
1374
+ destinationContract: runnerOptions.destinationContract
1375
+ });
1376
+ };
1377
+ return {
1378
+ async execute(options) {
1379
+ const { driver, ...runnerOptions } = options;
1380
+ return runMongo(driver, runnerOptions);
1381
+ },
1382
+ async executeAcrossSpaces({ driver, perSpaceOptions }) {
1383
+ const members = perSpaceOptions.map(toSpaceMember);
1384
+ const perSpaceResults = [];
1385
+ for (let i = 0; i < perSpaceOptions.length; i++) {
1386
+ const spaceOptions = perSpaceOptions[i];
1387
+ if (!spaceOptions) continue;
1388
+ const member = members[i];
1389
+ if (!member) continue;
1390
+ const others = members.filter((_, j) => j !== i);
1391
+ const projectSchema = (schema) => {
1392
+ return new MongoSchemaIR(projectSchemaToSpace(schema, member, others).collections);
1393
+ };
1394
+ const result = await runMongo(driver, {
1395
+ ...spaceOptions,
1396
+ projectSchema
1397
+ });
1398
+ if (!result.ok) return notOk({
1399
+ ...result.failure,
1400
+ failingSpace: spaceOptions.space
1401
+ });
1402
+ perSpaceResults.push({
1403
+ space: spaceOptions.space,
1404
+ value: result.value
1405
+ });
1406
+ }
1407
+ return ok({ perSpaceResults });
1408
+ }
1409
+ };
1410
+ },
1411
+ contractToSchema(contract) {
1412
+ return contractToMongoSchemaIR(contract);
1413
+ }
1414
+ },
1415
+ create() {
1416
+ return {
1417
+ familyId: "mongo",
1418
+ targetId: "mongo"
1419
+ };
1420
+ }
1421
+ };
1422
+ /**
1423
+ * Synthesise the minimum {@link projectSchemaToSpace}-compatible
1424
+ * `ContractSpaceMember` shape from a per-space option entry. The
1425
+ * projector only reads `spaceId` and `contract.storage`; the rest of
1426
+ * `ContractSpaceMember` (head ref invariants, hydrated migration
1427
+ * graph) is irrelevant at runner time and stubbed with sentinels.
1428
+ *
1429
+ * The `as unknown as ContractSpaceMember` cast is the load-bearing bit
1430
+ * — the projector duck-types its members so a sentinel-shaped graph
1431
+ * never gets read, but the framework type carries a richer shape.
1432
+ */
1433
+ function toSpaceMember(opts) {
1434
+ return {
1435
+ spaceId: opts.space,
1436
+ contract: opts.destinationContract,
1437
+ headRef: {
1438
+ hash: "",
1439
+ invariants: []
1440
+ },
1441
+ migrations: {
1442
+ graph: {
1443
+ nodes: /* @__PURE__ */ new Set(),
1444
+ forwardChain: /* @__PURE__ */ new Map(),
1445
+ reverseChain: /* @__PURE__ */ new Map(),
1446
+ migrationByHash: /* @__PURE__ */ new Map()
1447
+ },
1448
+ packagesByMigrationHash: /* @__PURE__ */ new Map()
1449
+ }
1450
+ };
1451
+ }
1452
+ //#endregion
1453
+ export { CollModCall, CreateCollectionCall, CreateIndexCall, DropCollectionCall, DropIndexCall, FilterEvaluator, MongoMigrationPlanner, MongoMigrationRunner, MongoTargetContractSerializer, MongoTargetDatabase, MongoTargetSchemaVerifier, MongoTargetUnspecifiedDatabase, PlannerProducedMongoMigration, deserializeMongoOp, deserializeMongoOps, mongoTargetDescriptor, renderCallsToTypeScript, renderOps, schemaCollectionToCreateCollectionOptions, schemaIndexToCreateIndexOptions, serializeMongoOps };
1398
1454
 
1399
1455
  //# sourceMappingURL=control.mjs.map