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