@prisma-next/target-mongo 0.4.0-dev.1 → 0.4.0-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/control.mjs CHANGED
@@ -1,5 +1,173 @@
1
+ import { a as dropIndex, i as dropCollection, n as createCollection, r as createIndex, t as collMod } from "./migration-factories-Brzz-QGG.mjs";
2
+ import { MongoSchemaCollection, MongoSchemaCollectionOptions, MongoSchemaIR, MongoSchemaIndex, MongoSchemaValidator, canonicalize, deepEqual } from "@prisma-next/mongo-schema-ir";
1
3
  import { RawAggregateCommand, RawFindOneAndUpdateCommand, RawInsertOneCommand } from "@prisma-next/mongo-query-ast/execution";
4
+ import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr } from "@prisma-next/mongo-query-ast/control";
5
+ import { type } from "arktype";
6
+ import { notOk, ok } from "@prisma-next/utils/result";
2
7
 
8
+ //#region src/core/contract-to-schema.ts
9
+ function convertIndex(index) {
10
+ return new MongoSchemaIndex({
11
+ keys: index.keys,
12
+ unique: index.unique,
13
+ sparse: index.sparse,
14
+ expireAfterSeconds: index.expireAfterSeconds,
15
+ partialFilterExpression: index.partialFilterExpression,
16
+ wildcardProjection: index.wildcardProjection,
17
+ collation: index.collation,
18
+ weights: index.weights,
19
+ default_language: index.default_language,
20
+ language_override: index.language_override
21
+ });
22
+ }
23
+ function convertValidator(v) {
24
+ return new MongoSchemaValidator({
25
+ jsonSchema: v.jsonSchema,
26
+ validationLevel: v.validationLevel,
27
+ validationAction: v.validationAction
28
+ });
29
+ }
30
+ function convertOptions(o) {
31
+ return new MongoSchemaCollectionOptions(o);
32
+ }
33
+ function convertCollection(name, def) {
34
+ return new MongoSchemaCollection({
35
+ name,
36
+ indexes: (def.indexes ?? []).map(convertIndex),
37
+ ...def.validator != null && { validator: convertValidator(def.validator) },
38
+ ...def.options != null && { options: convertOptions(def.options) }
39
+ });
40
+ }
41
+ function contractToMongoSchemaIR(contract) {
42
+ if (!contract) return new MongoSchemaIR([]);
43
+ return new MongoSchemaIR(Object.entries(contract.storage.collections).map(([name, def]) => convertCollection(name, def)));
44
+ }
45
+
46
+ //#endregion
47
+ //#region src/core/ddl-formatter.ts
48
+ function formatKeySpec(keys) {
49
+ return `{ ${keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`).join(", ")} }`;
50
+ }
51
+ function formatOptions(cmd) {
52
+ const parts = [];
53
+ if (cmd.unique) parts.push("unique: true");
54
+ if (cmd.sparse) parts.push("sparse: true");
55
+ if (cmd.expireAfterSeconds !== void 0) parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
56
+ if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
57
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
58
+ if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
59
+ if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
60
+ if (cmd.language_override) parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
61
+ if (cmd.wildcardProjection) parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
62
+ if (cmd.partialFilterExpression) parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
63
+ if (parts.length === 0) return void 0;
64
+ return `{ ${parts.join(", ")} }`;
65
+ }
66
+ function formatCreateCollectionOptions(cmd) {
67
+ const parts = [];
68
+ if (cmd.capped) parts.push("capped: true");
69
+ if (cmd.size !== void 0) parts.push(`size: ${cmd.size}`);
70
+ if (cmd.max !== void 0) parts.push(`max: ${cmd.max}`);
71
+ if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
72
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
73
+ if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
74
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
75
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
76
+ if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
77
+ if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
78
+ if (parts.length === 0) return void 0;
79
+ return `{ ${parts.join(", ")} }`;
80
+ }
81
+ var MongoDdlCommandFormatter = class {
82
+ createIndex(cmd) {
83
+ const keySpec = formatKeySpec(cmd.keys);
84
+ const opts = formatOptions(cmd);
85
+ return opts ? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})` : `db.${cmd.collection}.createIndex(${keySpec})`;
86
+ }
87
+ dropIndex(cmd) {
88
+ return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
89
+ }
90
+ createCollection(cmd) {
91
+ const opts = formatCreateCollectionOptions(cmd);
92
+ return opts ? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})` : `db.createCollection(${JSON.stringify(cmd.collection)})`;
93
+ }
94
+ dropCollection(cmd) {
95
+ return `db.${cmd.collection}.drop()`;
96
+ }
97
+ collMod(cmd) {
98
+ const parts = [`collMod: ${JSON.stringify(cmd.collection)}`];
99
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
100
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
101
+ if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
102
+ if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
103
+ return `db.runCommand({ ${parts.join(", ")} })`;
104
+ }
105
+ };
106
+ const formatter = new MongoDdlCommandFormatter();
107
+ function formatMongoOperations(operations) {
108
+ const statements = [];
109
+ for (const operation of operations) {
110
+ const candidate = operation;
111
+ if (!("execute" in candidate) || !Array.isArray(candidate["execute"])) continue;
112
+ for (const step of candidate["execute"]) if (step.command && typeof step.command.accept === "function") statements.push(step.command.accept(formatter));
113
+ }
114
+ return statements;
115
+ }
116
+
117
+ //#endregion
118
+ //#region src/core/filter-evaluator.ts
119
+ function getNestedField(doc, path) {
120
+ const parts = path.split(".");
121
+ let current = doc;
122
+ for (const part of parts) {
123
+ if (current === null || current === void 0 || typeof current !== "object") return;
124
+ const record = current;
125
+ if (!Object.hasOwn(record, part)) return;
126
+ current = record[part];
127
+ }
128
+ return current;
129
+ }
130
+ function evaluateFieldOp(op, actual, expected) {
131
+ switch (op) {
132
+ case "$eq": return deepEqual(actual, expected);
133
+ case "$ne": return !deepEqual(actual, expected);
134
+ case "$gt": return typeof actual === typeof expected && actual > expected;
135
+ case "$gte": return typeof actual === typeof expected && actual >= expected;
136
+ case "$lt": return typeof actual === typeof expected && actual < expected;
137
+ case "$lte": return typeof actual === typeof expected && actual <= expected;
138
+ case "$in": return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
139
+ default: throw new Error(`Unsupported filter operator in migration check: ${op}`);
140
+ }
141
+ }
142
+ var FilterEvaluator = class {
143
+ doc = {};
144
+ evaluate(filter, doc) {
145
+ this.doc = doc;
146
+ return filter.accept(this);
147
+ }
148
+ field(expr) {
149
+ const value = getNestedField(this.doc, expr.field);
150
+ return evaluateFieldOp(expr.op, value, expr.value);
151
+ }
152
+ and(expr) {
153
+ return expr.exprs.every((child) => child.accept(this));
154
+ }
155
+ or(expr) {
156
+ return expr.exprs.some((child) => child.accept(this));
157
+ }
158
+ not(expr) {
159
+ return !expr.expr.accept(this);
160
+ }
161
+ exists(expr) {
162
+ const has = getNestedField(this.doc, expr.field) !== void 0;
163
+ return expr.exists ? has : !has;
164
+ }
165
+ expr(_expr) {
166
+ throw new Error("Aggregation expression filters are not supported in migration checks");
167
+ }
168
+ };
169
+
170
+ //#endregion
3
171
  //#region src/core/marker-ledger.ts
4
172
  const COLLECTION = "_prisma_migrations";
5
173
  const MARKER_ID = "marker";
@@ -58,5 +226,741 @@ async function writeLedgerEntry(db, entry) {
58
226
  }
59
227
 
60
228
  //#endregion
61
- export { initMarker, readMarker, updateMarker, writeLedgerEntry };
229
+ //#region src/core/mongo-ops-serializer.ts
230
+ const CreateIndexJson = type({
231
+ kind: "\"createIndex\"",
232
+ collection: "string",
233
+ keys: type({
234
+ field: "string",
235
+ direction: type("1 | -1 | \"text\" | \"2dsphere\" | \"2d\" | \"hashed\"")
236
+ }).array().atLeastLength(1),
237
+ "unique?": "boolean",
238
+ "sparse?": "boolean",
239
+ "expireAfterSeconds?": "number",
240
+ "partialFilterExpression?": "Record<string, unknown>",
241
+ "name?": "string",
242
+ "wildcardProjection?": "Record<string, unknown>",
243
+ "collation?": "Record<string, unknown>",
244
+ "weights?": "Record<string, unknown>",
245
+ "default_language?": "string",
246
+ "language_override?": "string"
247
+ });
248
+ const DropIndexJson = type({
249
+ kind: "\"dropIndex\"",
250
+ collection: "string",
251
+ name: "string"
252
+ });
253
+ const CreateCollectionJson = type({
254
+ kind: "\"createCollection\"",
255
+ collection: "string",
256
+ "validator?": "Record<string, unknown>",
257
+ "validationLevel?": "\"strict\" | \"moderate\"",
258
+ "validationAction?": "\"error\" | \"warn\"",
259
+ "capped?": "boolean",
260
+ "size?": "number",
261
+ "max?": "number",
262
+ "timeseries?": "Record<string, unknown>",
263
+ "collation?": "Record<string, unknown>",
264
+ "changeStreamPreAndPostImages?": "Record<string, unknown>",
265
+ "clusteredIndex?": "Record<string, unknown>"
266
+ });
267
+ const DropCollectionJson = type({
268
+ kind: "\"dropCollection\"",
269
+ collection: "string"
270
+ });
271
+ const CollModJson = type({
272
+ kind: "\"collMod\"",
273
+ collection: "string",
274
+ "validator?": "Record<string, unknown>",
275
+ "validationLevel?": "\"strict\" | \"moderate\"",
276
+ "validationAction?": "\"error\" | \"warn\"",
277
+ "changeStreamPreAndPostImages?": "Record<string, unknown>"
278
+ });
279
+ const ListIndexesJson = type({
280
+ kind: "\"listIndexes\"",
281
+ collection: "string"
282
+ });
283
+ const ListCollectionsJson = type({ kind: "\"listCollections\"" });
284
+ const FieldFilterJson = type({
285
+ kind: "\"field\"",
286
+ field: "string",
287
+ op: "string",
288
+ value: "unknown"
289
+ });
290
+ const ExistsFilterJson = type({
291
+ kind: "\"exists\"",
292
+ field: "string",
293
+ exists: "boolean"
294
+ });
295
+ const CheckJson = type({
296
+ description: "string",
297
+ source: "Record<string, unknown>",
298
+ filter: "Record<string, unknown>",
299
+ expect: "\"exists\" | \"notExists\""
300
+ });
301
+ const StepJson = type({
302
+ description: "string",
303
+ command: "Record<string, unknown>"
304
+ });
305
+ const OperationJson = type({
306
+ id: "string",
307
+ label: "string",
308
+ operationClass: "\"additive\" | \"widening\" | \"destructive\"",
309
+ precheck: "Record<string, unknown>[]",
310
+ execute: "Record<string, unknown>[]",
311
+ postcheck: "Record<string, unknown>[]"
312
+ });
313
+ function validate(schema, data, context) {
314
+ try {
315
+ return schema.assert(data);
316
+ } catch (error) {
317
+ /* v8 ignore start -- assertion libraries always throw Error instances */
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ /* v8 ignore stop */
320
+ throw new Error(`Invalid ${context}: ${message}`);
321
+ }
322
+ }
323
+ function deserializeFilterExpr(json) {
324
+ const record = json;
325
+ const kind = record["kind"];
326
+ switch (kind) {
327
+ case "field": {
328
+ const data = validate(FieldFilterJson, json, "field filter");
329
+ return MongoFieldFilter.of(data.field, data.op, data.value);
330
+ }
331
+ case "and": {
332
+ const exprs = record["exprs"];
333
+ if (!Array.isArray(exprs)) throw new Error("Invalid and filter: missing exprs array");
334
+ return MongoAndExpr.of(exprs.map(deserializeFilterExpr));
335
+ }
336
+ case "or": {
337
+ const exprs = record["exprs"];
338
+ if (!Array.isArray(exprs)) throw new Error("Invalid or filter: missing exprs array");
339
+ return MongoOrExpr.of(exprs.map(deserializeFilterExpr));
340
+ }
341
+ case "not": {
342
+ const expr = record["expr"];
343
+ if (!expr || typeof expr !== "object") throw new Error("Invalid not filter: missing expr");
344
+ return new MongoNotExpr(deserializeFilterExpr(expr));
345
+ }
346
+ case "exists": {
347
+ const data = validate(ExistsFilterJson, json, "exists filter");
348
+ return new MongoExistsExpr(data.field, data.exists);
349
+ }
350
+ default: throw new Error(`Unknown filter expression kind: ${kind}`);
351
+ }
352
+ }
353
+ function deserializeDdlCommand(json) {
354
+ const kind = json["kind"];
355
+ switch (kind) {
356
+ case "createIndex": {
357
+ const data = validate(CreateIndexJson, json, "createIndex command");
358
+ return new CreateIndexCommand(data.collection, data.keys, {
359
+ unique: data.unique,
360
+ sparse: data.sparse,
361
+ expireAfterSeconds: data.expireAfterSeconds,
362
+ partialFilterExpression: data.partialFilterExpression,
363
+ name: data.name,
364
+ wildcardProjection: data.wildcardProjection,
365
+ collation: data.collation,
366
+ weights: data.weights,
367
+ default_language: data.default_language,
368
+ language_override: data.language_override
369
+ });
370
+ }
371
+ case "dropIndex": {
372
+ const data = validate(DropIndexJson, json, "dropIndex command");
373
+ return new DropIndexCommand(data.collection, data.name);
374
+ }
375
+ case "createCollection": {
376
+ const data = validate(CreateCollectionJson, json, "createCollection command");
377
+ return new CreateCollectionCommand(data.collection, {
378
+ validator: data.validator,
379
+ validationLevel: data.validationLevel,
380
+ validationAction: data.validationAction,
381
+ capped: data.capped,
382
+ size: data.size,
383
+ max: data.max,
384
+ timeseries: data.timeseries,
385
+ collation: data.collation,
386
+ changeStreamPreAndPostImages: data.changeStreamPreAndPostImages,
387
+ clusteredIndex: data.clusteredIndex
388
+ });
389
+ }
390
+ case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
391
+ case "collMod": {
392
+ const data = validate(CollModJson, json, "collMod command");
393
+ return new CollModCommand(data.collection, {
394
+ validator: data.validator,
395
+ validationLevel: data.validationLevel,
396
+ validationAction: data.validationAction,
397
+ changeStreamPreAndPostImages: data.changeStreamPreAndPostImages
398
+ });
399
+ }
400
+ default: throw new Error(`Unknown DDL command kind: ${kind}`);
401
+ }
402
+ }
403
+ function deserializeInspectionCommand(json) {
404
+ const kind = json["kind"];
405
+ switch (kind) {
406
+ case "listIndexes": return new ListIndexesCommand(validate(ListIndexesJson, json, "listIndexes command").collection);
407
+ case "listCollections":
408
+ validate(ListCollectionsJson, json, "listCollections command");
409
+ return new ListCollectionsCommand();
410
+ default: throw new Error(`Unknown inspection command kind: ${kind}`);
411
+ }
412
+ }
413
+ function deserializeCheck(json) {
414
+ const data = validate(CheckJson, json, "migration check");
415
+ return {
416
+ description: data.description,
417
+ source: deserializeInspectionCommand(data.source),
418
+ filter: deserializeFilterExpr(data.filter),
419
+ expect: data.expect
420
+ };
421
+ }
422
+ function deserializeStep(json) {
423
+ const data = validate(StepJson, json, "migration step");
424
+ return {
425
+ description: data.description,
426
+ command: deserializeDdlCommand(data.command)
427
+ };
428
+ }
429
+ function deserializeMongoOp(json) {
430
+ const data = validate(OperationJson, json, "migration operation");
431
+ return {
432
+ id: data.id,
433
+ label: data.label,
434
+ operationClass: data.operationClass,
435
+ precheck: data.precheck.map(deserializeCheck),
436
+ execute: data.execute.map(deserializeStep),
437
+ postcheck: data.postcheck.map(deserializeCheck)
438
+ };
439
+ }
440
+ function deserializeMongoOps(json) {
441
+ return json.map(deserializeMongoOp);
442
+ }
443
+ function serializeMongoOps(ops) {
444
+ return JSON.stringify(ops, null, 2);
445
+ }
446
+
447
+ //#endregion
448
+ //#region src/core/op-factory-call.ts
449
+ var OpFactoryCallNode = class {
450
+ freeze() {
451
+ Object.freeze(this);
452
+ }
453
+ };
454
+ function formatKeys(keys) {
455
+ return keys.map((k) => `${k.field}:${k.direction}`).join(", ");
456
+ }
457
+ var CreateIndexCall = class extends OpFactoryCallNode {
458
+ factory = "createIndex";
459
+ operationClass = "additive";
460
+ collection;
461
+ keys;
462
+ options;
463
+ label;
464
+ constructor(collection, keys, options) {
465
+ super();
466
+ this.collection = collection;
467
+ this.keys = keys;
468
+ this.options = options;
469
+ this.label = `Create index on ${collection} (${formatKeys(keys)})`;
470
+ this.freeze();
471
+ }
472
+ accept(visitor) {
473
+ return visitor.createIndex(this);
474
+ }
475
+ };
476
+ var DropIndexCall = class extends OpFactoryCallNode {
477
+ factory = "dropIndex";
478
+ operationClass = "destructive";
479
+ collection;
480
+ keys;
481
+ label;
482
+ constructor(collection, keys) {
483
+ super();
484
+ this.collection = collection;
485
+ this.keys = keys;
486
+ this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
487
+ this.freeze();
488
+ }
489
+ accept(visitor) {
490
+ return visitor.dropIndex(this);
491
+ }
492
+ };
493
+ var CreateCollectionCall = class extends OpFactoryCallNode {
494
+ factory = "createCollection";
495
+ operationClass = "additive";
496
+ collection;
497
+ options;
498
+ label;
499
+ constructor(collection, options) {
500
+ super();
501
+ this.collection = collection;
502
+ this.options = options;
503
+ this.label = `Create collection ${collection}`;
504
+ this.freeze();
505
+ }
506
+ accept(visitor) {
507
+ return visitor.createCollection(this);
508
+ }
509
+ };
510
+ var DropCollectionCall = class extends OpFactoryCallNode {
511
+ factory = "dropCollection";
512
+ operationClass = "destructive";
513
+ collection;
514
+ label;
515
+ constructor(collection) {
516
+ super();
517
+ this.collection = collection;
518
+ this.label = `Drop collection ${collection}`;
519
+ this.freeze();
520
+ }
521
+ accept(visitor) {
522
+ return visitor.dropCollection(this);
523
+ }
524
+ };
525
+ var CollModCall = class extends OpFactoryCallNode {
526
+ factory = "collMod";
527
+ collection;
528
+ options;
529
+ meta;
530
+ operationClass;
531
+ label;
532
+ constructor(collection, options, meta) {
533
+ super();
534
+ this.collection = collection;
535
+ this.options = options;
536
+ this.meta = meta;
537
+ this.operationClass = meta?.operationClass ?? "destructive";
538
+ this.label = meta?.label ?? `Modify collection ${collection}`;
539
+ this.freeze();
540
+ }
541
+ accept(visitor) {
542
+ return visitor.collMod(this);
543
+ }
544
+ };
545
+ function schemaIndexToCreateIndexOptions(index) {
546
+ return {
547
+ unique: index.unique || void 0,
548
+ sparse: index.sparse,
549
+ expireAfterSeconds: index.expireAfterSeconds,
550
+ partialFilterExpression: index.partialFilterExpression,
551
+ wildcardProjection: index.wildcardProjection,
552
+ collation: index.collation,
553
+ weights: index.weights,
554
+ default_language: index.default_language,
555
+ language_override: index.language_override
556
+ };
557
+ }
558
+ function schemaCollectionToCreateCollectionOptions(coll) {
559
+ const opts = coll.options;
560
+ const validator = coll.validator;
561
+ if (!opts && !validator) return void 0;
562
+ return {
563
+ capped: opts?.capped ? true : void 0,
564
+ size: opts?.capped?.size,
565
+ max: opts?.capped?.max,
566
+ timeseries: opts?.timeseries,
567
+ collation: opts?.collation,
568
+ clusteredIndex: opts?.clusteredIndex ? {
569
+ key: { _id: 1 },
570
+ unique: true,
571
+ ...opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}
572
+ } : void 0,
573
+ validator: validator ? { $jsonSchema: validator.jsonSchema } : void 0,
574
+ validationLevel: validator?.validationLevel,
575
+ validationAction: validator?.validationAction,
576
+ changeStreamPreAndPostImages: opts?.changeStreamPreAndPostImages
577
+ };
578
+ }
579
+
580
+ //#endregion
581
+ //#region src/core/render-ops.ts
582
+ const renderVisitor = {
583
+ createIndex(call) {
584
+ return createIndex(call.collection, call.keys, call.options);
585
+ },
586
+ dropIndex(call) {
587
+ return dropIndex(call.collection, call.keys);
588
+ },
589
+ createCollection(call) {
590
+ return createCollection(call.collection, call.options);
591
+ },
592
+ dropCollection(call) {
593
+ return dropCollection(call.collection);
594
+ },
595
+ collMod(call) {
596
+ return collMod(call.collection, call.options, call.meta);
597
+ }
598
+ };
599
+ function renderOps(calls) {
600
+ return calls.map((call) => call.accept(renderVisitor));
601
+ }
602
+
603
+ //#endregion
604
+ //#region src/core/mongo-planner.ts
605
+ function buildIndexLookupKey(index) {
606
+ const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
607
+ const opts = [
608
+ index.unique ? "unique" : "",
609
+ index.sparse ? "sparse" : "",
610
+ index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : "",
611
+ index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : "",
612
+ index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : "",
613
+ index.collation ? `col:${canonicalize(index.collation)}` : "",
614
+ index.weights ? `wt:${canonicalize(index.weights)}` : "",
615
+ index.default_language ? `dl:${index.default_language}` : "",
616
+ index.language_override ? `lo:${index.language_override}` : ""
617
+ ].filter(Boolean).join(";");
618
+ return opts ? `${keys}|${opts}` : keys;
619
+ }
620
+ function validatorsEqual(a, b) {
621
+ if (!a && !b) return true;
622
+ if (!a || !b) return false;
623
+ return a.validationLevel === b.validationLevel && a.validationAction === b.validationAction && canonicalize(a.jsonSchema) === canonicalize(b.jsonSchema);
624
+ }
625
+ function classifyValidatorUpdate(origin, dest) {
626
+ let hasDestructive = false;
627
+ if (canonicalize(origin.jsonSchema) !== canonicalize(dest.jsonSchema)) hasDestructive = true;
628
+ if (origin.validationAction !== dest.validationAction) {
629
+ if (dest.validationAction === "error") hasDestructive = true;
630
+ }
631
+ if (origin.validationLevel !== dest.validationLevel) {
632
+ if (dest.validationLevel === "strict") hasDestructive = true;
633
+ }
634
+ return hasDestructive ? "destructive" : "widening";
635
+ }
636
+ function hasImmutableOptionChange(origin, dest) {
637
+ if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
638
+ if (canonicalize(origin?.timeseries) !== canonicalize(dest?.timeseries)) return "timeseries";
639
+ if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
640
+ if (canonicalize(origin?.clusteredIndex) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
641
+ }
642
+ function collectionHasOptions(coll) {
643
+ return !!(coll.options || coll.validator);
644
+ }
645
+ var MongoMigrationPlanner = class {
646
+ planCalls(options) {
647
+ const contract = options.contract;
648
+ const originIR = options.schema;
649
+ const destinationIR = contractToMongoSchemaIR(contract);
650
+ const collCreates = [];
651
+ const drops = [];
652
+ const creates = [];
653
+ const validatorOps = [];
654
+ const mutableOptionOps = [];
655
+ const collDrops = [];
656
+ const conflicts = [];
657
+ const allCollectionNames = new Set([...originIR.collectionNames, ...destinationIR.collectionNames]);
658
+ for (const collName of [...allCollectionNames].sort()) {
659
+ const originColl = originIR.collection(collName);
660
+ const destColl = destinationIR.collection(collName);
661
+ if (!originColl) {
662
+ if (destColl && collectionHasOptions(destColl)) {
663
+ const opts = schemaCollectionToCreateCollectionOptions(destColl);
664
+ collCreates.push(new CreateCollectionCall(collName, opts));
665
+ }
666
+ } else if (!destColl) collDrops.push(new DropCollectionCall(collName));
667
+ else {
668
+ const immutableChange = hasImmutableOptionChange(originColl.options, destColl.options);
669
+ if (immutableChange) conflicts.push({
670
+ kind: "policy-violation",
671
+ summary: `Cannot change immutable collection option '${immutableChange}' on ${collName}`,
672
+ why: `MongoDB does not support modifying the '${immutableChange}' option after collection creation`
673
+ });
674
+ const mutableCall = planMutableOptionsDiffCall(collName, originColl.options, destColl.options);
675
+ if (mutableCall) mutableOptionOps.push(mutableCall);
676
+ const validatorCall = planValidatorDiffCall(collName, originColl.validator, destColl.validator);
677
+ if (validatorCall) validatorOps.push(validatorCall);
678
+ }
679
+ const originLookup = /* @__PURE__ */ new Map();
680
+ if (originColl) for (const idx of originColl.indexes) originLookup.set(buildIndexLookupKey(idx), idx);
681
+ const destLookup = /* @__PURE__ */ new Map();
682
+ if (destColl) for (const idx of destColl.indexes) destLookup.set(buildIndexLookupKey(idx), idx);
683
+ for (const [lookupKey, idx] of originLookup) if (!destLookup.has(lookupKey)) drops.push(new DropIndexCall(collName, idx.keys));
684
+ for (const [lookupKey, idx] of destLookup) if (!originLookup.has(lookupKey)) creates.push(new CreateIndexCall(collName, idx.keys, schemaIndexToCreateIndexOptions(idx)));
685
+ }
686
+ if (conflicts.length > 0) return {
687
+ kind: "failure",
688
+ conflicts
689
+ };
690
+ const allCalls = [
691
+ ...collCreates,
692
+ ...drops,
693
+ ...creates,
694
+ ...validatorOps,
695
+ ...mutableOptionOps,
696
+ ...collDrops
697
+ ];
698
+ for (const call of allCalls) if (!options.policy.allowedOperationClasses.includes(call.operationClass)) conflicts.push({
699
+ kind: "policy-violation",
700
+ summary: `${call.operationClass} operation disallowed: ${call.label}`,
701
+ why: `Policy does not allow '${call.operationClass}' operations`
702
+ });
703
+ if (conflicts.length > 0) return {
704
+ kind: "failure",
705
+ conflicts
706
+ };
707
+ return {
708
+ kind: "success",
709
+ calls: allCalls
710
+ };
711
+ }
712
+ plan(options) {
713
+ const contract = options.contract;
714
+ const result = this.planCalls(options);
715
+ if (result.kind === "failure") return result;
716
+ return {
717
+ kind: "success",
718
+ plan: {
719
+ targetId: "mongo",
720
+ destination: { storageHash: contract.storage.storageHash },
721
+ operations: renderOps(result.calls)
722
+ }
723
+ };
724
+ }
725
+ };
726
+ function planValidatorDiffCall(collName, originValidator, destValidator) {
727
+ if (validatorsEqual(originValidator, destValidator)) return void 0;
728
+ if (destValidator) {
729
+ const operationClass = originValidator ? classifyValidatorUpdate(originValidator, destValidator) : "destructive";
730
+ return new CollModCall(collName, {
731
+ validator: { $jsonSchema: destValidator.jsonSchema },
732
+ validationLevel: destValidator.validationLevel,
733
+ validationAction: destValidator.validationAction
734
+ }, {
735
+ id: `validator.${collName}.${originValidator ? "update" : "add"}`,
736
+ label: `${originValidator ? "Update" : "Add"} validator on ${collName}`,
737
+ operationClass
738
+ });
739
+ }
740
+ return new CollModCall(collName, {
741
+ validator: {},
742
+ validationLevel: "strict",
743
+ validationAction: "error"
744
+ }, {
745
+ id: `validator.${collName}.remove`,
746
+ label: `Remove validator on ${collName}`,
747
+ operationClass: "widening"
748
+ });
749
+ }
750
+ function planMutableOptionsDiffCall(collName, origin, dest) {
751
+ const originCSPPI = origin?.changeStreamPreAndPostImages;
752
+ const destCSPPI = dest?.changeStreamPreAndPostImages;
753
+ if (deepEqual(originCSPPI, destCSPPI)) return void 0;
754
+ const desiredCSPPI = destCSPPI ?? { enabled: false };
755
+ return new CollModCall(collName, { changeStreamPreAndPostImages: desiredCSPPI }, {
756
+ id: `options.${collName}.update`,
757
+ label: `Update mutable options on ${collName}`,
758
+ operationClass: desiredCSPPI.enabled ? "widening" : "destructive"
759
+ });
760
+ }
761
+
762
+ //#endregion
763
+ //#region src/core/mongo-runner.ts
764
+ function runnerFailure(code, summary, opts) {
765
+ return notOk({
766
+ code,
767
+ summary,
768
+ ...opts
769
+ });
770
+ }
771
+ var MongoMigrationRunner = class {
772
+ constructor(deps) {
773
+ this.deps = deps;
774
+ }
775
+ async execute(options) {
776
+ const { commandExecutor, inspectionExecutor, markerOps } = this.deps;
777
+ const operations = deserializeMongoOps(options.plan.operations);
778
+ const policyCheck = this.enforcePolicyCompatibility(options.policy, operations);
779
+ if (policyCheck) return policyCheck;
780
+ const existingMarker = await markerOps.readMarker();
781
+ const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
782
+ if (markerCheck) return markerCheck;
783
+ const checks = options.executionChecks;
784
+ const runPrechecks = checks?.prechecks !== false;
785
+ const runPostchecks = checks?.postchecks !== false;
786
+ const runIdempotency = checks?.idempotencyChecks !== false;
787
+ const filterEvaluator = new FilterEvaluator();
788
+ let operationsExecuted = 0;
789
+ for (const operation of operations) {
790
+ options.callbacks?.onOperationStart?.(operation);
791
+ try {
792
+ if (runPostchecks && runIdempotency) {
793
+ if (await this.allChecksSatisfied(operation.postcheck, inspectionExecutor, filterEvaluator)) continue;
794
+ }
795
+ if (runPrechecks) {
796
+ if (!await this.evaluateChecks(operation.precheck, inspectionExecutor, filterEvaluator)) return runnerFailure("PRECHECK_FAILED", `Operation ${operation.id} failed during precheck`, { meta: { operationId: operation.id } });
797
+ }
798
+ for (const step of operation.execute) await step.command.accept(commandExecutor);
799
+ if (runPostchecks) {
800
+ if (!await this.evaluateChecks(operation.postcheck, inspectionExecutor, filterEvaluator)) return runnerFailure("POSTCHECK_FAILED", `Operation ${operation.id} failed during postcheck`, { meta: { operationId: operation.id } });
801
+ }
802
+ operationsExecuted += 1;
803
+ } finally {
804
+ options.callbacks?.onOperationComplete?.(operation);
805
+ }
806
+ }
807
+ const destination = options.plan.destination;
808
+ const profileHash = options.destinationContract.profileHash ?? destination.storageHash;
809
+ if (operationsExecuted === 0 && existingMarker?.storageHash === destination.storageHash && existingMarker.profileHash === profileHash) return ok({
810
+ operationsPlanned: operations.length,
811
+ operationsExecuted
812
+ });
813
+ if (existingMarker) {
814
+ if (!await markerOps.updateMarker(existingMarker.storageHash, {
815
+ storageHash: destination.storageHash,
816
+ profileHash
817
+ })) return runnerFailure("MARKER_CAS_FAILURE", "Marker was modified by another process during migration execution.", { meta: {
818
+ expectedStorageHash: existingMarker.storageHash,
819
+ destinationStorageHash: destination.storageHash
820
+ } });
821
+ } else await markerOps.initMarker({
822
+ storageHash: destination.storageHash,
823
+ profileHash
824
+ });
825
+ const originHash = existingMarker?.storageHash ?? "";
826
+ await markerOps.writeLedgerEntry({
827
+ edgeId: `${originHash}->${destination.storageHash}`,
828
+ from: originHash,
829
+ to: destination.storageHash
830
+ });
831
+ return ok({
832
+ operationsPlanned: operations.length,
833
+ operationsExecuted
834
+ });
835
+ }
836
+ async evaluateChecks(checks, inspectionExecutor, filterEvaluator) {
837
+ for (const check of checks) {
838
+ const matchFound = (await check.source.accept(inspectionExecutor)).some((doc) => filterEvaluator.evaluate(check.filter, doc));
839
+ if (!(check.expect === "exists" ? matchFound : !matchFound)) return false;
840
+ }
841
+ return true;
842
+ }
843
+ async allChecksSatisfied(checks, inspectionExecutor, filterEvaluator) {
844
+ if (checks.length === 0) return false;
845
+ return this.evaluateChecks(checks, inspectionExecutor, filterEvaluator);
846
+ }
847
+ enforcePolicyCompatibility(policy, operations) {
848
+ const allowedClasses = new Set(policy.allowedOperationClasses);
849
+ for (const operation of operations) if (!allowedClasses.has(operation.operationClass)) return runnerFailure("POLICY_VIOLATION", `Operation ${operation.id} has class "${operation.operationClass}" which is not allowed by policy.`, {
850
+ why: `Policy only allows: ${[...allowedClasses].join(", ")}.`,
851
+ meta: {
852
+ operationId: operation.id,
853
+ operationClass: operation.operationClass
854
+ }
855
+ });
856
+ }
857
+ ensureMarkerCompatibility(marker, plan) {
858
+ const origin = plan.origin ?? null;
859
+ if (!origin) {
860
+ if (marker) return runnerFailure("MARKER_ORIGIN_MISMATCH", "Database already has a contract marker but the plan has no origin. This would silently overwrite the existing marker.", { meta: { markerStorageHash: marker.storageHash } });
861
+ return;
862
+ }
863
+ if (!marker) return runnerFailure("MARKER_ORIGIN_MISMATCH", `Missing contract marker: expected origin storage hash ${origin.storageHash}.`, { meta: { expectedOriginStorageHash: origin.storageHash } });
864
+ if (marker.storageHash !== origin.storageHash) return runnerFailure("MARKER_ORIGIN_MISMATCH", `Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`, { meta: {
865
+ markerStorageHash: marker.storageHash,
866
+ expectedOriginStorageHash: origin.storageHash
867
+ } });
868
+ }
869
+ };
870
+
871
+ //#endregion
872
+ //#region src/core/render-typescript.ts
873
+ function renderTypeScript(calls, meta) {
874
+ const imports = buildImports(collectFactoryNames(calls));
875
+ const planBody = calls.map((c) => c.accept(renderCallVisitor)).join(",\n");
876
+ return [
877
+ imports,
878
+ "",
879
+ "class M extends Migration {",
880
+ meta ? buildDescribeMethod(meta) : "",
881
+ " override plan() {",
882
+ " return [",
883
+ indent(planBody, 6),
884
+ " ];",
885
+ " }",
886
+ "}",
887
+ "",
888
+ "export default M;",
889
+ "Migration.run(import.meta.url, M);",
890
+ ""
891
+ ].join("\n");
892
+ }
893
+ function collectFactoryNames(calls) {
894
+ const names = /* @__PURE__ */ new Set();
895
+ for (const call of calls) names.add(call.factory);
896
+ return [...names].sort();
897
+ }
898
+ function buildImports(factoryNames) {
899
+ const lines = ["import { Migration } from '@prisma-next/family-mongo/migration';"];
900
+ if (factoryNames.length > 0) lines.push(`import { ${factoryNames.join(", ")} } from '@prisma-next/target-mongo/migration';`);
901
+ return lines.join("\n");
902
+ }
903
+ function buildDescribeMethod(meta) {
904
+ const lines = [];
905
+ lines.push(" override describe() {");
906
+ lines.push(" return {");
907
+ lines.push(` from: ${JSON.stringify(meta.from)},`);
908
+ lines.push(` to: ${JSON.stringify(meta.to)},`);
909
+ if (meta.kind) lines.push(` kind: ${JSON.stringify(meta.kind)},`);
910
+ if (meta.labels && meta.labels.length > 0) lines.push(` labels: ${renderLiteral(meta.labels)},`);
911
+ lines.push(" };");
912
+ lines.push(" }");
913
+ lines.push("");
914
+ return lines.join("\n");
915
+ }
916
+ const renderCallVisitor = {
917
+ createIndex(call) {
918
+ return call.options ? `createIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)}, ${renderLiteral(call.options)})` : `createIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)})`;
919
+ },
920
+ dropIndex(call) {
921
+ return `dropIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)})`;
922
+ },
923
+ createCollection(call) {
924
+ return call.options ? `createCollection(${renderLiteral(call.collection)}, ${renderLiteral(call.options)})` : `createCollection(${renderLiteral(call.collection)})`;
925
+ },
926
+ dropCollection(call) {
927
+ return `dropCollection(${renderLiteral(call.collection)})`;
928
+ },
929
+ collMod(call) {
930
+ return call.meta ? `collMod(${renderLiteral(call.collection)}, ${renderLiteral(call.options)}, ${renderLiteral(call.meta)})` : `collMod(${renderLiteral(call.collection)}, ${renderLiteral(call.options)})`;
931
+ }
932
+ };
933
+ function renderLiteral(value) {
934
+ if (value === void 0) return "undefined";
935
+ if (value === null) return "null";
936
+ if (typeof value === "string") return JSON.stringify(value);
937
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
938
+ if (Array.isArray(value)) {
939
+ if (value.length === 0) return "[]";
940
+ const items = value.map((v) => renderLiteral(v));
941
+ const singleLine = `[${items.join(", ")}]`;
942
+ if (singleLine.length <= 80) return singleLine;
943
+ return `[\n${items.map((i) => ` ${i}`).join(",\n")},\n]`;
944
+ }
945
+ if (typeof value === "object") {
946
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0);
947
+ if (entries.length === 0) return "{}";
948
+ const items = entries.map(([k, v]) => `${renderKey(k)}: ${renderLiteral(v)}`);
949
+ const singleLine = `{ ${items.join(", ")} }`;
950
+ if (singleLine.length <= 80) return singleLine;
951
+ return `{\n${items.map((i) => ` ${i}`).join(",\n")},\n}`;
952
+ }
953
+ return String(value);
954
+ }
955
+ function renderKey(key) {
956
+ if (key === "__proto__") return JSON.stringify(key);
957
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
958
+ }
959
+ function indent(text, spaces) {
960
+ const pad = " ".repeat(spaces);
961
+ return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : line).join("\n");
962
+ }
963
+
964
+ //#endregion
965
+ export { CollModCall, CreateCollectionCall, CreateIndexCall, DropCollectionCall, DropIndexCall, FilterEvaluator, MongoMigrationPlanner, MongoMigrationRunner, contractToMongoSchemaIR, deserializeMongoOp, deserializeMongoOps, formatMongoOperations, initMarker, readMarker, renderOps, renderTypeScript, schemaCollectionToCreateCollectionOptions, schemaIndexToCreateIndexOptions, serializeMongoOps, updateMarker, writeLedgerEntry };
62
966
  //# sourceMappingURL=control.mjs.map