@prisma/orm-target-mongo 8.0.0-rc.2-dev.3 → 8.0.0-rc.20

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 (122) hide show
  1. package/README.md +1 -5
  2. package/dist/adapter.d.mts +2 -2
  3. package/dist/adapter.d.mts.map +1 -1
  4. package/dist/adapter.mjs +3 -3
  5. package/dist/adapter.mjs.map +1 -1
  6. package/dist/adapter__control.d.mts +204 -3
  7. package/dist/adapter__control.d.mts.map +1 -0
  8. package/dist/adapter__control.mjs +481 -3
  9. package/dist/adapter__control.mjs.map +1 -0
  10. package/dist/adapter__runtime.d.mts +3 -1
  11. package/dist/adapter__runtime.d.mts.map +1 -1
  12. package/dist/adapter__runtime.mjs +5 -3
  13. package/dist/adapter__runtime.mjs.map +1 -1
  14. package/dist/codec-ids-BKQbXhZt.d.mts +18 -0
  15. package/dist/codec-ids-BKQbXhZt.d.mts.map +1 -0
  16. package/dist/codec-ids-WdrumsYA-CZVjuKW8.mjs +18 -0
  17. package/dist/codec-ids-WdrumsYA-CZVjuKW8.mjs.map +1 -0
  18. package/dist/codec-types-oKxM85p-.d.mts +89 -0
  19. package/dist/codec-types-oKxM85p-.d.mts.map +1 -0
  20. package/dist/codecs-B1ab7vfj-QL33tkm2.mjs +392 -0
  21. package/dist/codecs-B1ab7vfj-QL33tkm2.mjs.map +1 -0
  22. package/dist/codecs-Y5GYsLPj.d.mts +36 -0
  23. package/dist/codecs-Y5GYsLPj.d.mts.map +1 -0
  24. package/dist/control-Cq6qWohQ.mjs +2421 -0
  25. package/dist/control-Cq6qWohQ.mjs.map +1 -0
  26. package/dist/{control-CxXmTy_h.d.mts → control-CrFS8bqh.d.mts} +133 -95
  27. package/dist/control-CrFS8bqh.d.mts.map +1 -0
  28. package/dist/data-types-LShlnAUM.d.mts +25 -0
  29. package/dist/data-types-LShlnAUM.d.mts.map +1 -0
  30. package/dist/data-types-hoKLBrYZ-h9cAgbOy.mjs +624 -0
  31. package/dist/data-types-hoKLBrYZ-h9cAgbOy.mjs.map +1 -0
  32. package/dist/descriptor-meta-DTj807jv-CjvK2XDD.mjs +146 -0
  33. package/dist/descriptor-meta-DTj807jv-CjvK2XDD.mjs.map +1 -0
  34. package/dist/descriptor-meta-runtime-BaRbjvD7-D0786_Lv.mjs +20 -0
  35. package/dist/descriptor-meta-runtime-BaRbjvD7-D0786_Lv.mjs.map +1 -0
  36. package/dist/driver.d.mts +1 -1
  37. package/dist/driver.mjs +1 -1
  38. package/dist/driver__control.d.mts +4 -3
  39. package/dist/driver__control.d.mts.map +1 -1
  40. package/dist/driver__control.mjs +5 -2
  41. package/dist/driver__control.mjs.map +1 -1
  42. package/dist/{migration-BkqcE2Uv.d.mts → migration-C4m4Uj8E.d.mts} +15 -7
  43. package/dist/migration-C4m4Uj8E.d.mts.map +1 -0
  44. package/dist/migration-factories-BqROEHRs-CfExMr9r.mjs +406 -0
  45. package/dist/migration-factories-BqROEHRs-CfExMr9r.mjs.map +1 -0
  46. package/dist/{mongo-adapter-B2HEyX0b-CQt2HH_o.mjs → mongo-adapter-Bo6ciURK-DIMJdC7y.mjs} +37 -18
  47. package/dist/mongo-adapter-Bo6ciURK-DIMJdC7y.mjs.map +1 -0
  48. package/dist/{mongo-adapter-CjJU3S9o-BIJruFYW.d.mts → mongo-adapter-wvK2fQOZ-FswLS-PJ.d.mts} +2 -2
  49. package/dist/mongo-adapter-wvK2fQOZ-FswLS-PJ.d.mts.map +1 -0
  50. package/dist/{mongo-driver-D1lEf5Kl-KPfGUVXK.d.mts → mongo-driver-CTyvKDUg-Bm9sBrfs.d.mts} +9 -4
  51. package/dist/mongo-driver-CTyvKDUg-Bm9sBrfs.d.mts.map +1 -0
  52. package/dist/{mongo-driver-Dk7mris6-CMTaUgPD.mjs → mongo-driver-Cu6FP6OU-DhOzFM2D.mjs} +34 -8
  53. package/dist/mongo-driver-Cu6FP6OU-DhOzFM2D.mjs.map +1 -0
  54. package/dist/mongo-target-errors-Btf3ML1w-QOtEG7EX.mjs +9 -0
  55. package/dist/mongo-target-errors-Btf3ML1w-QOtEG7EX.mjs.map +1 -0
  56. package/dist/{op-factory-call-B7EKa4Ri-B2Lb6W68.d.mts → op-factory-call-BoYr7vvM-CjA8nx36.d.mts} +43 -9
  57. package/dist/op-factory-call-BoYr7vvM-CjA8nx36.d.mts.map +1 -0
  58. package/dist/pack-CIfujQmd.d.mts +266 -0
  59. package/dist/pack-CIfujQmd.d.mts.map +1 -0
  60. package/dist/prisma6-binding-DhnqslLT.d.mts +54 -0
  61. package/dist/prisma6-binding-DhnqslLT.d.mts.map +1 -0
  62. package/dist/prisma6-binding-vt4tbT-Z.mjs +45 -0
  63. package/dist/prisma6-binding-vt4tbT-Z.mjs.map +1 -0
  64. package/dist/{runtime-B9_-kjBX.d.mts → runtime-DbFgyDql.d.mts} +2 -2
  65. package/dist/runtime-DbFgyDql.d.mts.map +1 -0
  66. package/dist/target.d.mts +11 -8
  67. package/dist/target.mjs +7 -3
  68. package/dist/target__codec-ids.d.mts +2 -0
  69. package/dist/target__codec-ids.mjs +2 -0
  70. package/dist/target__codec-types.d.mts +2 -2
  71. package/dist/target__codecs.d.mts +2 -0
  72. package/dist/target__codecs.mjs +2 -0
  73. package/dist/target__control.d.mts +3 -4
  74. package/dist/target__control.mjs +2 -2
  75. package/dist/target__data-types.d.mts +2 -0
  76. package/dist/target__data-types.mjs +2 -0
  77. package/dist/target__migration.d.mts +2 -2
  78. package/dist/target__migration.mjs +2 -2
  79. package/dist/target__pack.d.mts +3 -2
  80. package/dist/target__pack.mjs +2 -2
  81. package/dist/target__pack.mjs.map +1 -1
  82. package/dist/target__prisma6-binding.d.mts +2 -0
  83. package/dist/target__prisma6-binding.mjs +2 -0
  84. package/dist/target__runtime.d.mts +1 -1
  85. package/dist/target__runtime.mjs +3 -3
  86. package/dist/target__runtime.mjs.map +1 -1
  87. package/package.json +16 -15
  88. package/dist/adapter__codec-ids.d.mts +0 -12
  89. package/dist/adapter__codec-ids.d.mts.map +0 -1
  90. package/dist/adapter__codec-ids.mjs +0 -2
  91. package/dist/adapter__codec-types.d.mts +0 -38
  92. package/dist/adapter__codec-types.d.mts.map +0 -1
  93. package/dist/adapter__codec-types.mjs +0 -1
  94. package/dist/adapter__codecs.d.mts +0 -14
  95. package/dist/adapter__codecs.d.mts.map +0 -1
  96. package/dist/adapter__codecs.mjs +0 -2
  97. package/dist/codec-ids-BennPcpQ-Btzz3ILV.mjs +0 -12
  98. package/dist/codec-ids-BennPcpQ-Btzz3ILV.mjs.map +0 -1
  99. package/dist/codec-types-Bx2fHyq_.d.mts +0 -35
  100. package/dist/codec-types-Bx2fHyq_.d.mts.map +0 -1
  101. package/dist/codecs-DLWO-Cjc-CWfQj0bY.mjs +0 -170
  102. package/dist/codecs-DLWO-Cjc-CWfQj0bY.mjs.map +0 -1
  103. package/dist/control-CxXmTy_h.d.mts.map +0 -1
  104. package/dist/control-D2-m_BQJ.mjs +0 -1563
  105. package/dist/control-D2-m_BQJ.mjs.map +0 -1
  106. package/dist/control-DQD76x-1.mjs +0 -437
  107. package/dist/control-DQD76x-1.mjs.map +0 -1
  108. package/dist/control-rPM2HNxW.d.mts +0 -170
  109. package/dist/control-rPM2HNxW.d.mts.map +0 -1
  110. package/dist/descriptor-meta-CBiaTwe5-HX742pZh.mjs +0 -17
  111. package/dist/descriptor-meta-CBiaTwe5-HX742pZh.mjs.map +0 -1
  112. package/dist/migration-BkqcE2Uv.d.mts.map +0 -1
  113. package/dist/migration-factories-CCJFG_3e-75oJ8xn2.mjs +0 -201
  114. package/dist/migration-factories-CCJFG_3e-75oJ8xn2.mjs.map +0 -1
  115. package/dist/mongo-adapter-B2HEyX0b-CQt2HH_o.mjs.map +0 -1
  116. package/dist/mongo-adapter-CjJU3S9o-BIJruFYW.d.mts.map +0 -1
  117. package/dist/mongo-driver-D1lEf5Kl-KPfGUVXK.d.mts.map +0 -1
  118. package/dist/mongo-driver-Dk7mris6-CMTaUgPD.mjs.map +0 -1
  119. package/dist/op-factory-call-B7EKa4Ri-B2Lb6W68.d.mts.map +0 -1
  120. package/dist/pack-MK8ZQfrL.d.mts +0 -22
  121. package/dist/pack-MK8ZQfrL.d.mts.map +0 -1
  122. package/dist/runtime-B9_-kjBX.d.mts.map +0 -1
