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