@@ -0,0 +1,2421 @@
1
+ import { t as mongoTargetError } from "./mongo-target-errors-Btf3ML1w-QOtEG7EX.mjs";
2
+ import { t as mongoTargetDescriptorMeta } from "./descriptor-meta-DTj807jv-CjvK2XDD.mjs";
3
+ import { a as dropCollection, c as renameField, d as unsetField, n as createCollection, o as dropIndex, r as createIndex, s as renameCollection, t as collMod, u as unrewritableFieldNameReason } from "./migration-factories-BqROEHRs-CfExMr9r.mjs";
4
+ import { type } from "arktype";
5
+ import { blindCast } from "@prisma/orm-framework/utils/casts";
6
+ import { ifDefined } from "@prisma/orm-framework/utils/defined";
7
+ import { NamespaceBase, UNBOUND_NAMESPACE_ID, elementCoordinates, freezeNode, hydrateNamespaceEntities } from "@prisma/orm-framework/components/ir";
8
+ import { MongoSchemaCollection, MongoSchemaIR, canonicalize, deepEqual, indexLookupKey } from "@prisma/orm-family-mongo/schema-ir";
9
+ import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr, RenameCollectionCommand, isMongoDmlOperation } from "@prisma/orm-family-mongo/query-ast/control";
10
+ import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma/orm-family-mongo/query-ast/execution";
11
+ import { canonicalizeSchemasForVerification, contractToMongoSchemaIR, diffMongoSchemas, fieldRewriteOf } from "@prisma/orm-family-mongo/family/control";
12
+ import { APP_SPACE_ID, VERIFY_CODE_SCHEMA_FAILURE, contractSnapshotJsonSpecifier, contractSnapshotTypesSpecifier, describeMigrationStatement, expectAbsent, expectPresent, failedPostcheckWhy, failedPrecheckWhy, issueOutcome, migrationRunnerFailure, modelDisplayName, schemaCheckWhy, withDatabaseUnchanged, withFailingSpace } from "@prisma/orm-framework/components/control";
13
+ import { notOk, ok } from "@prisma/orm-framework/utils/result";
14
+ import { MongoContractSerializerBase, MongoSchemaVerifierBase } from "@prisma/orm-family-mongo/family/ir";
15
+ import { MongoCollection, MongoStorage, collectionControlPoliciesOf, collectionControlPolicyOf, collectionRootModelsOf, hasOwnFieldStoredAs, modelAndBasesOf, mongoModelsOf, ownEmbedStoredNamesOf, ownStoredNameOf, ownStoredNamesOf, validatorFieldNamesOf, validatorRootShapesOf } from "@prisma/orm-family-mongo/contract";
16
+ import { composeMongoEntityKinds } from "@prisma/orm-family-mongo/contract/entity-kinds";
17
+ import { asNamespaceId, governingControlPolicy } from "@prisma/orm-framework/contract/types";
18
+ import { InternalError } from "@prisma/orm-framework/utils/internal-error";
19
+ import { TsExpression, jsonToTsSource, renderImports } from "@prisma/orm-framework/ts-render";
20
+ import { Migration } from "@prisma/orm-toolchain/migration-tools/migration";
21
+ import { resolveRequirementSpecifiers } from "@prisma/orm-framework/components/emission";
22
+ import { detectScaffoldRuntime, shebangLineFor } from "@prisma/orm-toolchain/migration-tools/migration-ts";
23
+ import { verifyMongoSchema } from "@prisma/orm-family-mongo/family/schema-verify";
24
+ //#region ../../../3-targets/mongo/1-target/dist/control.mjs
25
+ /**
26
+ * Mongo target `Namespace` concretion. In Mongo the "namespace" concept
27
+ * binds to the connection's `db` field — a `MongoTargetDatabase` instance
28
+ * names the database the collections live under. `entries['collection']`
29
+ * holds collection IR. The unbound singleton (below) is the late-bound
30
+ * namespace whose binding the connection's `db` resolves at runtime rather
31
+ * than at authoring time.
32
+ *
33
+ * Qualifier emission is the rendering seam: query / DDL emission asks the
34
+ * namespace for its qualifier (e.g. `"<db>.<collection>"`) and consumes
35
+ * the result polymorphically. The unbound singleton overrides these
36
+ * methods to elide the prefix entirely — call sites stay polymorphic and
37
+ * never branch on `id === UNBOUND_NAMESPACE_ID`.
38
+ */
39
+ var MongoTargetDatabase = class extends NamespaceBase {
40
+ id;
41
+ entries;
42
+ constructor(input) {
43
+ super();
44
+ this.id = input.id;
45
+ const rawEntries = {
46
+ collection: {},
47
+ ...input.entries
48
+ };
49
+ this.entries = Object.freeze(blindCast(hydrateNamespaceEntities(rawEntries, composeMongoEntityKinds(), "carry")));
50
+ Object.defineProperty(this, "kind", {
51
+ value: "database",
52
+ writable: false,
53
+ enumerable: false,
54
+ configurable: true
55
+ });
56
+ freezeNode(this);
57
+ }
58
+ get collection() {
59
+ return this.entries.collection ?? Object.freeze({});
60
+ }
61
+ /**
62
+ * The bare qualifier as it would appear in a rendered string. The
63
+ * unbound-database singleton overrides this to return `''`.
64
+ */
65
+ qualifier() {
66
+ return this.id;
67
+ }
68
+ /**
69
+ * Qualify a collection name with the database prefix. The
70
+ * unbound-database singleton overrides this to emit just the
71
+ * collection name. Used by emission/introspection paths that need a
72
+ * fully-qualified reference.
73
+ */
74
+ qualifyCollection(collectionName) {
75
+ return `${this.id}.${collectionName}`;
76
+ }
77
+ };
78
+ /**
79
+ * Singleton subclass for the reserved sentinel namespace id
80
+ * (`UNBOUND_NAMESPACE_ID`) — the late-bound namespace whose binding the
81
+ * connection's `db` resolves at runtime. Overrides qualifier emission
82
+ * to elide the database prefix; call sites that consume `qualifier()`
83
+ * / `qualifyCollection()` get unqualified output without branching on
84
+ * the namespace id.
85
+ *
86
+ * This is the target-side materialization of "the framework provides
87
+ * affordances; targets implement specifics": the framework names the
88
+ * sentinel; Mongo decides what late-bound means here (the collection
89
+ * name, naked — the database is supplied by the live connection).
90
+ */
91
+ var MongoTargetUnboundDatabase = class MongoTargetUnboundDatabase extends MongoTargetDatabase {
92
+ static instance = new MongoTargetUnboundDatabase();
93
+ constructor() {
94
+ super({ id: UNBOUND_NAMESPACE_ID });
95
+ }
96
+ qualifier() {
97
+ return "";
98
+ }
99
+ qualifyCollection(collectionName) {
100
+ return collectionName;
101
+ }
102
+ };
103
+ var MongoTargetContractSerializer = class extends MongoContractSerializerBase {
104
+ constructTargetContract(validated) {
105
+ const { storage, ...rest } = validated;
106
+ const namespaces = Object.fromEntries(Object.entries(storage.namespaces).map(([nsId, nsData]) => {
107
+ const collectionCount = Object.keys(nsData.entries.collection ?? {}).length;
108
+ const valueSetCount = Object.keys(nsData.entries["valueSet"] ?? {}).length;
109
+ if (nsId === UNBOUND_NAMESPACE_ID && collectionCount === 0 && valueSetCount === 0) return [nsId, MongoTargetUnboundDatabase.instance];
110
+ const dbInput = { id: nsData.id };
111
+ if (nsData.entries["collection"] !== void 0 || nsData.entries["valueSet"] !== void 0) dbInput.entries = {
112
+ collection: blindCast(nsData.entries["collection"] ?? {}),
113
+ ...ifDefined("valueSet", blindCast(nsData.entries["valueSet"]))
114
+ };
115
+ return [nsId, new MongoTargetDatabase(dbInput)];
116
+ }));
117
+ const targetStorage = new MongoStorage({
118
+ storageHash: storage.storageHash,
119
+ namespaces
120
+ });
121
+ return {
122
+ ...rest,
123
+ storage: targetStorage
124
+ };
125
+ }
126
+ serializeContract(contract) {
127
+ const { storage, ...rest } = contract;
128
+ const namespacesJson = {};
129
+ for (const [nsId, ns] of Object.entries(storage.namespaces)) {
130
+ const collectionsOut = {};
131
+ for (const [collName, coll] of Object.entries(ns.entries.collection ?? {})) collectionsOut[collName] = blindCast(JSON.parse(JSON.stringify(coll)));
132
+ const valueSetOut = {};
133
+ for (const [vsName, vs] of Object.entries(ns.entries.valueSet ?? {})) valueSetOut[vsName] = blindCast(JSON.parse(JSON.stringify(vs)));
134
+ namespacesJson[nsId] = {
135
+ id: ns.id,
136
+ kind: "mongo-database",
137
+ entries: {
138
+ collection: collectionsOut,
139
+ ...ifDefined("valueSet", Object.keys(valueSetOut).length > 0 ? valueSetOut : void 0)
140
+ }
141
+ };
142
+ }
143
+ return blindCast({
144
+ ...rest,
145
+ storage: {
146
+ storageHash: String(storage.storageHash),
147
+ namespaces: namespacesJson
148
+ }
149
+ });
150
+ }
151
+ };
152
+ /**
153
+ * The single module a scaffolded Mongo `migration.ts` imports from: the
154
+ * operation factories below, plus the `Migration` base and `MigrationCLI`
155
+ * that `render-typescript.ts` adds. The authored name; `render-typescript.ts`
156
+ * maps it to whatever the consuming application's import root calls it,
157
+ * once over the whole assembled import list.
158
+ */
159
+ const TARGET_MIGRATION_MODULE = "@internal/target-mongo/migration";
160
+ var OpFactoryCallNode = class extends TsExpression {
161
+ importRequirements() {
162
+ return [{
163
+ moduleSpecifier: TARGET_MIGRATION_MODULE,
164
+ symbol: this.factoryName
165
+ }];
166
+ }
167
+ freeze() {
168
+ Object.freeze(this);
169
+ }
170
+ };
171
+ function formatKeys(keys) {
172
+ return keys.map((k) => `${k.field}:${k.direction}`).join(", ");
173
+ }
174
+ var CreateIndexCall = class extends OpFactoryCallNode {
175
+ factoryName = "createIndex";
176
+ operationClass = "additive";
177
+ collection;
178
+ keys;
179
+ options;
180
+ label;
181
+ constructor(collection, keys, options) {
182
+ super();
183
+ this.collection = collection;
184
+ this.keys = keys;
185
+ this.options = options;
186
+ this.label = `Create index on ${collection} (${formatKeys(keys)})`;
187
+ this.freeze();
188
+ }
189
+ toOp() {
190
+ return createIndex(this.collection, this.keys, this.options);
191
+ }
192
+ renderTypeScript() {
193
+ return this.options ? `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)}, ${jsonToTsSource(this.options)})` : `createIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
194
+ }
195
+ };
196
+ var DropIndexCall = class extends OpFactoryCallNode {
197
+ factoryName = "dropIndex";
198
+ operationClass = "widening";
199
+ collection;
200
+ keys;
201
+ label;
202
+ constructor(collection, keys) {
203
+ super();
204
+ this.collection = collection;
205
+ this.keys = keys;
206
+ this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
207
+ this.freeze();
208
+ }
209
+ toOp() {
210
+ return dropIndex(this.collection, this.keys);
211
+ }
212
+ renderTypeScript() {
213
+ return `dropIndex(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.keys)})`;
214
+ }
215
+ };
216
+ var CreateCollectionCall = class extends OpFactoryCallNode {
217
+ factoryName = "createCollection";
218
+ operationClass = "additive";
219
+ collection;
220
+ options;
221
+ label;
222
+ constructor(collection, options) {
223
+ super();
224
+ this.collection = collection;
225
+ this.options = options;
226
+ this.label = `Create collection ${collection}`;
227
+ this.freeze();
228
+ }
229
+ toOp() {
230
+ return createCollection(this.collection, this.options);
231
+ }
232
+ renderTypeScript() {
233
+ return this.options ? `createCollection(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})` : `createCollection(${jsonToTsSource(this.collection)})`;
234
+ }
235
+ };
236
+ var DropCollectionCall = class extends OpFactoryCallNode {
237
+ factoryName = "dropCollection";
238
+ operationClass = "destructive";
239
+ collection;
240
+ label;
241
+ constructor(collection) {
242
+ super();
243
+ this.collection = collection;
244
+ this.label = `Drop collection ${collection}`;
245
+ this.freeze();
246
+ }
247
+ toOp() {
248
+ return dropCollection(this.collection);
249
+ }
250
+ renderTypeScript() {
251
+ return `dropCollection(${jsonToTsSource(this.collection)})`;
252
+ }
253
+ };
254
+ var RenameCollectionCall = class extends OpFactoryCallNode {
255
+ factoryName = "renameCollection";
256
+ operationClass = "widening";
257
+ from;
258
+ to;
259
+ label;
260
+ constructor(from, to) {
261
+ super();
262
+ this.from = from;
263
+ this.to = to;
264
+ this.label = `Rename collection ${from} to ${to}`;
265
+ this.freeze();
266
+ }
267
+ toOp() {
268
+ return renameCollection(this.from, this.to);
269
+ }
270
+ renderTypeScript() {
271
+ return `renameCollection(${jsonToTsSource(this.from)}, ${jsonToTsSource(this.to)})`;
272
+ }
273
+ };
274
+ function renderFilterOption(filter) {
275
+ return filter === void 0 ? "" : `, ${jsonToTsSource({ filter })}`;
276
+ }
277
+ var RenameFieldCall = class extends OpFactoryCallNode {
278
+ factoryName = "renameField";
279
+ operationClass = "widening";
280
+ collection;
281
+ from;
282
+ to;
283
+ filter;
284
+ label;
285
+ constructor(collection, from, to, filter) {
286
+ super();
287
+ this.collection = collection;
288
+ this.from = from;
289
+ this.to = to;
290
+ this.filter = filter;
291
+ this.label = this.toOp().label;
292
+ this.freeze();
293
+ }
294
+ toOp() {
295
+ return renameField(this.collection, this.from, this.to, ifDefined("filter", this.filter));
296
+ }
297
+ renderTypeScript() {
298
+ return `renameField(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.from)}, ${jsonToTsSource(this.to)}${renderFilterOption(this.filter)})`;
299
+ }
300
+ };
301
+ var UnsetFieldCall = class extends OpFactoryCallNode {
302
+ factoryName = "unsetField";
303
+ operationClass = "destructive";
304
+ collection;
305
+ field;
306
+ filter;
307
+ label;
308
+ constructor(collection, field, filter) {
309
+ super();
310
+ this.collection = collection;
311
+ this.field = field;
312
+ this.filter = filter;
313
+ this.label = this.toOp().label;
314
+ this.freeze();
315
+ }
316
+ toOp() {
317
+ return unsetField(this.collection, this.field, ifDefined("filter", this.filter));
318
+ }
319
+ renderTypeScript() {
320
+ return `unsetField(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.field)}${renderFilterOption(this.filter)})`;
321
+ }
322
+ };
323
+ var CollModCall = class extends OpFactoryCallNode {
324
+ factoryName = "collMod";
325
+ collection;
326
+ options;
327
+ meta;
328
+ operationClass;
329
+ label;
330
+ constructor(collection, options, meta) {
331
+ super();
332
+ this.collection = collection;
333
+ this.options = options;
334
+ this.meta = meta;
335
+ this.operationClass = meta?.operationClass ?? "widening";
336
+ this.label = meta?.label ?? `Modify collection ${collection}`;
337
+ this.freeze();
338
+ }
339
+ toOp() {
340
+ return collMod(this.collection, this.options, this.meta);
341
+ }
342
+ renderTypeScript() {
343
+ return this.meta ? `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)}, ${jsonToTsSource(this.meta)})` : `collMod(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.options)})`;
344
+ }
345
+ };
346
+ function schemaIndexToCreateIndexOptions(index) {
347
+ return {
348
+ ...index.unique ? { unique: true } : {},
349
+ ...ifDefined("sparse", index.sparse),
350
+ ...ifDefined("expireAfterSeconds", index.expireAfterSeconds),
351
+ ...ifDefined("partialFilterExpression", index.partialFilterExpression),
352
+ ...ifDefined("wildcardProjection", index.wildcardProjection),
353
+ ...ifDefined("collation", index.collation),
354
+ ...ifDefined("weights", index.weights),
355
+ ...ifDefined("default_language", index.default_language),
356
+ ...ifDefined("language_override", index.language_override)
357
+ };
358
+ }
359
+ function schemaCollectionToCreateCollectionOptions(coll) {
360
+ const opts = coll.options;
361
+ const validator = coll.validator;
362
+ if (!opts && !validator) return void 0;
363
+ return {
364
+ ...opts?.capped ? { capped: true } : {},
365
+ ...ifDefined("size", opts?.capped?.size),
366
+ ...ifDefined("max", opts?.capped?.max),
367
+ ...ifDefined("timeseries", opts?.timeseries),
368
+ ...ifDefined("collation", opts?.collation),
369
+ ...opts?.clusteredIndex ? { clusteredIndex: {
370
+ key: { _id: 1 },
371
+ unique: true,
372
+ ...opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}
373
+ } } : {},
374
+ ...validator ? { validator: { $jsonSchema: validator.jsonSchema } } : {},
375
+ ...ifDefined("validationLevel", validator?.validationLevel),
376
+ ...ifDefined("validationAction", validator?.validationAction),
377
+ ...ifDefined("changeStreamPreAndPostImages", opts?.changeStreamPreAndPostImages)
378
+ };
379
+ }
380
+ /** The collection a model stores its documents in; a model without `@@map` names it verbatim. */
381
+ function collectionOf(contract, coordinate) {
382
+ const collection = contract?.domain.namespaces[coordinate.namespaceId]?.models[coordinate.model]?.storage["collection"];
383
+ return typeof collection === "string" ? collection : coordinate.model;
384
+ }
385
+ function modelAt(contract, coordinate) {
386
+ const models = mongoModelsOf(contract.domain, coordinate.namespaceId);
387
+ return Object.hasOwn(models, coordinate.model) ? models[coordinate.model] : void 0;
388
+ }
389
+ /** The stored name of `field` of the model at `coordinate`, or undefined when the model does not declare it. */
390
+ function storedFieldNameAt(contract, coordinate) {
391
+ const model = modelAt(contract, coordinate);
392
+ return model === void 0 || !Object.hasOwn(model.fields, coordinate.field) ? void 0 : ownStoredNameOf(model, coordinate.field);
393
+ }
394
+ /** The stored name of a field the statement's resolution found on the model at `coordinate`. */
395
+ function resolvedStoredNameAt(contract, coordinate) {
396
+ const storedName = storedFieldNameAt(contract, coordinate);
397
+ if (storedName === void 0) throw new InternalError(`Field "${coordinate.model}.${coordinate.field}" a statement names has no stored name`);
398
+ return storedName;
399
+ }
400
+ /**
401
+ * The condition that selects a variant's documents in the collection it shares with its base: the
402
+ * base's discriminator field holding the variant's value. A model that is not a variant has none.
403
+ */
404
+ function variantFilter(contract, coordinate) {
405
+ const base = modelAt(contract, coordinate)?.base;
406
+ if (base === void 0) return void 0;
407
+ const baseModel = modelAt(contract, {
408
+ namespaceId: asNamespaceId(base.namespace),
409
+ model: base.model
410
+ });
411
+ const field = baseModel?.discriminator?.field;
412
+ const value = Object.entries(baseModel?.variants ?? {}).find(([, variant]) => variant.namespace === coordinate.namespaceId && variant.model === coordinate.model)?.[0];
413
+ return baseModel === void 0 || field === void 0 || value === void 0 ? void 0 : { [ownStoredNameOf(baseModel, field)]: value };
414
+ }
415
+ function declaredCollection(contract, collection) {
416
+ return Object.values(contract.storage.namespaces).map((namespace) => namespace.entries["collection"]?.[collection]).find((entry) => entry instanceof MongoCollection);
417
+ }
418
+ /**
419
+ * The control policy of `collection`. A collection the contract leaves out keeps the policy `fromContract` gave it, when that was not `managed`, so leaving an `external` collection out of the contract does not drop it.
420
+ */
421
+ function collectionControlOf(contract, fromContract, collection) {
422
+ const declared = declaredCollection(contract, collection);
423
+ const original = declared === void 0 && fromContract !== null ? declaredCollection(fromContract, collection) : void 0;
424
+ const policy = collectionControlPolicyOf(collection, collectionControlPoliciesOf(contract));
425
+ if (original === void 0 || fromContract === null) return {
426
+ policy,
427
+ declared: declared?.control
428
+ };
429
+ const originalPolicy = governingControlPolicy({ explicitNodeControlPolicy: original.control }, fromContract.defaultControlPolicy);
430
+ return {
431
+ policy: originalPolicy === "managed" ? policy : originalPolicy,
432
+ declared: original.control
433
+ };
434
+ }
435
+ function collectionControlPolicy(contract, collection) {
436
+ return collectionControlOf(contract, null, collection).policy;
437
+ }
438
+ /** Why MongoDB would refuse a collection name, if it would. */
439
+ function invalidCollectionNameReason(name) {
440
+ if (name === "") return "it is empty";
441
+ if (name.includes("$")) return "it contains \"$\"";
442
+ if (name.includes("\0")) return "it contains a null character";
443
+ if (name.startsWith("system.")) return "it starts with \"system.\", which MongoDB reserves";
444
+ }
445
+ function isEmbedRelation(relation) {
446
+ return !("on" in relation) && !("through" in relation);
447
+ }
448
+ function storedField(model, field) {
449
+ if (model === void 0) return { kind: "absent" };
450
+ if (Object.hasOwn(model.fields, field)) return { kind: "field" };
451
+ const relation = Object.hasOwn(model.relations, field) ? model.relations[field] : void 0;
452
+ if (relation === void 0) return { kind: "absent" };
453
+ return isEmbedRelation(relation) ? { kind: "embed" } : { kind: "reference" };
454
+ }
455
+ function refused(statement, label, reason, why) {
456
+ return {
457
+ kind: "statementRefused",
458
+ summary: `${label}: ${reason}`,
459
+ why,
460
+ refusedStatement: statement
461
+ };
462
+ }
463
+ const LEAVE_OUT = "Leave out this statement.";
464
+ const SEPARATE_THE_MODELS = "move the models to separate collections, or carry out the change in a dataTransform operation of a migration.ts, with a filter that selects only their documents";
465
+ /**
466
+ * The discriminator field and value a variant's filter selects, or `undefined` for a filter that selects by anything else, or no filter.
467
+ */
468
+ function discriminatorValueOf(filter) {
469
+ const entries = Object.entries(filter ?? {});
470
+ if (entries.length !== 1) return void 0;
471
+ const [entry] = entries;
472
+ if (entry === void 0) return void 0;
473
+ const [field, value] = entry;
474
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? {
475
+ field,
476
+ value
477
+ } : void 0;
478
+ }
479
+ /**
480
+ * The other models stored in `collection`, root models and their variants, whose documents a change of `storedName` with `filter` reaches, and which store the field themselves or through their base. No discriminator tells the documents of several root models apart, so without a filter, or with a base model's filter, every other root model's documents are reached. A variant's filter, one discriminator value, reaches a model only when its documents can hold that value: a variant of another root model with the same discriminator field and value, or a model that declares the discriminator field itself.
481
+ */
482
+ function otherModelsReached(contract, model, collection, filter, storedName) {
483
+ const models = mongoModelsOf(contract.domain, model.namespaceId);
484
+ const baseOf = (name) => {
485
+ const base = models[name]?.base;
486
+ return base === void 0 ? void 0 : models[base.model];
487
+ };
488
+ const storesOrInherits = (name, storedName) => modelAndBasesOf(models, name).some((each) => ownStores(each, storedName));
489
+ const selected = discriminatorValueOf(filter);
490
+ const canHold = (name) => {
491
+ if (selected === void 0) return true;
492
+ const base = baseOf(name);
493
+ const discriminator = base?.discriminator?.field;
494
+ if (base !== void 0 && discriminator !== void 0 && ownStoredNameOf(base, discriminator) === selected.field) return Object.entries(base.variants ?? {}).some(([value, variant]) => variant.model === name && value === selected.value);
495
+ return storesOrInherits(name, selected.field);
496
+ };
497
+ const subjectRoot = modelAt(contract, model)?.base?.model ?? model.model;
498
+ return (collectionRootModelsOf(models)[collection] ?? []).filter((name) => name !== subjectRoot).flatMap((name) => [name, ...Object.values(models[name]?.variants ?? {}).map((variant) => variant.model)]).filter((name) => canHold(name) && storesOrInherits(name, storedName));
499
+ }
500
+ function sharedCollectionWhy(collection, others, change) {
501
+ const names = others.map((name) => JSON.stringify(name)).join(", ");
502
+ return `Collection ${JSON.stringify(collection)} stores several models and no discriminator tells their documents apart, so ${change} of ${names} too.`;
503
+ }
504
+ /**
505
+ * A removal the plan cannot carry out because another root model stored in the same collection still stores the field, and no discriminator selects the documents of the model that loses it.
506
+ */
507
+ function sharedCollectionRemovalRefusals(removals, contract) {
508
+ return removals.flatMap((removal) => {
509
+ const { subject, call } = removal;
510
+ if (subject.kind !== "field") return [];
511
+ const others = otherModelsReached(contract, subject, call.collection, call.filter, call.field);
512
+ if (others.length === 0) return [];
513
+ return [{
514
+ kind: "fieldRemovalRefused",
515
+ summary: `Cannot remove field ${JSON.stringify(call.field)} of model ${JSON.stringify(subject.model)} from collection ${JSON.stringify(call.collection)}: model ${others.map((name) => JSON.stringify(name)).join(", ")} stores it in the same collection`,
516
+ why: `${sharedCollectionWhy(call.collection, others, "removing the field would remove it from the documents")} To keep it in ${others.map((name) => JSON.stringify(name)).join(", ")} documents only, ${SEPARATE_THE_MODELS}.`
517
+ }];
518
+ });
519
+ }
520
+ const TIME_SERIES_UPDATES = "MongoDB updates the documents of a time-series collection only through its metaField";
521
+ /**
522
+ * A removal the plan cannot carry out because its collection is a time-series collection, whose
523
+ * documents MongoDB does not let an update change except through the metaField.
524
+ */
525
+ function timeSeriesRemovalRefusals(removals, schema) {
526
+ return removals.filter((removal) => schema.collection(removal.call.collection)?.options?.timeseries !== void 0).map((removal) => ({
527
+ kind: "fieldRemovalRefused",
528
+ summary: `Cannot remove field ${JSON.stringify(removal.call.field)} from collection ${JSON.stringify(removal.call.collection)}: it is a time-series collection`,
529
+ why: `${TIME_SERIES_UPDATES}, so the field cannot be removed from the documents that hold it. Keep the field in the contract, as an optional field if new documents leave it out, then run this command again.`
530
+ }));
531
+ }
532
+ /**
533
+ * Plans the rename each statement states, in order: a model statement whose collection changes renames the
534
+ * collection in the schema the diff then starts from, and a field statement whose stored name
535
+ * changes renames the field in the documents that hold it.
536
+ */
537
+ function planStatements(input) {
538
+ let schema = input.schema;
539
+ const collectionRenames = [];
540
+ const fieldRenames = [];
541
+ const callOfStatement = [];
542
+ const conflicts = [];
543
+ const renamedCollections = /* @__PURE__ */ new Map();
544
+ for (const statement of input.statements) {
545
+ const label = `Cannot ${describeMigrationStatement(statement, input.fromContract, input.contract)}`;
546
+ const outcome = statement.entity === "namespace" ? {
547
+ kind: "refused",
548
+ conflict: refused(statement, label, "MongoDB does not rename namespaces in this release", LEAVE_OUT)
549
+ } : statement.entity === "model" ? planModelStatement(statement, label, schema, input) : planFieldStatement(statement, label, renamedCollections, schema, input);
550
+ if (outcome.kind === "refused") {
551
+ conflicts.push(outcome.conflict);
552
+ callOfStatement.push(void 0);
553
+ continue;
554
+ }
555
+ if (outcome.kind === "renameCollection") {
556
+ schema = renameInSchema(schema, outcome.call.from, outcome.call.to);
557
+ renamedCollections.set(outcome.call.from, outcome.call.to);
558
+ collectionRenames.push(outcome.call);
559
+ }
560
+ if (outcome.kind === "renameField") {
561
+ const same = fieldRenames.find((call) => sameFieldRename(call, outcome.call));
562
+ if (same !== void 0) {
563
+ callOfStatement.push(same);
564
+ continue;
565
+ }
566
+ fieldRenames.push(outcome.call);
567
+ }
568
+ callOfStatement.push(outcome.kind === "unchanged" ? void 0 : outcome.call);
569
+ }
570
+ return conflicts.length > 0 ? notOk(conflicts) : ok({
571
+ schema,
572
+ collectionRenames,
573
+ fieldRenames,
574
+ callOfStatement
575
+ });
576
+ }
577
+ /** Whether two field renames rewrite the same documents the same way, as when every root model of a shared collection renames a field together. */
578
+ function sameFieldRename(a, b) {
579
+ return a.collection === b.collection && a.from === b.from && a.to === b.to && JSON.stringify(a.filter) === JSON.stringify(b.filter);
580
+ }
581
+ function renameInSchema(schema, from, to) {
582
+ return new MongoSchemaIR(schema.collections.map((collection) => collection.name === from ? new MongoSchemaCollection({
583
+ name: to,
584
+ indexes: collection.indexes,
585
+ ...collection.validator === void 0 ? {} : { validator: collection.validator },
586
+ ...collection.options === void 0 ? {} : { options: collection.options }
587
+ }) : collection));
588
+ }
589
+ function controlPolicyRefusal(statement, label, contract, collection) {
590
+ const policy = collectionControlPolicy(contract, collection);
591
+ if (policy === "managed") return void 0;
592
+ return {
593
+ kind: "refused",
594
+ conflict: refused(statement, label, `collection "${collection}" has control policy "${policy}"`, `Statements rename only collections, and fields of collections, whose control policy is "managed". Make the change in the database yourself and leave out this statement.`)
595
+ };
596
+ }
597
+ /**
598
+ * The reason and advice when a statement renames a variant whose discriminator value changes: its
599
+ * documents keep the old value, which the new validator does not accept.
600
+ */
601
+ function discriminatorValueChange(statement, input) {
602
+ const before = variantFilter(input.fromContract, statement.from);
603
+ const after = variantFilter(input.contract, statement.to);
604
+ const base = modelAt(input.contract, statement.to)?.base;
605
+ if (before === void 0 || after === void 0 || base === void 0) return void 0;
606
+ const [field, oldValue] = Object.entries(before)[0] ?? [];
607
+ const newValue = Object.values(after)[0];
608
+ if (field === void 0 || oldValue === newValue) return void 0;
609
+ const fromName = modelDisplayName(statement.from);
610
+ const toName = modelDisplayName(statement.to);
611
+ return [`its discriminator value changes from ${JSON.stringify(oldValue)} to ${JSON.stringify(newValue)}`, `The documents of "${fromName}" hold ${field} ${JSON.stringify(oldValue)}, and this release does not rewrite a discriminator value, so they would no longer match "${toName}" and would fail validation. Keep the value: declare "${toName}" with @@base(${base.model}, ${JSON.stringify(oldValue)}). A later release changes a stored value with --convert.`];
612
+ }
613
+ function planModelStatement(statement, label, schema, input) {
614
+ const discriminatorChange = discriminatorValueChange(statement, input);
615
+ if (discriminatorChange !== void 0) return {
616
+ kind: "refused",
617
+ conflict: refused(statement, label, ...discriminatorChange)
618
+ };
619
+ const from = collectionOf(input.fromContract, statement.from);
620
+ const to = collectionOf(input.contract, statement.to);
621
+ if (from === to) return { kind: "unchanged" };
622
+ const policyRefusal = controlPolicyRefusal(statement, label, input.contract, to);
623
+ if (policyRefusal !== void 0) return policyRefusal;
624
+ const refuse = (reason, why) => ({
625
+ kind: "refused",
626
+ conflict: refused(statement, label, reason, why)
627
+ });
628
+ const source = schema.collection(from);
629
+ if (source === void 0) return refuse(`the schema being planned from has no collection "${from}"`, `The database has no collection "${from}", or "${from}" is a view, which MongoDB cannot rename. Inspect it with prisma db schema, or ${LEAVE_OUT.toLowerCase()}`);
630
+ if (source.options?.timeseries !== void 0) return refuse(`collection "${from}" is a time-series collection`, `MongoDB cannot rename a time-series collection. Leave out this statement. To keep the documents, first create collection "${to}" yourself with the same time-series options and copy them into it; the plan then drops "${from}" once --delete names the model.`);
631
+ const invalid = invalidCollectionNameReason(to);
632
+ if (invalid !== void 0) return refuse(`MongoDB does not accept the collection name "${to}": ${invalid}`, "Give the model a collection name MongoDB accepts with @@map, then run this command again.");
633
+ if (schema.collection(to) !== void 0) return refuse(`the schema being planned from already has a collection "${to}"`, `A rename cannot replace a collection that already exists. Rename or drop collection "${to}" first, or ${LEAVE_OUT.toLowerCase()}`);
634
+ return {
635
+ kind: "renameCollection",
636
+ call: new RenameCollectionCall(from, to)
637
+ };
638
+ }
639
+ function planFieldStatement(statement, label, renamedCollections, schema, input) {
640
+ const refuse = (reason, why) => ({
641
+ kind: "refused",
642
+ conflict: refused(statement, label, reason, why)
643
+ });
644
+ const fromField = storedField(modelAt(input.fromContract, statement.from), statement.from.field);
645
+ const toField = storedField(modelAt(input.contract, statement.to), statement.to.field);
646
+ if (fromField.kind === "embed" || toField.kind === "embed") return refuse("the field is an embedded relation", `Renaming an embedded relation is not supported in this release. ${LEAVE_OUT}`);
647
+ if (fromField.kind !== "field" && toField.kind !== "field") return { kind: "unchanged" };
648
+ if (fromField.kind !== "field" || toField.kind !== "field") return refuse("the field is stored in documents on one side of the statement only", `A relation is not stored in documents, so a statement cannot rename a field to or from one. ${LEAVE_OUT}`);
649
+ const fromStored = resolvedStoredNameAt(input.fromContract, statement.from);
650
+ const toStored = resolvedStoredNameAt(input.contract, statement.to);
651
+ const originCollection = collectionOf(input.fromContract, statement.from);
652
+ const collection = renamedCollections.get(originCollection) ?? originCollection;
653
+ const destinationCollection = collectionOf(input.contract, statement.to);
654
+ if (fromStored === toStored && collection === destinationCollection) return { kind: "unchanged" };
655
+ if (fromStored === "_id" || toStored === "_id") return refuse("it renames _id", `MongoDB does not let a document change its _id. ${LEAVE_OUT}`);
656
+ const unsafeFrom = unrewritableFieldNameReason(fromStored);
657
+ if (unsafeFrom !== void 0) return refuse(`the stored name "${fromStored}" cannot be rewritten`, `${unsafeFrom}, so a statement cannot move its values. Rename it in the database yourself, or ${LEAVE_OUT.toLowerCase()}`);
658
+ const unsafeTo = unrewritableFieldNameReason(toStored);
659
+ if (unsafeTo !== void 0) return refuse(`the stored name "${toStored}" cannot be written`, `${unsafeTo}. Give the field a stored name without "." that does not start with "$" (with @map), or ${LEAVE_OUT.toLowerCase()}`);
660
+ if (collection !== destinationCollection) return refuse(`the model's collection changes from "${originCollection}" to "${destinationCollection}", and no statement renames it`, collectionChangeAdvice(statement, originCollection, destinationCollection));
661
+ if (schema.collection(collection)?.options?.timeseries !== void 0) return refuse(`collection "${collection}" is a time-series collection`, `${TIME_SERIES_UPDATES}, so a statement cannot move the values of a field in them. Keep the stored name: store the field as ${JSON.stringify(fromStored)} (with @map(${JSON.stringify(fromStored)}) in PSL), then run this command again without this statement.`);
662
+ const policyRefusal = controlPolicyRefusal(statement, label, input.contract, collection);
663
+ if (policyRefusal !== void 0) return policyRefusal;
664
+ const variant = variantFilter(input.fromContract, statement.from);
665
+ const renamedTogether = new Set(input.statements.flatMap((other) => other.entity === "field" && other.from.namespaceId === statement.from.namespaceId && storedFieldNameAt(input.fromContract, other.from) === fromStored && storedFieldNameAt(input.contract, other.to) === toStored ? [other.from.model] : []));
666
+ const others = otherModelsReached(input.fromContract, statement.from, originCollection, variant, fromStored).filter((name) => !renamedTogether.has(name));
667
+ if (others.length > 0) return refuse(`model ${others.map((name) => JSON.stringify(name)).join(", ")} stores field ${JSON.stringify(fromStored)} in the same collection, ${JSON.stringify(collection)}`, `${sharedCollectionWhy(collection, others, "renaming the field would rename it in the documents")} To change it, ${SEPARATE_THE_MODELS}.`);
668
+ return {
669
+ kind: "renameField",
670
+ call: new RenameFieldCall(collection, fromStored, toStored, variant ?? rootDocumentsLosing(input.contract, statement.to, fromStored)?.filter)
671
+ };
672
+ }
673
+ function collectionChangeAdvice(statement, from, to) {
674
+ if (!sameModel(statement.from, statement.to)) return `Add --rename ${modelDisplayName(statement.from)}:${modelDisplayName(statement.to)} before this statement, so the collection is renamed with its documents.`;
675
+ return `A statement cannot rename the collection of a model that keeps its name. Rename the collection in a step of its own first: change only @@map and emit the contract. ${renameCollectionByHandSteps(statement.from, from, to)} Then change the field and run this statement again.`;
676
+ }
677
+ function renameCollectionByHandSteps(model, from, to) {
678
+ return `With db update, rename the collection in the database yourself, for example db.getCollection(${JSON.stringify(from)}).renameCollection(${JSON.stringify(to)}) in mongosh, then run db update. With migration plan, plan it, passing --delete ${modelDisplayName(model)} to answer its question; in its migration.ts make renameCollection(${JSON.stringify(from)}, ${JSON.stringify(to)}) the first operation, imported with the other operations, delete the drop of "${from}", and delete the create of "${to}" if there is one; run node on that migration.ts so it writes ops.json again; then apply it with prisma db migrate.`;
679
+ }
680
+ /**
681
+ * How to keep the documents of a model that keeps its name while its collection changes from
682
+ * `from` to `to`: no statement can rename that collection, so the user renames it by hand.
683
+ */
684
+ function keepCollectionByHandAdvice(model, from, to) {
685
+ return `To keep its documents, rename collection "${from}" to "${to}" instead; a statement cannot rename the collection of a model that keeps its name. ${renameCollectionByHandSteps(model, from, to)}`;
686
+ }
687
+ function modelCoordinates(contract) {
688
+ return Object.entries(contract.domain?.namespaces ?? {}).flatMap(([namespaceId, namespace]) => Object.keys(namespace.models).map((model) => ({
689
+ namespaceId: asNamespaceId(namespaceId),
690
+ model
691
+ })));
692
+ }
693
+ function sameModel(a, b) {
694
+ return a.namespaceId === b.namespaceId && a.model === b.model;
695
+ }
696
+ /**
697
+ * The destination model an origin model became: the one a statement renamed it to, else the one of
698
+ * the same name, else the only one stored the same way (same collection, same variant value).
699
+ */
700
+ function destinationModelOf(origin, input) {
701
+ const renamed = input.statements.find((statement) => statement.entity === "model" && sameModel(statement.from, origin));
702
+ if (renamed !== void 0) return renamed.to;
703
+ if (modelAt(input.contract, origin) !== void 0) return origin;
704
+ const collection = input.collectionAfterRenames(collectionOf(input.fromContract, origin));
705
+ const filter = JSON.stringify(variantFilter(input.fromContract, origin) ?? null);
706
+ const stored = modelCoordinates(input.contract).filter((candidate) => collectionOf(input.contract, candidate) === collection && JSON.stringify(variantFilter(input.contract, candidate) ?? null) === filter);
707
+ const [only] = stored;
708
+ return stored.length === 1 ? only : void 0;
709
+ }
710
+ function renamedByStatement(input, field) {
711
+ return input.statements.some((statement) => statement.entity === "field" && sameModel(statement.from, field) && statement.from.field === field.field);
712
+ }
713
+ /** Whether one of `model`'s own fields or embed relations is stored under `storedName`. */
714
+ function ownStores(model, storedName) {
715
+ return hasOwnFieldStoredAs(model, storedName) || Object.values(ownEmbedStoredNamesOf(model)).includes(storedName);
716
+ }
717
+ /** The stored names of the documents of the model at `coordinate`: its fields and embed relations, its own and those it inherits. */
718
+ function storedFieldsOf(contract, coordinate) {
719
+ const models = mongoModelsOf(contract.domain, coordinate.namespaceId);
720
+ return new Set(modelAndBasesOf(models, coordinate.model).flatMap((model) => [...Object.values(ownStoredNamesOf(model)), ...Object.values(ownEmbedStoredNamesOf(model))]));
721
+ }
722
+ /**
723
+ * The documents of a root model that lose `field`: all of them, or, when some of its destination
724
+ * variants now store the field themselves, those whose discriminator value is none of theirs.
725
+ * `undefined` when the root model itself still stores it.
726
+ */
727
+ function rootDocumentsLosing(contract, root, storedName) {
728
+ const model = modelAt(contract, root);
729
+ if (model === void 0 || ownStores(model, storedName)) return void 0;
730
+ const discriminator = model.discriminator?.field;
731
+ const keeping = Object.entries(model.variants ?? {}).filter(([, variant]) => {
732
+ const variantModel = modelAt(contract, {
733
+ namespaceId: variant.namespace,
734
+ model: variant.model
735
+ });
736
+ return variantModel !== void 0 && ownStores(variantModel, storedName);
737
+ }).map(([value]) => value).sort();
738
+ if (discriminator === void 0 || keeping.length === 0) return { filter: void 0 };
739
+ return { filter: { [ownStoredNameOf(model, discriminator)]: { $nin: keeping } } };
740
+ }
741
+ /**
742
+ * Each field an origin model stores that its destination model no longer stores, in a collection
743
+ * the plan keeps: removing it is data loss, carried out by unsetting it from the documents that
744
+ * hold it, in a collection whose control policy is `managed`. A variant's field is unset only from that variant's documents, unless its destination
745
+ * base now stores it. A base field is unset from the documents of every variant that does not now
746
+ * store it itself.
747
+ */
748
+ function planFieldRemovals(input) {
749
+ return modelCoordinates(input.fromContract).flatMap((origin) => {
750
+ const collection = input.collectionAfterRenames(collectionOf(input.fromContract, origin));
751
+ if (!input.keptCollections.has(collection)) return [];
752
+ if (collectionControlPolicy(input.contract, collection) !== "managed") return [];
753
+ const destination = destinationModelOf(origin, input);
754
+ if (destination === void 0) return [];
755
+ const model = modelAt(input.fromContract, origin);
756
+ if (model === void 0) return [];
757
+ const variant = variantFilter(input.fromContract, origin);
758
+ const losing = (storedName) => {
759
+ if (variant !== void 0) return storedFieldsOf(input.contract, destination).has(storedName) ? void 0 : { filter: variant };
760
+ return rootDocumentsLosing(input.contract, destination, storedName);
761
+ };
762
+ return Object.entries(ownStoredNamesOf(model)).filter(([field, storedName]) => unrewritableFieldNameReason(storedName) === void 0 && !renamedByStatement(input, {
763
+ ...origin,
764
+ field
765
+ })).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).flatMap(([field, storedName]) => {
766
+ const lost = losing(storedName);
767
+ return lost === void 0 ? [] : [{
768
+ call: new UnsetFieldCall(collection, storedName, lost.filter),
769
+ subject: {
770
+ kind: "field",
771
+ ...origin,
772
+ field
773
+ }
774
+ }];
775
+ });
776
+ });
777
+ }
778
+ /**
779
+ * Without an origin contract, a field is known removed when the origin validator declares it for a
780
+ * root model and the destination validator declares it nowhere. It is named by its storage name.
781
+ */
782
+ function planValidatorFieldRemovals(input) {
783
+ return input.origin.collections.flatMap((originCollection) => {
784
+ if (collectionControlPolicy(input.contract, originCollection.name) !== "managed") return [];
785
+ const destination = input.destination.collection(originCollection.name);
786
+ if (destination?.validator === void 0 || originCollection.validator === void 0) return [];
787
+ const kept = validatorFieldNamesOf(destination.validator.jsonSchema);
788
+ return [...new Set(validatorRootShapesOf(originCollection.validator.jsonSchema).flatMap((root) => Object.keys(root.properties)))].filter((field) => unrewritableFieldNameReason(field) === void 0 && !kept.has(field)).sort().map((field) => ({
789
+ call: new UnsetFieldCall(originCollection.name, field),
790
+ subject: {
791
+ kind: "storage",
792
+ name: `${originCollection.name}.${field}`
793
+ }
794
+ }));
795
+ });
796
+ }
797
+ function renderOps(calls) {
798
+ return calls.map((call) => call.toOp());
799
+ }
800
+ /**
801
+ * Always-present base imports for the rendered scaffold, both from the
802
+ * target's own `migration` entry:
803
+ *
804
+ * - `Migration` — the user-facing Mongo `Migration` base, forwarded from
805
+ * `@internal/family-mongo`; subclasses don't need to redeclare
806
+ * `targetId` or thread family/target generics.
807
+ * - `MigrationCLI` — the migration-file CLI entrypoint that loads
808
+ * `prisma.config.ts`, assembles a `ControlStack`, and instantiates
809
+ * the migration class. The migration file owns this dependency directly:
810
+ * pulling CLI machinery in at script run time is acceptable because the
811
+ * script's whole purpose is to be invoked from the project that owns the
812
+ * config.
813
+ *
814
+ * Naming one package rather than three is what lets an application that
815
+ * installed only `@prisma/orm-mongo` run a scaffolded migration: emitted
816
+ * code may name only a direct dependency (ADR 242), and the facade carries
817
+ * the target's `migration` entry but neither the family base nor the CLI.
818
+ */
819
+ const BASE_IMPORTS = [{
820
+ moduleSpecifier: TARGET_MIGRATION_MODULE,
821
+ symbol: "Migration"
822
+ }, {
823
+ moduleSpecifier: TARGET_MIGRATION_MODULE,
824
+ symbol: "MigrationCLI"
825
+ }];
826
+ /**
827
+ * Render a list of Mongo `OpFactoryCall`s as a `migration.ts` source string.
828
+ * The result is shebanged, imports the contract JSON from the shared
829
+ * snapshot store (the destination contract, plus the source contract for a
830
+ * non-baseline migration), extends `Migration<Start, End>` (or
831
+ * `Migration<never, End>` for a baseline) from
832
+ * `@internal/target-mongo/migration`,
833
+ * assigns the JSON to `endContractJson` / `startContractJson`, and implements
834
+ * `operations`. The `Migration` base derives `describe()` from those fields.
835
+ *
836
+ * The walk is polymorphic: each call node contributes its own
837
+ * `renderTypeScript()` expression and declares its own `importRequirements()`.
838
+ * The top-level renderer aggregates imports across all nodes and emits one
839
+ * `import { … } from "…"` line per module. The `Migration` / `MigrationCLI`
840
+ * base imports and the contract-JSON imports are always emitted, independent
841
+ * of the call nodes.
842
+ */
843
+ function renderCallsToTypeScript(calls, meta) {
844
+ const imports = buildImports(calls, meta);
845
+ const operationsBody = calls.map((c) => c.renderTypeScript()).join(",\n");
846
+ const hasStart = meta.from !== null;
847
+ const startField = hasStart ? [" override readonly startContractJson = startContract;"] : [];
848
+ return [
849
+ shebangLineFor(detectScaffoldRuntime()),
850
+ imports,
851
+ "",
852
+ `class M extends Migration<${hasStart ? "Start" : "never"}, End> {`,
853
+ ...startField,
854
+ " override readonly endContractJson = endContract;",
855
+ "",
856
+ " override get operations() {",
857
+ " return [",
858
+ indent(operationsBody, 6),
859
+ " ];",
860
+ " }",
861
+ "}",
862
+ "",
863
+ "export default M;",
864
+ "MigrationCLI.run(import.meta.url, M);",
865
+ ""
866
+ ].join("\n");
867
+ }
868
+ function buildImports(calls, meta) {
869
+ const requirements = [...BASE_IMPORTS, ...contractImports(meta)];
870
+ for (const call of calls) for (const req of call.importRequirements()) requirements.push(req);
871
+ return renderImports(resolveRequirementSpecifiers(requirements, meta.resolveImportSpecifier));
872
+ }
873
+ /**
874
+ * The committed contract-JSON imports the scaffold reads its from/to identity
875
+ * from, resolved to the deduplicated snapshot store under
876
+ * `meta.snapshotsImportPath`. The end snapshot is always present; the start
877
+ * snapshot is added only for a non-baseline migration (`meta.from !== null`).
878
+ * The matching `Contract` type imports (aliased `Start`/`End`) feed the
879
+ * `Migration<Start, End>` generics. Baseline emits `Migration<never, End>` with
880
+ * no start imports — `never` is the honest "no prior contract" Start.
881
+ */
882
+ function contractImports(meta) {
883
+ const reqs = [{
884
+ moduleSpecifier: contractSnapshotJsonSpecifier(meta.snapshotsImportPath, meta.to),
885
+ symbol: "endContract",
886
+ kind: "default",
887
+ attributes: { type: "json" }
888
+ }, {
889
+ moduleSpecifier: contractSnapshotTypesSpecifier(meta.snapshotsImportPath, meta.to),
890
+ symbol: "Contract",
891
+ alias: "End",
892
+ typeOnly: true
893
+ }];
894
+ if (meta.from !== null) {
895
+ reqs.push({
896
+ moduleSpecifier: contractSnapshotJsonSpecifier(meta.snapshotsImportPath, meta.from),
897
+ symbol: "startContract",
898
+ kind: "default",
899
+ attributes: { type: "json" }
900
+ });
901
+ reqs.push({
902
+ moduleSpecifier: contractSnapshotTypesSpecifier(meta.snapshotsImportPath, meta.from),
903
+ symbol: "Contract",
904
+ alias: "Start",
905
+ typeOnly: true
906
+ });
907
+ }
908
+ return reqs;
909
+ }
910
+ function indent(text, spaces) {
911
+ const pad = " ".repeat(spaces);
912
+ return text.split("\n").map((line) => line.trim() ? `${pad}${line}` : line).join("\n");
913
+ }
914
+ /**
915
+ * Planner-produced Mongo migration, returned by `MongoMigrationPlanner.plan(...)`
916
+ * and `MongoMigrationPlanner.emptyMigration(...)`.
917
+ *
918
+ * Unlike user-authored migrations (which extend `MongoMigration` from
919
+ * `@internal/family-mongo`), this class lives inside the target and holds
920
+ * the richer authoring IR (`OpFactoryCall[]`) needed to render itself back to
921
+ * TypeScript source. It implements `MigrationPlanWithAuthoringSurface` so
922
+ * that the CLI can uniformly ask any planner result to serialize itself to a
923
+ * `migration.ts`.
924
+ *
925
+ * It extends the framework `Migration` base directly and declares its own
926
+ * `targetId` rather than inheriting `MongoMigration`, whose added value is
927
+ * the lazily-built contract views a planner result never reads.
928
+ */
929
+ var PlannerProducedMongoMigration = class extends Migration {
930
+ calls;
931
+ meta;
932
+ snapshotsImportPath;
933
+ targetId = "mongo";
934
+ constructor(calls, meta, snapshotsImportPath) {
935
+ super();
936
+ this.calls = calls;
937
+ this.meta = meta;
938
+ this.snapshotsImportPath = snapshotsImportPath;
939
+ }
940
+ get operations() {
941
+ return renderOps(this.calls);
942
+ }
943
+ describe() {
944
+ return this.meta;
945
+ }
946
+ renderTypeScript(resolveImportSpecifier) {
947
+ return renderCallsToTypeScript(this.calls, {
948
+ from: this.meta.from,
949
+ to: this.meta.to,
950
+ snapshotsImportPath: this.snapshotsImportPath,
951
+ resolveImportSpecifier
952
+ });
953
+ }
954
+ };
955
+ function validatorsEqual(a, b) {
956
+ if (!a && !b) return true;
957
+ if (!a || !b) return false;
958
+ return a.validationLevel === b.validationLevel && a.validationAction === b.validationAction && canonicalize(a.jsonSchema) === canonicalize(b.jsonSchema);
959
+ }
960
+ function isPlainObject(v) {
961
+ return typeof v === "object" && v !== null && !Array.isArray(v);
962
+ }
963
+ /** Each field a root model of the validator declares, with the distinct schemas the root models give it, in canonical form. */
964
+ function propertiesOf(schema) {
965
+ const schemas = /* @__PURE__ */ new Map();
966
+ for (const root of validatorRootShapesOf(schema)) for (const [field, fieldSchema] of Object.entries(root.properties)) schemas.set(field, (schemas.get(field) ?? /* @__PURE__ */ new Set()).add(canonicalize(fieldSchema)));
967
+ return Object.fromEntries([...schemas].map(([field, distinct]) => [field, [...distinct].sort().join("|")]));
968
+ }
969
+ /**
970
+ * A label naming the properties whose schema changed, so a dry run shows what a validator update does: `Update validator on items (changed: meta; added: note)`.
971
+ */
972
+ function validatorUpdateLabel(collName, origin, dest) {
973
+ const originProps = propertiesOf(origin.jsonSchema);
974
+ const destProps = propertiesOf(dest.jsonSchema);
975
+ const changed = Object.keys(destProps).filter((field) => Object.hasOwn(originProps, field) && originProps[field] !== destProps[field]);
976
+ const added = Object.keys(destProps).filter((field) => !Object.hasOwn(originProps, field));
977
+ const removed = Object.keys(originProps).filter((field) => !Object.hasOwn(destProps, field));
978
+ const parts = [
979
+ ...changed.length > 0 ? [`changed: ${changed.join(", ")}`] : [],
980
+ ...added.length > 0 ? [`added: ${added.join(", ")}`] : [],
981
+ ...removed.length > 0 ? [`removed: ${removed.join(", ")}`] : []
982
+ ];
983
+ return parts.length === 0 ? `Update validator on ${collName}` : `Update validator on ${collName} (${parts.join("; ")})`;
984
+ }
985
+ /**
986
+ * The keys of an option MongoDB fills in when a collection is created without them, each with a
987
+ * test for the value it fills in. `listCollections` reports a time-series collection's granularity
988
+ * as "seconds" and its bucketMaxSpanSeconds when they were not given, and bucketRoundingSeconds only
989
+ * for custom bucketing, which a contract cannot state; and a clustered index's v, key and unique, and
990
+ * its name as "_id_".
991
+ */
992
+ const SERVER_FILLED = {
993
+ timeseries: {
994
+ granularity: (value) => value === "seconds",
995
+ bucketMaxSpanSeconds: () => true,
996
+ bucketRoundingSeconds: () => true
997
+ },
998
+ clusteredIndex: {
999
+ v: () => true,
1000
+ key: () => true,
1001
+ unique: () => true,
1002
+ name: (value) => value === "_id_"
1003
+ }
1004
+ };
1005
+ /** The origin's value of an option without the keys the server filled in and the destination leaves out. */
1006
+ function withoutServerFilled(option, origin, dest) {
1007
+ if (!isPlainObject(origin)) return origin;
1008
+ const filled = SERVER_FILLED[option];
1009
+ return Object.fromEntries(Object.entries(origin).filter(([key, value]) => isPlainObject(dest) && Object.hasOwn(dest, key) || !filled[key]?.(value)));
1010
+ }
1011
+ function hasImmutableOptionChange(origin, dest) {
1012
+ if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
1013
+ if (canonicalize(withoutServerFilled("timeseries", origin?.timeseries, dest?.timeseries)) !== canonicalize(dest?.timeseries)) return "timeseries";
1014
+ if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
1015
+ if (canonicalize(withoutServerFilled("clusteredIndex", origin?.clusteredIndex, dest?.clusteredIndex)) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
1016
+ }
1017
+ function collectionHasOptions(coll) {
1018
+ return !!(coll.options || coll.validator);
1019
+ }
1020
+ /**
1021
+ * The model of `fromContract` that stores its documents in `collection`, if any: the root model
1022
+ * when variants share the collection with it.
1023
+ */
1024
+ function modelStoredIn(fromContract, collection) {
1025
+ const storing = Object.entries(fromContract.domain.namespaces).flatMap(([namespaceId, namespace]) => Object.entries(namespace.models).flatMap(([model, definition]) => {
1026
+ const coordinate = {
1027
+ namespaceId: asNamespaceId(namespaceId),
1028
+ model
1029
+ };
1030
+ return collectionOf(fromContract, coordinate) === collection ? [{
1031
+ coordinate,
1032
+ isRoot: definition.base === void 0
1033
+ }] : [];
1034
+ }));
1035
+ return (storing.find(({ isRoot }) => isRoot) ?? storing[0])?.coordinate;
1036
+ }
1037
+ /** A convert or backfill statement, which this release cannot carry out on MongoDB. */
1038
+ function unsupportedStatementConflict(statement, fromContract, contract) {
1039
+ return {
1040
+ kind: "statementRefused",
1041
+ summary: `MongoDB does not apply ${statement.kind} statements in this release, so nothing was planned: ${describeMigrationStatement(statement, fromContract, contract)}`,
1042
+ why: `MongoDB cannot carry out ${statement.kind} statements in this release. Leave out the statement.`,
1043
+ refusedStatement: statement
1044
+ };
1045
+ }
1046
+ /**
1047
+ * Each collection drop of the plan, with the model whose documents it loses. When that model keeps
1048
+ * its name and the plan brings its new collection into existence, the drop says how to rename the
1049
+ * collection by hand instead.
1050
+ */
1051
+ function collectionDrops(calls, fromContract, contract, collections) {
1052
+ const created = (collection) => collections.origin.collection(collection) === void 0 && collections.destination.collection(collection) !== void 0;
1053
+ return calls.flatMap((call, operationIndex) => {
1054
+ if (!(call instanceof DropCollectionCall)) return [];
1055
+ const model = fromContract === null ? void 0 : modelStoredIn(fromContract, call.collection);
1056
+ const lost = {
1057
+ operationIndex,
1058
+ loss: "drop",
1059
+ label: call.label
1060
+ };
1061
+ if (model === void 0) return [{
1062
+ ...lost,
1063
+ subject: {
1064
+ kind: "storage",
1065
+ name: call.collection
1066
+ }
1067
+ }];
1068
+ const subject = {
1069
+ kind: "model",
1070
+ ...model
1071
+ };
1072
+ const destinationCollection = modelStoredInDestination(contract, model);
1073
+ return [destinationCollection !== void 0 && created(destinationCollection) ? {
1074
+ ...lost,
1075
+ subject,
1076
+ keepDataAdvice: keepCollectionByHandAdvice(model, call.collection, destinationCollection)
1077
+ } : {
1078
+ ...lost,
1079
+ subject
1080
+ }];
1081
+ });
1082
+ }
1083
+ /** The collection the destination stores `model` in, when the destination still has that model. */
1084
+ function modelStoredInDestination(contract, model) {
1085
+ const namespace = contract.domain.namespaces[model.namespaceId];
1086
+ return namespace !== void 0 && Object.hasOwn(namespace.models, model.model) ? collectionOf(contract, model) : void 0;
1087
+ }
1088
+ function callsOf(diff) {
1089
+ return [
1090
+ ...diff.collCreates,
1091
+ ...diff.drops,
1092
+ ...diff.creates,
1093
+ ...diff.validatorOps,
1094
+ ...diff.mutableOptionOps,
1095
+ ...diff.collDrops
1096
+ ];
1097
+ }
1098
+ function planCollectionDiff(collName, originColl, destColl) {
1099
+ const collCreates = [];
1100
+ const drops = [];
1101
+ const creates = [];
1102
+ const validatorOps = [];
1103
+ const mutableOptionOps = [];
1104
+ const collDrops = [];
1105
+ const conflicts = [];
1106
+ if (!originColl) {
1107
+ if (destColl && (collectionHasOptions(destColl) || destColl.indexes.length === 0)) {
1108
+ const opts = collectionHasOptions(destColl) ? schemaCollectionToCreateCollectionOptions(destColl) : void 0;
1109
+ collCreates.push(new CreateCollectionCall(collName, opts));
1110
+ }
1111
+ } else if (!destColl) collDrops.push(new DropCollectionCall(collName));
1112
+ else {
1113
+ const immutableChange = hasImmutableOptionChange(originColl.options, destColl.options);
1114
+ if (immutableChange) conflicts.push({
1115
+ kind: "policy-violation",
1116
+ summary: `Cannot change immutable collection option '${immutableChange}' on ${collName}`,
1117
+ why: `MongoDB does not support modifying the '${immutableChange}' option after collection creation`
1118
+ });
1119
+ const mutableCall = planMutableOptionsDiffCall(collName, originColl.options, destColl.options);
1120
+ if (mutableCall) mutableOptionOps.push(mutableCall);
1121
+ const validatorCall = planValidatorDiffCall(collName, originColl.validator, destColl.validator);
1122
+ if (validatorCall) validatorOps.push(validatorCall);
1123
+ }
1124
+ const originLookup = /* @__PURE__ */ new Map();
1125
+ if (originColl) for (const idx of originColl.indexes) originLookup.set(indexLookupKey(idx), idx);
1126
+ const destLookup = /* @__PURE__ */ new Map();
1127
+ if (destColl) for (const idx of destColl.indexes) destLookup.set(indexLookupKey(idx), idx);
1128
+ for (const [lookupKey, idx] of originLookup) if (!destLookup.has(lookupKey)) drops.push(new DropIndexCall(collName, idx.keys));
1129
+ for (const [lookupKey, idx] of destLookup) if (!originLookup.has(lookupKey)) creates.push(new CreateIndexCall(collName, idx.keys, schemaIndexToCreateIndexOptions(idx)));
1130
+ return {
1131
+ collCreates,
1132
+ drops,
1133
+ creates,
1134
+ validatorOps,
1135
+ mutableOptionOps,
1136
+ collDrops,
1137
+ conflicts
1138
+ };
1139
+ }
1140
+ /**
1141
+ * The warning for a change a collection's control policy left out of the plan, worded as the Postgres planner words a suppressed call.
1142
+ */
1143
+ function suppressedCallWarning(call, collection, control) {
1144
+ const declared = control.declared !== void 0 && control.declared !== control.policy ? ` but declared '${control.declared}'` : "";
1145
+ return {
1146
+ kind: "controlPolicySuppressedCall",
1147
+ summary: `control policy suppressed: ${call.label} — collection "${collection}" has effective control '${control.policy}'${declared}`,
1148
+ meta: {
1149
+ controlPolicy: control.policy,
1150
+ ...ifDefined("declaredControlPolicy", control.declared)
1151
+ }
1152
+ };
1153
+ }
1154
+ var MongoMigrationPlanner = class {
1155
+ planCalls(options) {
1156
+ const contract = blindCast(options.contract);
1157
+ const originIR = options.statementCalls?.schema ?? blindCast(options.schema);
1158
+ const destinationIR = contractToMongoSchemaIR(contract);
1159
+ const collCreates = [];
1160
+ const drops = [];
1161
+ const creates = [];
1162
+ const validatorOps = [];
1163
+ const mutableOptionOps = [];
1164
+ const collDrops = [];
1165
+ const conflicts = [];
1166
+ const warnings = [];
1167
+ const allCollectionNames = /* @__PURE__ */ new Set([...originIR.collectionNames, ...destinationIR.collectionNames]);
1168
+ for (const collName of [...allCollectionNames].sort()) {
1169
+ const originColl = originIR.collection(collName);
1170
+ const planned = planCollectionDiff(collName, originColl, destinationIR.collection(collName));
1171
+ const control = collectionControlOf(contract, options.fromContract, collName);
1172
+ if (!plansChangesTo(control.policy, originColl !== void 0)) {
1173
+ warnings.push(...callsOf(planned).map((call) => suppressedCallWarning(call, collName, control)));
1174
+ continue;
1175
+ }
1176
+ collCreates.push(...planned.collCreates);
1177
+ drops.push(...planned.drops);
1178
+ creates.push(...planned.creates);
1179
+ validatorOps.push(...planned.validatorOps);
1180
+ mutableOptionOps.push(...planned.mutableOptionOps);
1181
+ collDrops.push(...planned.collDrops);
1182
+ conflicts.push(...planned.conflicts);
1183
+ }
1184
+ if (conflicts.length > 0) return {
1185
+ kind: "failure",
1186
+ conflicts
1187
+ };
1188
+ const allCalls = [
1189
+ ...options.statementCalls?.collectionRenames ?? [],
1190
+ ...collCreates,
1191
+ ...drops,
1192
+ ...options.statementCalls?.rewrites ?? [],
1193
+ ...creates,
1194
+ ...validatorOps,
1195
+ ...mutableOptionOps,
1196
+ ...collDrops
1197
+ ];
1198
+ for (const call of allCalls) if (!options.policy.allowedOperationClasses.includes(call.operationClass)) conflicts.push({
1199
+ kind: "policy-violation",
1200
+ summary: `${call.operationClass} operation disallowed: ${call.label}`,
1201
+ why: `Policy does not allow '${call.operationClass}' operations`,
1202
+ refusedOperationClass: call.operationClass
1203
+ });
1204
+ if (conflicts.length > 0) return {
1205
+ kind: "failure",
1206
+ conflicts
1207
+ };
1208
+ return {
1209
+ kind: "success",
1210
+ calls: allCalls,
1211
+ warnings
1212
+ };
1213
+ }
1214
+ plan(options) {
1215
+ const contract = blindCast(options.contract);
1216
+ const originIR = blindCast(options.schema);
1217
+ const fromContract = options.fromContract;
1218
+ const unsupported = options.statements.find((statement) => statement.kind !== "rename");
1219
+ if (unsupported !== void 0) return {
1220
+ kind: "failure",
1221
+ conflicts: [unsupportedStatementConflict(unsupported, fromContract ?? contract, contract)]
1222
+ };
1223
+ const renames = options.statements.filter((statement) => statement.kind === "rename");
1224
+ const statementPlan = planStatements({
1225
+ statements: renames,
1226
+ fromContract: fromContract ?? contract,
1227
+ contract,
1228
+ schema: originIR
1229
+ });
1230
+ if (!statementPlan.ok) return {
1231
+ kind: "failure",
1232
+ conflicts: statementPlan.failure
1233
+ };
1234
+ const { schema, collectionRenames, fieldRenames, callOfStatement } = statementPlan.value;
1235
+ const destinationIR = contractToMongoSchemaIR(contract);
1236
+ const renamedTo = new Map(collectionRenames.map((call) => [call.from, call.to]));
1237
+ const removals = fromContract === null ? planValidatorFieldRemovals({
1238
+ origin: schema,
1239
+ destination: destinationIR,
1240
+ contract
1241
+ }) : planFieldRemovals({
1242
+ statements: renames,
1243
+ fromContract,
1244
+ contract,
1245
+ collectionAfterRenames: (collection) => renamedTo.get(collection) ?? collection,
1246
+ keptCollections: new Set(schema.collectionNames.filter((name) => destinationIR.collection(name) !== void 0))
1247
+ });
1248
+ const removalRefusals = [...timeSeriesRemovalRefusals(removals, schema), ...sharedCollectionRemovalRefusals(removals, contract)];
1249
+ if (removalRefusals.length > 0) return {
1250
+ kind: "failure",
1251
+ conflicts: removalRefusals
1252
+ };
1253
+ const result = this.planCalls({
1254
+ ...options,
1255
+ statementCalls: {
1256
+ schema,
1257
+ collectionRenames,
1258
+ rewrites: [...fieldRenames, ...removals.map((removal) => removal.call)]
1259
+ }
1260
+ });
1261
+ if (result.kind === "failure") return result;
1262
+ const { calls, warnings } = result;
1263
+ const fieldLoss = removals.map((removal) => ({
1264
+ operationIndex: calls.indexOf(removal.call),
1265
+ subject: removal.subject,
1266
+ loss: "drop",
1267
+ label: removal.call.label
1268
+ }));
1269
+ return {
1270
+ kind: "success",
1271
+ plan: new PlannerProducedMongoMigration(calls, {
1272
+ from: options.origin?.storageHash ?? null,
1273
+ to: contract.storage.storageHash
1274
+ }, options.snapshotsImportPath),
1275
+ appliedStatements: options.statements.map((statement, index) => {
1276
+ const call = callOfStatement[index];
1277
+ return {
1278
+ statement,
1279
+ operationIndexes: call === void 0 ? [] : [calls.indexOf(call)]
1280
+ };
1281
+ }),
1282
+ dataLoss: [...collectionDrops(calls, fromContract, contract, {
1283
+ origin: schema,
1284
+ destination: destinationIR
1285
+ }), ...fieldLoss].sort((a, b) => a.operationIndex - b.operationIndex),
1286
+ accessChanges: [],
1287
+ temporaryValues: [],
1288
+ ...warnings.length > 0 ? { warnings } : {}
1289
+ };
1290
+ }
1291
+ /**
1292
+ * Produce an empty `migration.ts` authoring surface for `migration new`.
1293
+ *
1294
+ * The "empty migration" is a `PlannerProducedMongoMigration` with no
1295
+ * operations; `renderTypeScript()` emits a stub class with the correct
1296
+ * `from`/`to` metadata that the user then fills in with operations. The
1297
+ * contract path on the context is unused — Mongo's emitted source does
1298
+ * not import from the generated contract `.d.ts`.
1299
+ */
1300
+ emptyMigration(context) {
1301
+ return new PlannerProducedMongoMigration([], {
1302
+ from: context.fromHash,
1303
+ to: context.toHash
1304
+ }, context.snapshotsImportPath);
1305
+ }
1306
+ };
1307
+ function plansChangesTo(policy, collectionExists) {
1308
+ switch (policy) {
1309
+ case "managed": return true;
1310
+ case "tolerated": return !collectionExists;
1311
+ case "external":
1312
+ case "observed": return false;
1313
+ }
1314
+ }
1315
+ function planValidatorDiffCall(collName, originValidator, destValidator) {
1316
+ if (validatorsEqual(originValidator, destValidator)) return void 0;
1317
+ if (destValidator) return new CollModCall(collName, {
1318
+ validator: { $jsonSchema: destValidator.jsonSchema },
1319
+ validationLevel: destValidator.validationLevel,
1320
+ validationAction: destValidator.validationAction
1321
+ }, {
1322
+ id: `validator.${collName}.${originValidator ? "update" : "add"}`,
1323
+ label: originValidator ? validatorUpdateLabel(collName, originValidator, destValidator) : `Add validator on ${collName}`,
1324
+ operationClass: "widening"
1325
+ });
1326
+ return new CollModCall(collName, {
1327
+ validator: {},
1328
+ validationLevel: "strict",
1329
+ validationAction: "error"
1330
+ }, {
1331
+ id: `validator.${collName}.remove`,
1332
+ label: `Remove validator on ${collName}`,
1333
+ operationClass: "widening"
1334
+ });
1335
+ }
1336
+ function planMutableOptionsDiffCall(collName, origin, dest) {
1337
+ const originCSPPI = origin?.changeStreamPreAndPostImages;
1338
+ const destCSPPI = dest?.changeStreamPreAndPostImages;
1339
+ if (deepEqual(originCSPPI, destCSPPI)) return void 0;
1340
+ return new CollModCall(collName, { changeStreamPreAndPostImages: destCSPPI ?? { enabled: false } }, {
1341
+ id: `options.${collName}.update`,
1342
+ label: `Update mutable options on ${collName}`,
1343
+ operationClass: "widening"
1344
+ });
1345
+ }
1346
+ function isRecord(value) {
1347
+ return value !== null && typeof value === "object";
1348
+ }
1349
+ function getNestedField(doc, path) {
1350
+ const parts = path.split(".");
1351
+ let current = doc;
1352
+ for (const part of parts) {
1353
+ if (!isRecord(current)) return;
1354
+ const record = current;
1355
+ if (!Object.hasOwn(record, part)) return;
1356
+ current = record[part];
1357
+ }
1358
+ return blindCast(current);
1359
+ }
1360
+ function evaluateFieldOp(op, actual, expected) {
1361
+ switch (op) {
1362
+ case "$eq": return deepEqual(actual, expected);
1363
+ case "$ne": return !deepEqual(actual, expected);
1364
+ case "$gt": return typeof actual === typeof expected && blindCast(actual) > blindCast(expected);
1365
+ case "$gte": return typeof actual === typeof expected && blindCast(actual) >= blindCast(expected);
1366
+ case "$lt": return typeof actual === typeof expected && blindCast(actual) < blindCast(expected);
1367
+ case "$lte": return typeof actual === typeof expected && blindCast(actual) <= blindCast(expected);
1368
+ case "$in": return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
1369
+ default: throw mongoTargetError("MIGRATION.OPERATION_UNSUPPORTED", `Unsupported filter operator in migration check: ${op}`, { meta: { operator: op } });
1370
+ }
1371
+ }
1372
+ var FilterEvaluator = class {
1373
+ doc = {};
1374
+ evaluate(filter, doc) {
1375
+ this.doc = doc;
1376
+ return filter.accept(this);
1377
+ }
1378
+ field(expr) {
1379
+ const value = getNestedField(this.doc, expr.field);
1380
+ return evaluateFieldOp(expr.op, value, expr.value);
1381
+ }
1382
+ and(expr) {
1383
+ return expr.exprs.every((child) => child.accept(this));
1384
+ }
1385
+ or(expr) {
1386
+ return expr.exprs.some((child) => child.accept(this));
1387
+ }
1388
+ not(expr) {
1389
+ return !expr.expr.accept(this);
1390
+ }
1391
+ exists(expr) {
1392
+ const has = getNestedField(this.doc, expr.field) !== void 0;
1393
+ return expr.exists ? has : !has;
1394
+ }
1395
+ expr(_expr) {
1396
+ throw mongoTargetError("MIGRATION.OPERATION_UNSUPPORTED", "Aggregation expression filters are not supported in migration checks", { meta: { operator: "$expr" } });
1397
+ }
1398
+ };
1399
+ const IndexKeyJson = type({
1400
+ field: "string",
1401
+ direction: type("1 | -1 | \"text\" | \"2dsphere\" | \"2d\" | \"hashed\"")
1402
+ });
1403
+ const CollationJson = type({
1404
+ locale: "string",
1405
+ "caseLevel?": "boolean",
1406
+ "caseFirst?": "string",
1407
+ "strength?": "number",
1408
+ "numericOrdering?": "boolean",
1409
+ "alternate?": "string",
1410
+ "maxVariable?": "string",
1411
+ "backwards?": "boolean",
1412
+ "normalization?": "boolean"
1413
+ });
1414
+ const CreateIndexJson = type({
1415
+ kind: "\"createIndex\"",
1416
+ collection: "string",
1417
+ keys: IndexKeyJson.array().atLeastLength(1),
1418
+ "unique?": "boolean",
1419
+ "sparse?": "boolean",
1420
+ "expireAfterSeconds?": "number",
1421
+ "partialFilterExpression?": "Record<string, unknown>",
1422
+ "name?": "string",
1423
+ "wildcardProjection?": "Record<string, 0 | 1>",
1424
+ "collation?": CollationJson,
1425
+ "weights?": "Record<string, number>",
1426
+ "default_language?": "string",
1427
+ "language_override?": "string"
1428
+ });
1429
+ const DropIndexJson = type({
1430
+ kind: "\"dropIndex\"",
1431
+ collection: "string",
1432
+ name: "string"
1433
+ });
1434
+ const CreateCollectionJson = type({
1435
+ kind: "\"createCollection\"",
1436
+ collection: "string",
1437
+ "validator?": "Record<string, unknown>",
1438
+ "validationLevel?": "\"strict\" | \"moderate\"",
1439
+ "validationAction?": "\"error\" | \"warn\"",
1440
+ "capped?": "boolean",
1441
+ "size?": "number",
1442
+ "max?": "number",
1443
+ "timeseries?": {
1444
+ timeField: "string",
1445
+ "metaField?": "string",
1446
+ "granularity?": "\"seconds\" | \"minutes\" | \"hours\""
1447
+ },
1448
+ "collation?": CollationJson,
1449
+ "changeStreamPreAndPostImages?": { enabled: "boolean" },
1450
+ "clusteredIndex?": {
1451
+ key: "Record<string, number>",
1452
+ unique: "boolean",
1453
+ "name?": "string"
1454
+ }
1455
+ });
1456
+ const DropCollectionJson = type({
1457
+ kind: "\"dropCollection\"",
1458
+ collection: "string"
1459
+ });
1460
+ const RenameCollectionJson = type({
1461
+ kind: "\"renameCollection\"",
1462
+ collection: "string",
1463
+ to: "string"
1464
+ });
1465
+ const CollModJson = type({
1466
+ kind: "\"collMod\"",
1467
+ collection: "string",
1468
+ "validator?": "Record<string, unknown>",
1469
+ "validationLevel?": "\"strict\" | \"moderate\"",
1470
+ "validationAction?": "\"error\" | \"warn\"",
1471
+ "changeStreamPreAndPostImages?": { enabled: "boolean" }
1472
+ });
1473
+ const ListIndexesJson = type({
1474
+ kind: "\"listIndexes\"",
1475
+ collection: "string"
1476
+ });
1477
+ const ListCollectionsJson = type({ kind: "\"listCollections\"" });
1478
+ const FieldFilterJson = type({
1479
+ kind: "\"field\"",
1480
+ field: "string",
1481
+ op: "string",
1482
+ value: "unknown"
1483
+ });
1484
+ const AndFilterJson = type({
1485
+ kind: "\"and\"",
1486
+ exprs: "Record<string, unknown>[]"
1487
+ });
1488
+ const OrFilterJson = type({
1489
+ kind: "\"or\"",
1490
+ exprs: "Record<string, unknown>[]"
1491
+ });
1492
+ const NotFilterJson = type({
1493
+ kind: "\"not\"",
1494
+ expr: "Record<string, unknown>"
1495
+ });
1496
+ const ExistsFilterJson = type({
1497
+ kind: "\"exists\"",
1498
+ field: "string",
1499
+ exists: "boolean"
1500
+ });
1501
+ const RawInsertOneJson = type({
1502
+ kind: "\"rawInsertOne\"",
1503
+ collection: "string",
1504
+ document: "Record<string, unknown>"
1505
+ });
1506
+ const RawInsertManyJson = type({
1507
+ kind: "\"rawInsertMany\"",
1508
+ collection: "string",
1509
+ documents: "Record<string, unknown>[]"
1510
+ });
1511
+ const RawUpdateOneJson = type({
1512
+ kind: "\"rawUpdateOne\"",
1513
+ collection: "string",
1514
+ filter: "Record<string, unknown>",
1515
+ update: "Record<string, unknown> | Record<string, unknown>[]"
1516
+ });
1517
+ const RawUpdateManyJson = type({
1518
+ kind: "\"rawUpdateMany\"",
1519
+ collection: "string",
1520
+ filter: "Record<string, unknown>",
1521
+ update: "Record<string, unknown> | Record<string, unknown>[]",
1522
+ "bypassDocumentValidation?": "boolean"
1523
+ });
1524
+ const RawDeleteOneJson = type({
1525
+ kind: "\"rawDeleteOne\"",
1526
+ collection: "string",
1527
+ filter: "Record<string, unknown>"
1528
+ });
1529
+ const RawDeleteManyJson = type({
1530
+ kind: "\"rawDeleteMany\"",
1531
+ collection: "string",
1532
+ filter: "Record<string, unknown>"
1533
+ });
1534
+ const RawAggregateJson = type({
1535
+ kind: "\"rawAggregate\"",
1536
+ collection: "string",
1537
+ pipeline: "Record<string, unknown>[]"
1538
+ });
1539
+ const RawFindOneAndUpdateJson = type({
1540
+ kind: "\"rawFindOneAndUpdate\"",
1541
+ collection: "string",
1542
+ filter: "Record<string, unknown>",
1543
+ update: "Record<string, unknown> | Record<string, unknown>[]",
1544
+ upsert: "boolean"
1545
+ });
1546
+ const RawFindOneAndDeleteJson = type({
1547
+ kind: "\"rawFindOneAndDelete\"",
1548
+ collection: "string",
1549
+ filter: "Record<string, unknown>"
1550
+ });
1551
+ const TypedAggregateJson = type({
1552
+ kind: "\"aggregate\"",
1553
+ collection: "string",
1554
+ pipeline: "Record<string, unknown>[]"
1555
+ });
1556
+ const QueryPlanJson = type({
1557
+ collection: "string",
1558
+ command: "Record<string, unknown>",
1559
+ meta: type({
1560
+ target: "string",
1561
+ storageHash: "string",
1562
+ lane: "string",
1563
+ "targetFamily?": "string",
1564
+ "profileHash?": "string",
1565
+ "annotations?": "Record<string, unknown>"
1566
+ })
1567
+ });
1568
+ const MatchStageJson = type({
1569
+ kind: "\"match\"",
1570
+ filter: "Record<string, unknown>"
1571
+ });
1572
+ const LimitStageJson = type({
1573
+ kind: "\"limit\"",
1574
+ limit: "number"
1575
+ });
1576
+ const SortStageJson = type({
1577
+ kind: "\"sort\"",
1578
+ sort: "Record<string, 1 | -1>"
1579
+ });
1580
+ const ProjectStageJson = type({
1581
+ kind: "\"project\"",
1582
+ projection: "Record<string, 0 | 1>"
1583
+ });
1584
+ const AddFieldsStageJson = type({
1585
+ kind: "\"addFields\"",
1586
+ fields: "Record<string, unknown>"
1587
+ });
1588
+ const LookupStageJson = type({
1589
+ kind: "\"lookup\"",
1590
+ from: "string",
1591
+ as: "string",
1592
+ "localField?": "string",
1593
+ "foreignField?": "string",
1594
+ "pipeline?": "Record<string, unknown>[]",
1595
+ "let_?": "Record<string, unknown>"
1596
+ });
1597
+ const CheckJson = type({
1598
+ description: "string",
1599
+ source: "Record<string, unknown>",
1600
+ filter: "Record<string, unknown>",
1601
+ expect: "\"exists\" | \"notExists\"",
1602
+ "object?": "string > 0"
1603
+ });
1604
+ const StepJson = type({
1605
+ description: "string",
1606
+ command: "Record<string, unknown>"
1607
+ });
1608
+ const DdlOperationJson = type({
1609
+ id: "string",
1610
+ label: "string",
1611
+ operationClass: "\"additive\" | \"widening\" | \"destructive\"",
1612
+ precheck: "Record<string, unknown>[]",
1613
+ execute: "Record<string, unknown>[]",
1614
+ postcheck: "Record<string, unknown>[]"
1615
+ });
1616
+ const DmlCheckJson = type({
1617
+ description: "string",
1618
+ source: "Record<string, unknown>",
1619
+ filter: "Record<string, unknown>",
1620
+ expect: "\"exists\" | \"notExists\""
1621
+ });
1622
+ const DmlOperationJson = type({
1623
+ id: "string",
1624
+ label: "string",
1625
+ operationClass: "\"additive\" | \"widening\" | \"destructive\" | \"data\"",
1626
+ name: "string",
1627
+ precheck: "Record<string, unknown>[]",
1628
+ run: "Record<string, unknown>[]",
1629
+ postcheck: "Record<string, unknown>[]"
1630
+ });
1631
+ function recordValue(json, key, context) {
1632
+ if (json === null || typeof json !== "object") throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Invalid ${context}: expected object`, { meta: { context } });
1633
+ return blindCast(Reflect.get(json, key));
1634
+ }
1635
+ function recordKind(json, context) {
1636
+ const kind = recordValue(json, "kind", context);
1637
+ return typeof kind === "string" ? kind : "";
1638
+ }
1639
+ function validate(schema, data, context) {
1640
+ try {
1641
+ return schema.assert(stripUndefinedDeep(data));
1642
+ } catch (error) {
1643
+ /* v8 ignore stop */
1644
+ throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Invalid ${context}: ${error instanceof Error ? error.message : String(error)}`, {
1645
+ meta: { context },
1646
+ cause: error
1647
+ });
1648
+ }
1649
+ }
1650
+ /**
1651
+ * Strip `undefined`-valued properties before they reach arktype's optional-key
1652
+ * assertions.
1653
+ *
1654
+ * Op IRs (e.g. `CreateCollectionCommand`) assign every optional field on
1655
+ * every instance — fields the caller did not provide land as
1656
+ * `undefined`-valued properties. arktype treats `{ foo?: 'boolean' }` as
1657
+ * "key may be absent, but if present must be boolean", so the bare instance
1658
+ * fails validation when it crosses the deserialize boundary in-process
1659
+ * (no JSON round-trip happens between planner → runner). This helper
1660
+ * recovers the JSON-round-tripped shape (undefined keys absent) without
1661
+ * forcing every caller to round-trip.
1662
+ *
1663
+ * Returns the original value reference whenever no change is needed.
1664
+ * That preserves prototype-bound payload values such as BSON wrappers
1665
+ * (`ObjectId`, `Decimal128`, `Binary`, …) which embed no `undefined`
1666
+ * own-enumerable properties and therefore never trigger a rebuild.
1667
+ * Top-level op IRs (class instances with `undefined` optional fields)
1668
+ * still get flattened to plain records as required by arktype.
1669
+ */
1670
+ function stripUndefinedDeep(value) {
1671
+ if (Array.isArray(value)) {
1672
+ let changed = false;
1673
+ const next = value.map((item) => {
1674
+ const stripped = stripUndefinedDeep(item);
1675
+ if (stripped !== item) changed = true;
1676
+ return stripped;
1677
+ });
1678
+ return changed ? next : value;
1679
+ }
1680
+ if (value === null || typeof value !== "object") return blindCast(value);
1681
+ const entries = Object.entries(value);
1682
+ const out = {};
1683
+ let changed = false;
1684
+ for (const [key, val] of entries) {
1685
+ if (val === void 0) {
1686
+ changed = true;
1687
+ continue;
1688
+ }
1689
+ const stripped = stripUndefinedDeep(val);
1690
+ if (stripped !== val) changed = true;
1691
+ out[key] = stripped;
1692
+ }
1693
+ return changed ? out : blindCast(value);
1694
+ }
1695
+ function deserializeFilterExpr(json) {
1696
+ const kind = recordKind(json, "filter expression");
1697
+ switch (kind) {
1698
+ case "field": {
1699
+ const data = validate(FieldFilterJson, json, "field filter");
1700
+ return MongoFieldFilter.of(data.field, data.op, blindCast(data.value));
1701
+ }
1702
+ case "and": {
1703
+ const data = validate(AndFilterJson, json, "and filter");
1704
+ return MongoAndExpr.of(data.exprs.map(deserializeFilterExpr));
1705
+ }
1706
+ case "or": {
1707
+ const data = validate(OrFilterJson, json, "or filter");
1708
+ return MongoOrExpr.of(data.exprs.map(deserializeFilterExpr));
1709
+ }
1710
+ case "not": return new MongoNotExpr(deserializeFilterExpr(validate(NotFilterJson, json, "not filter").expr));
1711
+ case "exists": {
1712
+ const data = validate(ExistsFilterJson, json, "exists filter");
1713
+ return new MongoExistsExpr(data.field, data.exists);
1714
+ }
1715
+ default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown filter expression kind: ${kind}`, { meta: {
1716
+ context: "filter expression",
1717
+ kind
1718
+ } });
1719
+ }
1720
+ }
1721
+ function deserializePipelineStage(json) {
1722
+ const kind = recordKind(json, "pipeline stage");
1723
+ switch (kind) {
1724
+ case "match": return new MongoMatchStage(deserializeFilterExpr(validate(MatchStageJson, json, "match stage").filter));
1725
+ case "limit": return new MongoLimitStage(validate(LimitStageJson, json, "limit stage").limit);
1726
+ case "sort": return new MongoSortStage(validate(SortStageJson, json, "sort stage").sort);
1727
+ case "project": return new MongoProjectStage(validate(ProjectStageJson, json, "project stage").projection);
1728
+ case "addFields": return new MongoAddFieldsStage(blindCast(validate(AddFieldsStageJson, json, "addFields stage").fields));
1729
+ case "lookup": {
1730
+ const data = validate(LookupStageJson, json, "lookup stage");
1731
+ const opts = {
1732
+ from: data.from,
1733
+ as: data.as
1734
+ };
1735
+ if (data.localField !== void 0) opts.localField = data.localField;
1736
+ if (data.foreignField !== void 0) opts.foreignField = data.foreignField;
1737
+ if (data.pipeline !== void 0) opts.pipeline = data.pipeline.map(deserializePipelineStage);
1738
+ if (data.let_ !== void 0) opts.let_ = blindCast(data.let_);
1739
+ return new MongoLookupStage(opts);
1740
+ }
1741
+ case "merge": {
1742
+ const whenMatched = recordValue(json, "whenMatched", "merge stage");
1743
+ const opts = { into: blindCast(recordValue(json, "into", "merge stage")) };
1744
+ const on = recordValue(json, "on", "merge stage");
1745
+ if (on !== void 0) opts.on = blindCast(on);
1746
+ if (whenMatched !== void 0) opts.whenMatched = typeof whenMatched === "string" ? whenMatched : blindCast(blindCast(whenMatched).map(deserializePipelineStage));
1747
+ const whenNotMatched = recordValue(json, "whenNotMatched", "merge stage");
1748
+ if (whenNotMatched !== void 0) opts.whenNotMatched = blindCast(whenNotMatched);
1749
+ return new MongoMergeStage(opts);
1750
+ }
1751
+ default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown pipeline stage kind: ${kind}`, { meta: {
1752
+ context: "pipeline stage",
1753
+ kind
1754
+ } });
1755
+ }
1756
+ }
1757
+ function deserializeDmlCommand(json) {
1758
+ const kind = recordKind(json, "DML command");
1759
+ switch (kind) {
1760
+ case "rawInsertOne": {
1761
+ const data = validate(RawInsertOneJson, json, "rawInsertOne command");
1762
+ return new RawInsertOneCommand(data.collection, data.document);
1763
+ }
1764
+ case "rawInsertMany": {
1765
+ const data = validate(RawInsertManyJson, json, "rawInsertMany command");
1766
+ return new RawInsertManyCommand(data.collection, data.documents);
1767
+ }
1768
+ case "rawUpdateOne": {
1769
+ const data = validate(RawUpdateOneJson, json, "rawUpdateOne command");
1770
+ return new RawUpdateOneCommand(data.collection, data.filter, data.update);
1771
+ }
1772
+ case "rawUpdateMany": {
1773
+ const data = validate(RawUpdateManyJson, json, "rawUpdateMany command");
1774
+ return new RawUpdateManyCommand(data.collection, data.filter, data.update, ifDefined("bypassDocumentValidation", data.bypassDocumentValidation));
1775
+ }
1776
+ case "rawDeleteOne": {
1777
+ const data = validate(RawDeleteOneJson, json, "rawDeleteOne command");
1778
+ return new RawDeleteOneCommand(data.collection, data.filter);
1779
+ }
1780
+ case "rawDeleteMany": {
1781
+ const data = validate(RawDeleteManyJson, json, "rawDeleteMany command");
1782
+ return new RawDeleteManyCommand(data.collection, data.filter);
1783
+ }
1784
+ case "rawAggregate": {
1785
+ const data = validate(RawAggregateJson, json, "rawAggregate command");
1786
+ return new RawAggregateCommand(data.collection, data.pipeline);
1787
+ }
1788
+ case "rawFindOneAndUpdate": {
1789
+ const data = validate(RawFindOneAndUpdateJson, json, "rawFindOneAndUpdate command");
1790
+ return new RawFindOneAndUpdateCommand(data.collection, data.filter, data.update, data.upsert);
1791
+ }
1792
+ case "rawFindOneAndDelete": {
1793
+ const data = validate(RawFindOneAndDeleteJson, json, "rawFindOneAndDelete command");
1794
+ return new RawFindOneAndDeleteCommand(data.collection, data.filter);
1795
+ }
1796
+ case "aggregate": {
1797
+ const data = validate(TypedAggregateJson, json, "aggregate command");
1798
+ const pipeline = data.pipeline.map(deserializePipelineStage);
1799
+ return new AggregateCommand(data.collection, pipeline);
1800
+ }
1801
+ default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown DML command kind: ${kind}`, { meta: {
1802
+ context: "DML command",
1803
+ kind
1804
+ } });
1805
+ }
1806
+ }
1807
+ function deserializeMongoQueryPlan(json) {
1808
+ const data = validate(QueryPlanJson, json, "Mongo query plan");
1809
+ const command = deserializeDmlCommand(data.command);
1810
+ const m = data.meta;
1811
+ const meta = {
1812
+ target: m.target,
1813
+ storageHash: m.storageHash,
1814
+ lane: m.lane,
1815
+ ...ifDefined("targetFamily", m.targetFamily),
1816
+ ...ifDefined("profileHash", m.profileHash),
1817
+ ...ifDefined("annotations", m.annotations)
1818
+ };
1819
+ return {
1820
+ collection: data.collection,
1821
+ command,
1822
+ meta
1823
+ };
1824
+ }
1825
+ function deserializeDdlCommand(json) {
1826
+ const kind = recordKind(json, "DDL command");
1827
+ switch (kind) {
1828
+ case "createIndex": {
1829
+ const data = validate(CreateIndexJson, json, "createIndex command");
1830
+ return new CreateIndexCommand(data.collection, data.keys, {
1831
+ ...ifDefined("unique", data.unique),
1832
+ ...ifDefined("sparse", data.sparse),
1833
+ ...ifDefined("expireAfterSeconds", data.expireAfterSeconds),
1834
+ ...ifDefined("partialFilterExpression", data.partialFilterExpression),
1835
+ ...ifDefined("name", data.name),
1836
+ ...ifDefined("wildcardProjection", data.wildcardProjection),
1837
+ ...ifDefined("collation", data.collation),
1838
+ ...ifDefined("weights", data.weights),
1839
+ ...ifDefined("default_language", data.default_language),
1840
+ ...ifDefined("language_override", data.language_override)
1841
+ });
1842
+ }
1843
+ case "dropIndex": {
1844
+ const data = validate(DropIndexJson, json, "dropIndex command");
1845
+ return new DropIndexCommand(data.collection, data.name);
1846
+ }
1847
+ case "createCollection": {
1848
+ const data = validate(CreateCollectionJson, json, "createCollection command");
1849
+ return new CreateCollectionCommand(data.collection, {
1850
+ ...ifDefined("validator", data.validator),
1851
+ ...ifDefined("validationLevel", data.validationLevel),
1852
+ ...ifDefined("validationAction", data.validationAction),
1853
+ ...ifDefined("capped", data.capped),
1854
+ ...ifDefined("size", data.size),
1855
+ ...ifDefined("max", data.max),
1856
+ ...ifDefined("timeseries", data.timeseries),
1857
+ ...ifDefined("collation", data.collation),
1858
+ ...ifDefined("changeStreamPreAndPostImages", data.changeStreamPreAndPostImages),
1859
+ ...ifDefined("clusteredIndex", data.clusteredIndex)
1860
+ });
1861
+ }
1862
+ case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
1863
+ case "renameCollection": {
1864
+ const data = validate(RenameCollectionJson, json, "renameCollection command");
1865
+ return new RenameCollectionCommand(data.collection, data.to);
1866
+ }
1867
+ case "collMod": {
1868
+ const data = validate(CollModJson, json, "collMod command");
1869
+ return new CollModCommand(data.collection, {
1870
+ ...ifDefined("validator", data.validator),
1871
+ ...ifDefined("validationLevel", data.validationLevel),
1872
+ ...ifDefined("validationAction", data.validationAction),
1873
+ ...ifDefined("changeStreamPreAndPostImages", data.changeStreamPreAndPostImages)
1874
+ });
1875
+ }
1876
+ default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown DDL command kind: ${kind}`, { meta: {
1877
+ context: "DDL command",
1878
+ kind
1879
+ } });
1880
+ }
1881
+ }
1882
+ function deserializeInspectionCommand(json) {
1883
+ const kind = recordKind(json, "inspection command");
1884
+ switch (kind) {
1885
+ case "listIndexes": return new ListIndexesCommand(validate(ListIndexesJson, json, "listIndexes command").collection);
1886
+ case "listCollections":
1887
+ validate(ListCollectionsJson, json, "listCollections command");
1888
+ return new ListCollectionsCommand();
1889
+ default: throw mongoTargetError("MIGRATION.INVALID_OPERATION_ENTRY", `Unknown inspection command kind: ${kind}`, { meta: {
1890
+ context: "inspection command",
1891
+ kind
1892
+ } });
1893
+ }
1894
+ }
1895
+ function deserializeCheck(json) {
1896
+ const data = validate(CheckJson, json, "migration check");
1897
+ return {
1898
+ description: data.description,
1899
+ source: deserializeInspectionCommand(data.source),
1900
+ filter: deserializeFilterExpr(data.filter),
1901
+ expect: data.expect,
1902
+ ...ifDefined("object", data.object)
1903
+ };
1904
+ }
1905
+ function deserializeStep(json) {
1906
+ const data = validate(StepJson, json, "migration step");
1907
+ return {
1908
+ description: data.description,
1909
+ command: deserializeDdlCommand(data.command)
1910
+ };
1911
+ }
1912
+ function isDmlOperationJson(json) {
1913
+ return Array.isArray(recordValue(json, "run", "migration operation"));
1914
+ }
1915
+ function deserializeDdlOp(json) {
1916
+ const data = validate(DdlOperationJson, json, "migration operation");
1917
+ return {
1918
+ id: data.id,
1919
+ label: data.label,
1920
+ operationClass: data.operationClass,
1921
+ precheck: data.precheck.map(deserializeCheck),
1922
+ execute: data.execute.map(deserializeStep),
1923
+ postcheck: data.postcheck.map(deserializeCheck)
1924
+ };
1925
+ }
1926
+ function deserializeDmlCheck(json) {
1927
+ const data = validate(DmlCheckJson, json, "DML operation check");
1928
+ return {
1929
+ description: data.description,
1930
+ source: deserializeMongoQueryPlan(data.source),
1931
+ filter: deserializeFilterExpr(data.filter),
1932
+ expect: data.expect
1933
+ };
1934
+ }
1935
+ function deserializeDmlOp(json) {
1936
+ const data = validate(DmlOperationJson, json, "DML operation");
1937
+ return {
1938
+ id: data.id,
1939
+ label: data.label,
1940
+ operationClass: data.operationClass,
1941
+ name: data.name,
1942
+ precheck: data.precheck.map(deserializeDmlCheck),
1943
+ run: data.run.map(deserializeMongoQueryPlan),
1944
+ postcheck: data.postcheck.map(deserializeDmlCheck)
1945
+ };
1946
+ }
1947
+ function deserializeMongoOp(json) {
1948
+ if (isDmlOperationJson(json)) return deserializeDmlOp(json);
1949
+ return deserializeDdlOp(json);
1950
+ }
1951
+ function deserializeMongoOps(json) {
1952
+ return json.map(deserializeMongoOp);
1953
+ }
1954
+ function serializeMongoOps(ops) {
1955
+ return JSON.stringify(ops, null, 2);
1956
+ }
1957
+ const READ_ONLY_CHECK_COMMAND_KINDS = /* @__PURE__ */ new Set(["aggregate", "rawAggregate"]);
1958
+ const CHANGED_DOCUMENT_COUNTS = [
1959
+ "insertedCount",
1960
+ "modifiedCount",
1961
+ "upsertedCount",
1962
+ "deletedCount"
1963
+ ];
1964
+ /** Whether a write command's result row says it changed no document: every count it carries is zero. */
1965
+ function reportsNoChangedDocuments(row) {
1966
+ if (typeof row !== "object" || row === null) return false;
1967
+ const counts = CHANGED_DOCUMENT_COUNTS.map((field) => Reflect.get(row, field)).filter((count) => count !== void 0);
1968
+ return counts.length > 0 && counts.every((count) => count === 0);
1969
+ }
1970
+ function failedCheckOf(operationId, phase, check) {
1971
+ return {
1972
+ operationId,
1973
+ phase,
1974
+ description: check.description,
1975
+ ...ifDefined("expectation", check.object === void 0 ? void 0 : check.expect === "exists" ? expectPresent(check.object) : expectAbsent(check.object))
1976
+ };
1977
+ }
1978
+ function runnerFailure(code, summary, options) {
1979
+ return notOk(migrationRunnerFailure(code, summary, options));
1980
+ }
1981
+ /**
1982
+ * The failure when a field rename's before-check finds documents that already hold the new name,
1983
+ * which `$rename` would overwrite: what to look for and fix, and that the plan resumes.
1984
+ */
1985
+ function renameOverwriteRefusal(op) {
1986
+ const rewrite = fieldRewriteOf(op);
1987
+ if (rewrite === void 0) return void 0;
1988
+ const renames = Reflect.get(rewrite.command.update, "$rename");
1989
+ const to = typeof renames === "object" && renames !== null ? Reflect.get(renames, rewrite.field) : void 0;
1990
+ if (typeof to !== "string") return void 0;
1991
+ const [check] = op.precheck;
1992
+ const source = check?.source.command;
1993
+ const match = source instanceof RawAggregateCommand ? Reflect.get(source.pipeline[0] ?? {}, "$match") : void 0;
1994
+ const find = `db.getCollection(${JSON.stringify(rewrite.collection)}).find(${JSON.stringify(match ?? {})})`;
1995
+ return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} stopped: some documents in collection ${rewrite.collection} already hold ${to} as well as ${rewrite.field}`, {
1996
+ why: `Renaming ${rewrite.field} to ${to} would overwrite their ${to}. Find them with ${find} in mongosh, and move or remove one of the two values in each. Nothing was renamed. Once the documents are fixed, run the command again; operations that already ran are skipped.`,
1997
+ meta: {
1998
+ operationId: op.id,
1999
+ name: op.name
2000
+ }
2001
+ });
2002
+ }
2003
+ /**
2004
+ * The failure when a collection rename's before-check finds that the new collection already exists.
2005
+ */
2006
+ function renameTargetExistsRefusal(op, failed) {
2007
+ const rename = op.execute.find((step) => step.command instanceof RenameCollectionCommand)?.command;
2008
+ if (!(rename instanceof RenameCollectionCommand) || failed.expect !== "notExists") return;
2009
+ const { collection: from, to } = rename;
2010
+ return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} stopped: collection ${to} already exists, so collection ${from} cannot be renamed to it`, {
2011
+ why: `Nothing was renamed. If ${to} holds nothing you need, drop it, then run the command again; operations that already ran are skipped. If an earlier operation of the same migration created ${to}, as a createIndex on ${to} does, make renameCollection(${JSON.stringify(from)}, ${JSON.stringify(to)}) the first operation of its migration.ts, run node on that migration.ts, and then drop ${to} and run the command again.`,
2012
+ meta: { operationId: op.id }
2013
+ });
2014
+ }
2015
+ const MONGO_UNAUTHORIZED = 13;
2016
+ /**
2017
+ * The failure for a rewrite that skips validation when the database user lacks the
2018
+ * `bypassDocumentValidation` action, as a `readWrite` user on a hosted cluster does.
2019
+ */
2020
+ function bypassValidationRefusal(op, plan, error) {
2021
+ if (!(plan.command instanceof RawUpdateManyCommand) || !plan.command.bypassDocumentValidation) return;
2022
+ if (typeof error !== "object" || error === null) return void 0;
2023
+ if (Reflect.get(error, "code") !== MONGO_UNAUTHORIZED) return void 0;
2024
+ const codeName = Reflect.get(error, "codeName");
2025
+ return runnerFailure("MIGRATION.EXECUTION_FAILED", `Operation ${op.id} failed: the database user may not skip document validation`, {
2026
+ why: `Renaming or removing a field rewrites documents with bypassDocumentValidation, because the old validator requires the old field. MongoDB refused it with error ${MONGO_UNAUTHORIZED} (${String(codeName)}). Grant the user the bypassDocumentValidation action on the database, for example with the dbAdmin or restore role, then run the command again; operations that already ran are skipped.`,
2027
+ meta: {
2028
+ operationId: op.id,
2029
+ mongoCode: MONGO_UNAUTHORIZED,
2030
+ codeName
2031
+ }
2032
+ });
2033
+ }
2034
+ var MongoMigrationRunner = class {
2035
+ deps;
2036
+ constructor(deps) {
2037
+ this.deps = deps;
2038
+ }
2039
+ async execute(options) {
2040
+ let changedDatabase = false;
2041
+ const result = await this.apply(options, () => {
2042
+ changedDatabase = true;
2043
+ });
2044
+ return result.ok ? ok({
2045
+ ...result.value,
2046
+ changedDatabase
2047
+ }) : notOk(withDatabaseUnchanged(result.failure, options.databaseChangedBefore !== true && !changedDatabase));
2048
+ }
2049
+ /** Applies the plan, calling `wrote` after each command that changes the database. */
2050
+ async apply(options, wrote) {
2051
+ const { inspectionExecutor, adapter, driver, executeDdl, markerOps } = this.deps;
2052
+ const operations = deserializeMongoOps(blindCast(options.plan.operations));
2053
+ const space = options.plan.spaceId ?? APP_SPACE_ID;
2054
+ const policyCheck = this.enforcePolicyCompatibility(options.policy, operations);
2055
+ if (policyCheck) return policyCheck;
2056
+ const existingMarker = await markerOps.readMarker(space);
2057
+ const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
2058
+ if (markerCheck) return markerCheck;
2059
+ const checks = options.executionChecks;
2060
+ const runPrechecks = checks?.prechecks !== false;
2061
+ const runPostchecks = checks?.postchecks !== false;
2062
+ const runIdempotency = checks?.idempotencyChecks !== false;
2063
+ const filterEvaluator = new FilterEvaluator();
2064
+ let operationsExecuted = 0;
2065
+ for (const operation of operations) {
2066
+ options.callbacks?.onOperationStart?.(operation);
2067
+ try {
2068
+ if (isMongoDmlOperation(operation)) {
2069
+ const result = await this.executeDmlOperation(operation, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks, wrote);
2070
+ if (result.failure) return result.failure;
2071
+ if (result.executed) operationsExecuted += 1;
2072
+ continue;
2073
+ }
2074
+ if (runPostchecks && runIdempotency) {
2075
+ if (await this.allChecksSatisfied(operation.postcheck, inspectionExecutor, filterEvaluator)) continue;
2076
+ }
2077
+ if (runPrechecks) {
2078
+ const failing = await this.firstFailingCheck(operation.precheck, inspectionExecutor, filterEvaluator);
2079
+ if (failing !== void 0) {
2080
+ const failedCheck = failedCheckOf(operation.id, "precheck", failing);
2081
+ return renameTargetExistsRefusal(operation, failing) ?? runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${operation.id} failed during precheck: ${failing.description}`, {
2082
+ why: failedPrecheckWhy(failedCheck),
2083
+ meta: { failedCheck }
2084
+ });
2085
+ }
2086
+ }
2087
+ for (const step of operation.execute) {
2088
+ await executeDdl(step.command);
2089
+ wrote();
2090
+ }
2091
+ if (runPostchecks) {
2092
+ const failing = await this.firstFailingCheck(operation.postcheck, inspectionExecutor, filterEvaluator);
2093
+ if (failing !== void 0) {
2094
+ const failedCheck = failedCheckOf(operation.id, "postcheck", failing);
2095
+ return runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${operation.id} failed during postcheck: ${failing.description}`, {
2096
+ why: failedPostcheckWhy(failedCheck),
2097
+ meta: {
2098
+ operationId: operation.id,
2099
+ failedCheck
2100
+ }
2101
+ });
2102
+ }
2103
+ }
2104
+ operationsExecuted += 1;
2105
+ } finally {
2106
+ options.callbacks?.onOperationComplete?.(operation);
2107
+ }
2108
+ }
2109
+ const destination = options.plan.destination;
2110
+ const profileHash = options.destinationContract.profileHash ?? destination.storageHash;
2111
+ const incomingInvariants = options.plan.providedInvariants ?? [];
2112
+ const existingInvariantSet = new Set(existingMarker?.invariants ?? []);
2113
+ const incomingIsSubsetOfExisting = incomingInvariants.every((id) => existingInvariantSet.has(id));
2114
+ const markerAlreadyAtDestination = existingMarker !== null && existingMarker.storageHash === destination.storageHash && existingMarker.profileHash === profileHash;
2115
+ if (!(operationsExecuted === 0 && markerAlreadyAtDestination && incomingIsSubsetOfExisting)) {
2116
+ const liveSchema = await this.deps.introspectSchema();
2117
+ const rawVerifyResult = verifyMongoSchema({
2118
+ contract: options.destinationContract,
2119
+ schema: liveSchema,
2120
+ strict: options.strictVerification ?? true,
2121
+ frameworkComponents: options.frameworkComponents,
2122
+ ...options.context ? { context: options.context } : {}
2123
+ });
2124
+ const verifyResult = options.scopeVerifyResult ? options.scopeVerifyResult(rawVerifyResult) : rawVerifyResult;
2125
+ if (!verifyResult.ok) return runnerFailure("MIGRATION.SCHEMA_VERIFY_FAILED", verifyResult.summary, {
2126
+ why: schemaCheckWhy(verifyResult.schema.issues),
2127
+ meta: { schemaIssues: verifyResult.schema.issues }
2128
+ });
2129
+ if (existingMarker) {
2130
+ if (!await markerOps.updateMarker(space, existingMarker.storageHash, {
2131
+ storageHash: destination.storageHash,
2132
+ profileHash,
2133
+ invariants: incomingInvariants
2134
+ })) {
2135
+ const found = await markerOps.readMarker(space);
2136
+ return runnerFailure("MIGRATION.MARKER_CAS_FAILURE", "Marker was modified by another process during migration execution.", { meta: {
2137
+ space,
2138
+ expectedStorageHash: existingMarker.storageHash,
2139
+ foundStorageHash: found?.storageHash ?? null,
2140
+ destinationStorageHash: destination.storageHash
2141
+ } });
2142
+ }
2143
+ } else await markerOps.initMarker(space, {
2144
+ storageHash: destination.storageHash,
2145
+ profileHash,
2146
+ invariants: incomingInvariants
2147
+ });
2148
+ wrote();
2149
+ await this.recordLedgerEntries(markerOps, space, options);
2150
+ }
2151
+ return ok({
2152
+ operationsPlanned: operations.length,
2153
+ operationsExecuted
2154
+ });
2155
+ }
2156
+ async recordLedgerEntries(markerOps, space, options) {
2157
+ const plan = options.plan;
2158
+ const edges = options.migrationEdges;
2159
+ const totalEdgeOps = edges.reduce((sum, edge) => sum + edge.operationCount, 0);
2160
+ if (totalEdgeOps !== plan.operations.length) throw new InternalError(`Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`);
2161
+ let offset = 0;
2162
+ for (const edge of edges) {
2163
+ const edgeOps = plan.operations.slice(offset, offset + edge.operationCount);
2164
+ offset += edge.operationCount;
2165
+ await markerOps.writeLedgerEntry(space, {
2166
+ edgeId: `${edge.from}->${edge.to}`,
2167
+ from: edge.from,
2168
+ to: edge.to,
2169
+ migrationName: edge.dirName,
2170
+ migrationHash: edge.migrationHash,
2171
+ operations: edgeOps
2172
+ });
2173
+ }
2174
+ }
2175
+ async executeDmlOperation(op, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks, wrote) {
2176
+ if (runPostchecks && runIdempotency && op.postcheck.length > 0) {
2177
+ if (await this.evaluateDmlChecks(op.postcheck, adapter, driver, filterEvaluator)) return { executed: false };
2178
+ }
2179
+ if (runPrechecks && op.precheck.length > 0) {
2180
+ if (!await this.evaluateDmlChecks(op.precheck, adapter, driver, filterEvaluator)) return {
2181
+ executed: false,
2182
+ failure: renameOverwriteRefusal(op) ?? runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} failed during precheck`, { meta: {
2183
+ operationId: op.id,
2184
+ name: op.name
2185
+ } })
2186
+ };
2187
+ }
2188
+ for (const plan of op.run) {
2189
+ const wireCommand = await adapter.lower(plan, {});
2190
+ try {
2191
+ let reportedNoChange = false;
2192
+ let reportedChange = false;
2193
+ for await (const row of driver.execute(wireCommand)) if (reportsNoChangedDocuments(row)) reportedNoChange = true;
2194
+ else reportedChange = true;
2195
+ if (reportedChange || !reportedNoChange) wrote();
2196
+ } catch (error) {
2197
+ const refusal = bypassValidationRefusal(op, plan, error);
2198
+ if (refusal === void 0) throw error;
2199
+ return {
2200
+ executed: false,
2201
+ failure: refusal
2202
+ };
2203
+ }
2204
+ }
2205
+ if (runPostchecks && op.postcheck.length > 0) {
2206
+ if (!await this.evaluateDmlChecks(op.postcheck, adapter, driver, filterEvaluator)) return {
2207
+ executed: false,
2208
+ failure: runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${op.id} failed during postcheck`, { meta: {
2209
+ operationId: op.id,
2210
+ name: op.name
2211
+ } })
2212
+ };
2213
+ }
2214
+ return { executed: true };
2215
+ }
2216
+ async evaluateDmlChecks(checks, adapter, driver, filterEvaluator) {
2217
+ for (const check of checks) {
2218
+ const commandKind = check.source.command.kind;
2219
+ if (!READ_ONLY_CHECK_COMMAND_KINDS.has(commandKind)) throw mongoTargetError("MIGRATION.DATA_TRANSFORM_CHECK_NOT_READ_ONLY", `Data-transform check rejected: command kind "${commandKind}" is not read-only`, {
2220
+ why: "Data-transform checks must use aggregate or rawAggregate commands so the pre/postcheck path cannot mutate the database.",
2221
+ fix: "Author the check.source as an aggregate pipeline (or rawAggregate) rather than a DML write command.",
2222
+ meta: {
2223
+ checkDescription: check.description,
2224
+ commandKind,
2225
+ collection: check.source.collection
2226
+ }
2227
+ });
2228
+ const wireCommand = await adapter.lower(check.source, {});
2229
+ let matchFound = false;
2230
+ for await (const row of driver.execute(wireCommand)) if (filterEvaluator.evaluate(check.filter, row)) {
2231
+ matchFound = true;
2232
+ break;
2233
+ }
2234
+ if (!(check.expect === "exists" ? matchFound : !matchFound)) return false;
2235
+ }
2236
+ return true;
2237
+ }
2238
+ async evaluateChecks(checks, inspectionExecutor, filterEvaluator) {
2239
+ return await this.firstFailingCheck(checks, inspectionExecutor, filterEvaluator) === void 0;
2240
+ }
2241
+ async firstFailingCheck(checks, inspectionExecutor, filterEvaluator) {
2242
+ for (const check of checks) {
2243
+ const matchFound = (await check.source.accept(inspectionExecutor)).some((doc) => filterEvaluator.evaluate(check.filter, doc));
2244
+ if (!(check.expect === "exists" ? matchFound : !matchFound)) return check;
2245
+ }
2246
+ }
2247
+ async allChecksSatisfied(checks, inspectionExecutor, filterEvaluator) {
2248
+ if (checks.length === 0) return false;
2249
+ return this.evaluateChecks(checks, inspectionExecutor, filterEvaluator);
2250
+ }
2251
+ enforcePolicyCompatibility(policy, operations) {
2252
+ const allowedClasses = new Set(policy.allowedOperationClasses);
2253
+ for (const operation of operations) if (!allowedClasses.has(operation.operationClass)) return runnerFailure("MIGRATION.POLICY_VIOLATION", `Operation ${operation.id} has class "${operation.operationClass}" which is not allowed by policy.`, {
2254
+ why: `Policy only allows: ${[...allowedClasses].join(", ")}.`,
2255
+ meta: {
2256
+ operationId: operation.id,
2257
+ operationClass: operation.operationClass
2258
+ }
2259
+ });
2260
+ }
2261
+ ensureMarkerCompatibility(marker, plan) {
2262
+ const origin = plan.origin ?? null;
2263
+ if (!origin) return;
2264
+ if (!marker) return runnerFailure("MIGRATION.MARKER_ORIGIN_MISMATCH", `Missing contract marker: expected origin storage hash ${origin.storageHash}.`, { meta: { expectedOriginStorageHash: origin.storageHash } });
2265
+ if (marker.storageHash !== origin.storageHash) return runnerFailure("MIGRATION.MARKER_ORIGIN_MISMATCH", `Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`, { meta: {
2266
+ markerStorageHash: marker.storageHash,
2267
+ expectedOriginStorageHash: origin.storageHash
2268
+ } });
2269
+ }
2270
+ };
2271
+ /**
2272
+ * Mongo target `SchemaVerifier` concretion. Extends the family base's
2273
+ * namespace-walk scaffolding and contributes the per-namespace diff via
2274
+ * `verifyNamespace`; the diff body reuses the existing target-side
2275
+ * helpers (`contractToMongoSchemaIR`, `canonicalizeSchemasForVerification`,
2276
+ * `diffMongoSchemas`) so production verification behaviour is unchanged.
2277
+ *
2278
+ * Today's invariant: every Mongo contract carries exactly one
2279
+ * namespace (the unbound singleton, materialised as
2280
+ * `MongoTargetUnboundDatabase`), so the family-base namespace walk
2281
+ * dispatches exactly once and the per-namespace body runs the existing
2282
+ * whole-schema diff. Future per-collection namespace assignment will
2283
+ * have this hook project the diff to the namespace's owned collections.
2284
+ *
2285
+ * `verifyTargetExtensions` returns the empty list — Mongo has no
2286
+ * target-only kinds today.
2287
+ *
2288
+ * Strict diff mode is `false` for SPI-routed calls; production
2289
+ * verification today still goes through `verifyMongoSchema` which
2290
+ * receives strict from the CLI.
2291
+ */
2292
+ var MongoTargetSchemaVerifier = class extends MongoSchemaVerifierBase {
2293
+ verifyNamespace(options) {
2294
+ const expectedIR = contractToMongoSchemaIR(options.contract);
2295
+ const { live, expected } = canonicalizeSchemasForVerification(options.schema, expectedIR);
2296
+ const collectionControlPolicy = (name) => this.collectionControlPolicyForName(options.contract, name);
2297
+ const { failures, warnings } = diffMongoSchemas(live, expected, false, collectionControlPolicy);
2298
+ return [...failures, ...warnings];
2299
+ }
2300
+ verifyTargetExtensions(_options) {
2301
+ return [];
2302
+ }
2303
+ };
2304
+ /**
2305
+ * The bare entity names the given contracts declare, unioned. The Mongo runner
2306
+ * asks this of every OTHER contract space in a multi-space apply, so each
2307
+ * space's post-apply verify can drop the extras those siblings claim.
2308
+ */
2309
+ function entityNamesDeclaredBy(contracts) {
2310
+ const owned = /* @__PURE__ */ new Set();
2311
+ for (const contract of contracts) for (const { entityName } of elementCoordinates(contract.storage)) owned.add(entityName);
2312
+ return owned;
2313
+ }
2314
+ /**
2315
+ * True when an issue reports a whole collection present in the database but
2316
+ * declared by no contract (an extra) — its path is exactly the collection
2317
+ * name, never a deeper index/validator/options auxiliary.
2318
+ */
2319
+ function extraCollectionName(issue) {
2320
+ if (issueOutcome(issue) !== "not-expected" || issue.path.length !== 1) return void 0;
2321
+ return issue.path[0];
2322
+ }
2323
+ /**
2324
+ * Scope a per-space post-apply verify result to the contract space's own
2325
+ * elements: drop the `extra` findings for collections another contract space
2326
+ * claims. The runner verifies the destination contract against the full live
2327
+ * database, which holds sibling spaces' collections — without the scoping a
2328
+ * multi-space apply could never pass strict verify. Extras claimed by NO space
2329
+ * survive, so genuine drift still fails the runner's verdict.
2330
+ *
2331
+ * The result is issue-based, so the verdict recomputes directly from the
2332
+ * surviving list: `ok` holds exactly when it is empty.
2333
+ */
2334
+ function scopeVerifyResultToSpace(result, ownedByOtherSpaces) {
2335
+ if (ownedByOtherSpaces.size === 0) return result;
2336
+ const issues = result.schema.issues.filter((issue) => {
2337
+ const name = extraCollectionName(issue);
2338
+ return name === void 0 || !ownedByOtherSpaces.has(name);
2339
+ });
2340
+ if (issues.length === result.schema.issues.length) return result;
2341
+ const ok = issues.length === 0;
2342
+ const { code: staleCode, ...envelope } = result;
2343
+ return {
2344
+ ...envelope,
2345
+ ok,
2346
+ ...ok ? {} : { code: result.code ?? VERIFY_CODE_SCHEMA_FAILURE },
2347
+ summary: ok ? "Database schema satisfies contract" : result.summary,
2348
+ schema: {
2349
+ ...result.schema,
2350
+ issues
2351
+ }
2352
+ };
2353
+ }
2354
+ /**
2355
+ * `migration.ts` default-exports a `Migration` subclass whose `operations`
2356
+ * getter returns the ordered list of operations and whose `describe()`
2357
+ * returns the manifest identity metadata. `MongoMigrationPlanner.plan()`
2358
+ * returns a `MigrationPlanWithAuthoringSurface` that knows how to render
2359
+ * itself back to such a file; `MongoMigrationPlanner.emptyMigration()`
2360
+ * returns the same shape for `migration new`. Users run the scaffolded
2361
+ * `migration.ts` directly (via `node migration.ts`) to self-emit
2362
+ * `ops.json` and attest the `migrationHash`.
2363
+ */
2364
+ const mongoTargetDescriptor = {
2365
+ ...mongoTargetDescriptorMeta,
2366
+ contractSerializer: new MongoTargetContractSerializer(),
2367
+ schemaVerifier: new MongoTargetSchemaVerifier(),
2368
+ migrations: {
2369
+ createPlanner(_adapter) {
2370
+ return new MongoMigrationPlanner();
2371
+ },
2372
+ createRunner(family) {
2373
+ let cachedDeps;
2374
+ const runMongo = async (driver, runnerOptions) => {
2375
+ cachedDeps ??= family.createRunnerDependencies({ driver });
2376
+ return new MongoMigrationRunner(cachedDeps).execute({
2377
+ ...runnerOptions,
2378
+ destinationContract: blindCast(runnerOptions.destinationContract)
2379
+ });
2380
+ };
2381
+ return { async execute({ driver, perSpaceOptions }) {
2382
+ const contracts = perSpaceOptions.map((opts) => blindCast(opts.destinationContract));
2383
+ const perSpaceResults = [];
2384
+ let changedDatabase = false;
2385
+ for (let i = 0; i < perSpaceOptions.length; i++) {
2386
+ const spaceOptions = perSpaceOptions[i];
2387
+ if (!spaceOptions) continue;
2388
+ const ownedByOtherSpaces = entityNamesDeclaredBy(contracts.filter((_, j) => j !== i));
2389
+ const scopeVerifyResult = (result) => scopeVerifyResultToSpace(result, ownedByOtherSpaces);
2390
+ const { space, ...runnerOptions } = spaceOptions;
2391
+ const result = await runMongo(driver, {
2392
+ ...runnerOptions,
2393
+ scopeVerifyResult,
2394
+ databaseChangedBefore: changedDatabase
2395
+ });
2396
+ if (!result.ok) return notOk(withFailingSpace(result.failure, space));
2397
+ const { changedDatabase: spaceChanged, ...value } = result.value;
2398
+ changedDatabase ||= spaceChanged;
2399
+ perSpaceResults.push({
2400
+ space,
2401
+ value
2402
+ });
2403
+ }
2404
+ return ok({ perSpaceResults });
2405
+ } };
2406
+ },
2407
+ contractToSchema(contract) {
2408
+ return contractToMongoSchemaIR(blindCast(contract));
2409
+ }
2410
+ },
2411
+ create() {
2412
+ return {
2413
+ familyId: "mongo",
2414
+ targetId: "mongo"
2415
+ };
2416
+ }
2417
+ };
2418
+ //#endregion
2419
+ export { schemaIndexToCreateIndexOptions as C, schemaCollectionToCreateCollectionOptions as S, deserializeMongoOp as _, DropIndexCall as a, renderCallsToTypeScript as b, MongoMigrationRunner as c, MongoTargetSchemaVerifier as d, MongoTargetUnboundDatabase as f, UnsetFieldCall as g, RenameFieldCall as h, DropCollectionCall as i, MongoTargetContractSerializer as l, RenameCollectionCall as m, CreateCollectionCall as n, FilterEvaluator as o, PlannerProducedMongoMigration as p, CreateIndexCall as r, MongoMigrationPlanner as s, CollModCall as t, MongoTargetDatabase as u, deserializeMongoOps as v, serializeMongoOps as w, renderOps as x, mongoTargetDescriptor as y };
2420
+
2421
+ //# sourceMappingURL=control-Cq6qWohQ.mjs.map