@prisma-next/target-mongo 0.4.0-dev.1 → 0.4.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/control.d.mts +107 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +905 -1
- package/dist/control.mjs.map +1 -1
- package/dist/migration-factories-Brzz-QGG.mjs +154 -0
- package/dist/migration-factories-Brzz-QGG.mjs.map +1 -0
- package/dist/migration.d.mts +4 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +2 -125
- package/dist/op-factory-call-CfPGebEH.d.mts +76 -0
- package/dist/op-factory-call-CfPGebEH.d.mts.map +1 -0
- package/package.json +9 -4
- package/src/core/contract-to-schema.ts +63 -0
- package/src/core/ddl-formatter.ts +112 -0
- package/src/core/filter-evaluator.ts +84 -0
- package/src/core/migration-factories.ts +51 -0
- package/src/core/mongo-ops-serializer.ts +277 -0
- package/src/core/mongo-planner.ts +306 -0
- package/src/core/mongo-runner.ts +275 -0
- package/src/core/op-factory-call.ts +196 -0
- package/src/core/render-ops.ts +39 -0
- package/src/core/render-typescript.ts +137 -0
- package/src/exports/control.ts +25 -0
- package/src/exports/migration.ts +1 -0
- package/dist/migration.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/target-mongo",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "MongoDB target pack for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"arktype": "^2.1.29",
|
|
8
9
|
"mongodb": "^6.16.0",
|
|
9
|
-
"@prisma-next/contract": "0.4.0-dev.
|
|
10
|
-
"@prisma-next/mongo-
|
|
11
|
-
"@prisma-next/framework-components": "0.4.0-dev.
|
|
10
|
+
"@prisma-next/contract": "0.4.0-dev.3",
|
|
11
|
+
"@prisma-next/mongo-contract": "0.4.0-dev.3",
|
|
12
|
+
"@prisma-next/framework-components": "0.4.0-dev.3",
|
|
13
|
+
"@prisma-next/mongo-query-ast": "0.4.0-dev.3",
|
|
14
|
+
"@prisma-next/utils": "0.4.0-dev.3",
|
|
15
|
+
"@prisma-next/mongo-schema-ir": "0.4.0-dev.3",
|
|
16
|
+
"@prisma-next/mongo-value": "0.4.0-dev.3"
|
|
12
17
|
},
|
|
13
18
|
"devDependencies": {
|
|
14
19
|
"mongodb-memory-server": "10.4.3",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MongoContract,
|
|
3
|
+
MongoStorageCollection,
|
|
4
|
+
MongoStorageCollectionOptions,
|
|
5
|
+
MongoStorageIndex,
|
|
6
|
+
MongoStorageValidator,
|
|
7
|
+
} from '@prisma-next/mongo-contract';
|
|
8
|
+
import {
|
|
9
|
+
MongoSchemaCollection,
|
|
10
|
+
MongoSchemaCollectionOptions,
|
|
11
|
+
MongoSchemaIndex,
|
|
12
|
+
MongoSchemaIR,
|
|
13
|
+
MongoSchemaValidator,
|
|
14
|
+
} from '@prisma-next/mongo-schema-ir';
|
|
15
|
+
|
|
16
|
+
function convertIndex(index: MongoStorageIndex): MongoSchemaIndex {
|
|
17
|
+
return new MongoSchemaIndex({
|
|
18
|
+
keys: index.keys,
|
|
19
|
+
unique: index.unique,
|
|
20
|
+
sparse: index.sparse,
|
|
21
|
+
expireAfterSeconds: index.expireAfterSeconds,
|
|
22
|
+
partialFilterExpression: index.partialFilterExpression,
|
|
23
|
+
wildcardProjection: index.wildcardProjection,
|
|
24
|
+
collation: index.collation,
|
|
25
|
+
weights: index.weights,
|
|
26
|
+
default_language: index.default_language,
|
|
27
|
+
language_override: index.language_override,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertValidator(v: MongoStorageValidator): MongoSchemaValidator {
|
|
32
|
+
return new MongoSchemaValidator({
|
|
33
|
+
jsonSchema: v.jsonSchema,
|
|
34
|
+
validationLevel: v.validationLevel,
|
|
35
|
+
validationAction: v.validationAction,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function convertOptions(o: MongoStorageCollectionOptions): MongoSchemaCollectionOptions {
|
|
40
|
+
return new MongoSchemaCollectionOptions(o);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function convertCollection(name: string, def: MongoStorageCollection): MongoSchemaCollection {
|
|
44
|
+
const indexes = (def.indexes ?? []).map(convertIndex);
|
|
45
|
+
return new MongoSchemaCollection({
|
|
46
|
+
name,
|
|
47
|
+
indexes,
|
|
48
|
+
...(def.validator != null && { validator: convertValidator(def.validator) }),
|
|
49
|
+
...(def.options != null && { options: convertOptions(def.options) }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function contractToMongoSchemaIR(contract: MongoContract | null): MongoSchemaIR {
|
|
54
|
+
if (!contract) {
|
|
55
|
+
return new MongoSchemaIR([]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const collections = Object.entries(contract.storage.collections).map(([name, def]) =>
|
|
59
|
+
convertCollection(name, def),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return new MongoSchemaIR(collections);
|
|
63
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
|
|
2
|
+
import type {
|
|
3
|
+
CollModCommand,
|
|
4
|
+
CreateCollectionCommand,
|
|
5
|
+
CreateIndexCommand,
|
|
6
|
+
DropCollectionCommand,
|
|
7
|
+
DropIndexCommand,
|
|
8
|
+
MongoDdlCommandVisitor,
|
|
9
|
+
MongoIndexKey,
|
|
10
|
+
} from '@prisma-next/mongo-query-ast/control';
|
|
11
|
+
|
|
12
|
+
function formatKeySpec(keys: ReadonlyArray<MongoIndexKey>): string {
|
|
13
|
+
const entries = keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`);
|
|
14
|
+
return `{ ${entries.join(', ')} }`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatOptions(cmd: CreateIndexCommand): string | undefined {
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
if (cmd.unique) parts.push('unique: true');
|
|
20
|
+
if (cmd.sparse) parts.push('sparse: true');
|
|
21
|
+
if (cmd.expireAfterSeconds !== undefined)
|
|
22
|
+
parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
|
|
23
|
+
if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
|
|
24
|
+
if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
|
|
25
|
+
if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
|
|
26
|
+
if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
|
|
27
|
+
if (cmd.language_override)
|
|
28
|
+
parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
|
|
29
|
+
if (cmd.wildcardProjection)
|
|
30
|
+
parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
|
|
31
|
+
if (cmd.partialFilterExpression)
|
|
32
|
+
parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
|
|
33
|
+
if (parts.length === 0) return undefined;
|
|
34
|
+
return `{ ${parts.join(', ')} }`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatCreateCollectionOptions(cmd: CreateCollectionCommand): string | undefined {
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
if (cmd.capped) parts.push('capped: true');
|
|
40
|
+
if (cmd.size !== undefined) parts.push(`size: ${cmd.size}`);
|
|
41
|
+
if (cmd.max !== undefined) parts.push(`max: ${cmd.max}`);
|
|
42
|
+
if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
|
|
43
|
+
if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
|
|
44
|
+
if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
|
|
45
|
+
if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
|
|
46
|
+
if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
|
|
47
|
+
if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
|
|
48
|
+
if (cmd.changeStreamPreAndPostImages)
|
|
49
|
+
parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
|
|
50
|
+
if (parts.length === 0) return undefined;
|
|
51
|
+
return `{ ${parts.join(', ')} }`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class MongoDdlCommandFormatter implements MongoDdlCommandVisitor<string> {
|
|
55
|
+
createIndex(cmd: CreateIndexCommand): string {
|
|
56
|
+
const keySpec = formatKeySpec(cmd.keys);
|
|
57
|
+
const opts = formatOptions(cmd);
|
|
58
|
+
return opts
|
|
59
|
+
? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})`
|
|
60
|
+
: `db.${cmd.collection}.createIndex(${keySpec})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
dropIndex(cmd: DropIndexCommand): string {
|
|
64
|
+
return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createCollection(cmd: CreateCollectionCommand): string {
|
|
68
|
+
const opts = formatCreateCollectionOptions(cmd);
|
|
69
|
+
return opts
|
|
70
|
+
? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})`
|
|
71
|
+
: `db.createCollection(${JSON.stringify(cmd.collection)})`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dropCollection(cmd: DropCollectionCommand): string {
|
|
75
|
+
return `db.${cmd.collection}.drop()`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
collMod(cmd: CollModCommand): string {
|
|
79
|
+
const parts: string[] = [`collMod: ${JSON.stringify(cmd.collection)}`];
|
|
80
|
+
if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
|
|
81
|
+
if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
|
|
82
|
+
if (cmd.validationAction)
|
|
83
|
+
parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
|
|
84
|
+
if (cmd.changeStreamPreAndPostImages)
|
|
85
|
+
parts.push(
|
|
86
|
+
`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`,
|
|
87
|
+
);
|
|
88
|
+
return `db.runCommand({ ${parts.join(', ')} })`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const formatter = new MongoDdlCommandFormatter();
|
|
93
|
+
|
|
94
|
+
interface MongoExecuteStep {
|
|
95
|
+
readonly command: { readonly accept: <R>(visitor: MongoDdlCommandVisitor<R>) => R };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatMongoOperations(operations: readonly MigrationPlanOperation[]): string[] {
|
|
99
|
+
const statements: string[] = [];
|
|
100
|
+
for (const operation of operations) {
|
|
101
|
+
const candidate = operation as unknown as Record<string, unknown>;
|
|
102
|
+
if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const step of candidate['execute'] as MongoExecuteStep[]) {
|
|
106
|
+
if (step.command && typeof step.command.accept === 'function') {
|
|
107
|
+
statements.push(step.command.accept(formatter));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return statements;
|
|
112
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MongoAndExpr,
|
|
3
|
+
MongoExistsExpr,
|
|
4
|
+
MongoExprFilter,
|
|
5
|
+
MongoFieldFilter,
|
|
6
|
+
MongoFilterExpr,
|
|
7
|
+
MongoFilterVisitor,
|
|
8
|
+
MongoNotExpr,
|
|
9
|
+
MongoOrExpr,
|
|
10
|
+
} from '@prisma-next/mongo-query-ast/control';
|
|
11
|
+
import { deepEqual } from '@prisma-next/mongo-schema-ir';
|
|
12
|
+
import type { MongoValue } from '@prisma-next/mongo-value';
|
|
13
|
+
|
|
14
|
+
function getNestedField(doc: Record<string, unknown>, path: string): unknown {
|
|
15
|
+
const parts = path.split('.');
|
|
16
|
+
let current: unknown = doc;
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const record = current as Record<string, unknown>;
|
|
22
|
+
if (!Object.hasOwn(record, part)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
current = record[part];
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function evaluateFieldOp(op: string, actual: unknown, expected: MongoValue): boolean {
|
|
31
|
+
switch (op) {
|
|
32
|
+
case '$eq':
|
|
33
|
+
return deepEqual(actual, expected);
|
|
34
|
+
case '$ne':
|
|
35
|
+
return !deepEqual(actual, expected);
|
|
36
|
+
case '$gt':
|
|
37
|
+
return typeof actual === typeof expected && (actual as number) > (expected as number);
|
|
38
|
+
case '$gte':
|
|
39
|
+
return typeof actual === typeof expected && (actual as number) >= (expected as number);
|
|
40
|
+
case '$lt':
|
|
41
|
+
return typeof actual === typeof expected && (actual as number) < (expected as number);
|
|
42
|
+
case '$lte':
|
|
43
|
+
return typeof actual === typeof expected && (actual as number) <= (expected as number);
|
|
44
|
+
case '$in':
|
|
45
|
+
return Array.isArray(expected) && expected.some((v) => deepEqual(actual, v));
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unsupported filter operator in migration check: ${op}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class FilterEvaluator implements MongoFilterVisitor<boolean> {
|
|
52
|
+
private doc: Record<string, unknown> = {};
|
|
53
|
+
|
|
54
|
+
evaluate(filter: MongoFilterExpr, doc: Record<string, unknown>): boolean {
|
|
55
|
+
this.doc = doc;
|
|
56
|
+
return filter.accept(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
field(expr: MongoFieldFilter): boolean {
|
|
60
|
+
const value = getNestedField(this.doc, expr.field);
|
|
61
|
+
return evaluateFieldOp(expr.op, value, expr.value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
and(expr: MongoAndExpr): boolean {
|
|
65
|
+
return expr.exprs.every((child) => child.accept(this));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
or(expr: MongoOrExpr): boolean {
|
|
69
|
+
return expr.exprs.some((child) => child.accept(this));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
not(expr: MongoNotExpr): boolean {
|
|
73
|
+
return !expr.expr.accept(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exists(expr: MongoExistsExpr): boolean {
|
|
77
|
+
const has = getNestedField(this.doc, expr.field) !== undefined;
|
|
78
|
+
return expr.exists ? has : !has;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expr(_expr: MongoExprFilter): boolean {
|
|
82
|
+
throw new Error('Aggregation expression filters are not supported in migration checks');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { MongoIndexKey } from '@prisma-next/mongo-query-ast/control';
|
|
|
2
2
|
import {
|
|
3
3
|
buildIndexOpId,
|
|
4
4
|
CollModCommand,
|
|
5
|
+
type CollModOptions,
|
|
5
6
|
CreateCollectionCommand,
|
|
6
7
|
type CreateCollectionOptions,
|
|
7
8
|
CreateIndexCommand,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
MongoFieldFilter,
|
|
17
18
|
type MongoMigrationPlanOperation,
|
|
18
19
|
} from '@prisma-next/mongo-query-ast/control';
|
|
20
|
+
import type { CollModMeta } from './op-factory-call';
|
|
19
21
|
|
|
20
22
|
function formatKeys(keys: ReadonlyArray<MongoIndexKey>): string {
|
|
21
23
|
return keys.map((k) => `${k.field}:${k.direction}`).join(', ');
|
|
@@ -177,6 +179,55 @@ export function setValidation(
|
|
|
177
179
|
};
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
export function collMod(
|
|
183
|
+
collection: string,
|
|
184
|
+
options: CollModOptions,
|
|
185
|
+
meta?: CollModMeta,
|
|
186
|
+
): MongoMigrationPlanOperation {
|
|
187
|
+
const hasValidator = options.validator != null && Object.keys(options.validator).length > 0;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
id: meta?.id ?? `collection.${collection}.collMod`,
|
|
191
|
+
label: meta?.label ?? `Modify collection ${collection}`,
|
|
192
|
+
operationClass: meta?.operationClass ?? 'destructive',
|
|
193
|
+
precheck:
|
|
194
|
+
options.validator != null
|
|
195
|
+
? [
|
|
196
|
+
{
|
|
197
|
+
description: `collection ${collection} exists`,
|
|
198
|
+
source: new ListCollectionsCommand(),
|
|
199
|
+
filter: MongoFieldFilter.eq('name', collection),
|
|
200
|
+
expect: 'exists' as const,
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
: [],
|
|
204
|
+
execute: [
|
|
205
|
+
{
|
|
206
|
+
description: `modify ${collection}`,
|
|
207
|
+
command: new CollModCommand(collection, options),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
postcheck: hasValidator
|
|
211
|
+
? [
|
|
212
|
+
{
|
|
213
|
+
description: `validator applied on ${collection}`,
|
|
214
|
+
source: new ListCollectionsCommand(),
|
|
215
|
+
filter: MongoAndExpr.of([
|
|
216
|
+
MongoFieldFilter.eq('name', collection),
|
|
217
|
+
...(options.validationLevel
|
|
218
|
+
? [MongoFieldFilter.eq('options.validationLevel', options.validationLevel)]
|
|
219
|
+
: []),
|
|
220
|
+
...(options.validationAction
|
|
221
|
+
? [MongoFieldFilter.eq('options.validationAction', options.validationAction)]
|
|
222
|
+
: []),
|
|
223
|
+
]),
|
|
224
|
+
expect: 'exists' as const,
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
: [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
180
231
|
export function validatedCollection(
|
|
181
232
|
name: string,
|
|
182
233
|
schema: Record<string, unknown>,
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { MigrationOperationClass } from '@prisma-next/framework-components/control';
|
|
2
|
+
import {
|
|
3
|
+
type AnyMongoDdlCommand,
|
|
4
|
+
type AnyMongoInspectionCommand,
|
|
5
|
+
CollModCommand,
|
|
6
|
+
CreateCollectionCommand,
|
|
7
|
+
CreateIndexCommand,
|
|
8
|
+
DropCollectionCommand,
|
|
9
|
+
DropIndexCommand,
|
|
10
|
+
ListCollectionsCommand,
|
|
11
|
+
ListIndexesCommand,
|
|
12
|
+
MongoAndExpr,
|
|
13
|
+
MongoExistsExpr,
|
|
14
|
+
MongoFieldFilter,
|
|
15
|
+
type MongoFilterExpr,
|
|
16
|
+
type MongoMigrationCheck,
|
|
17
|
+
type MongoMigrationPlanOperation,
|
|
18
|
+
type MongoMigrationStep,
|
|
19
|
+
MongoNotExpr,
|
|
20
|
+
MongoOrExpr,
|
|
21
|
+
} from '@prisma-next/mongo-query-ast/control';
|
|
22
|
+
import { type } from 'arktype';
|
|
23
|
+
|
|
24
|
+
const IndexKeyDirection = type('1 | -1 | "text" | "2dsphere" | "2d" | "hashed"');
|
|
25
|
+
const IndexKeyJson = type({ field: 'string', direction: IndexKeyDirection });
|
|
26
|
+
|
|
27
|
+
const CreateIndexJson = type({
|
|
28
|
+
kind: '"createIndex"',
|
|
29
|
+
collection: 'string',
|
|
30
|
+
keys: IndexKeyJson.array().atLeastLength(1),
|
|
31
|
+
'unique?': 'boolean',
|
|
32
|
+
'sparse?': 'boolean',
|
|
33
|
+
'expireAfterSeconds?': 'number',
|
|
34
|
+
'partialFilterExpression?': 'Record<string, unknown>',
|
|
35
|
+
'name?': 'string',
|
|
36
|
+
'wildcardProjection?': 'Record<string, unknown>',
|
|
37
|
+
'collation?': 'Record<string, unknown>',
|
|
38
|
+
'weights?': 'Record<string, unknown>',
|
|
39
|
+
'default_language?': 'string',
|
|
40
|
+
'language_override?': 'string',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const DropIndexJson = type({
|
|
44
|
+
kind: '"dropIndex"',
|
|
45
|
+
collection: 'string',
|
|
46
|
+
name: 'string',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const CreateCollectionJson = type({
|
|
50
|
+
kind: '"createCollection"',
|
|
51
|
+
collection: 'string',
|
|
52
|
+
'validator?': 'Record<string, unknown>',
|
|
53
|
+
'validationLevel?': '"strict" | "moderate"',
|
|
54
|
+
'validationAction?': '"error" | "warn"',
|
|
55
|
+
'capped?': 'boolean',
|
|
56
|
+
'size?': 'number',
|
|
57
|
+
'max?': 'number',
|
|
58
|
+
'timeseries?': 'Record<string, unknown>',
|
|
59
|
+
'collation?': 'Record<string, unknown>',
|
|
60
|
+
'changeStreamPreAndPostImages?': 'Record<string, unknown>',
|
|
61
|
+
'clusteredIndex?': 'Record<string, unknown>',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const DropCollectionJson = type({
|
|
65
|
+
kind: '"dropCollection"',
|
|
66
|
+
collection: 'string',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const CollModJson = type({
|
|
70
|
+
kind: '"collMod"',
|
|
71
|
+
collection: 'string',
|
|
72
|
+
'validator?': 'Record<string, unknown>',
|
|
73
|
+
'validationLevel?': '"strict" | "moderate"',
|
|
74
|
+
'validationAction?': '"error" | "warn"',
|
|
75
|
+
'changeStreamPreAndPostImages?': 'Record<string, unknown>',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const ListIndexesJson = type({
|
|
79
|
+
kind: '"listIndexes"',
|
|
80
|
+
collection: 'string',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const ListCollectionsJson = type({
|
|
84
|
+
kind: '"listCollections"',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const FieldFilterJson = type({
|
|
88
|
+
kind: '"field"',
|
|
89
|
+
field: 'string',
|
|
90
|
+
op: 'string',
|
|
91
|
+
value: 'unknown',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const ExistsFilterJson = type({
|
|
95
|
+
kind: '"exists"',
|
|
96
|
+
field: 'string',
|
|
97
|
+
exists: 'boolean',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const CheckJson = type({
|
|
101
|
+
description: 'string',
|
|
102
|
+
source: 'Record<string, unknown>',
|
|
103
|
+
filter: 'Record<string, unknown>',
|
|
104
|
+
expect: '"exists" | "notExists"',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const StepJson = type({
|
|
108
|
+
description: 'string',
|
|
109
|
+
command: 'Record<string, unknown>',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const OperationJson = type({
|
|
113
|
+
id: 'string',
|
|
114
|
+
label: 'string',
|
|
115
|
+
operationClass: '"additive" | "widening" | "destructive"',
|
|
116
|
+
precheck: 'Record<string, unknown>[]',
|
|
117
|
+
execute: 'Record<string, unknown>[]',
|
|
118
|
+
postcheck: 'Record<string, unknown>[]',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function validate<T>(schema: { assert: (data: unknown) => T }, data: unknown, context: string): T {
|
|
122
|
+
try {
|
|
123
|
+
return schema.assert(data);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
/* v8 ignore start -- assertion libraries always throw Error instances */
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
/* v8 ignore stop */
|
|
128
|
+
throw new Error(`Invalid ${context}: ${message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function deserializeFilterExpr(json: unknown): MongoFilterExpr {
|
|
133
|
+
const record = json as Record<string, unknown>;
|
|
134
|
+
const kind = record['kind'] as string;
|
|
135
|
+
switch (kind) {
|
|
136
|
+
case 'field': {
|
|
137
|
+
const data = validate(FieldFilterJson, json, 'field filter');
|
|
138
|
+
return MongoFieldFilter.of(data.field, data.op, data.value as never);
|
|
139
|
+
}
|
|
140
|
+
case 'and': {
|
|
141
|
+
const exprs = record['exprs'];
|
|
142
|
+
if (!Array.isArray(exprs)) throw new Error('Invalid and filter: missing exprs array');
|
|
143
|
+
return MongoAndExpr.of(exprs.map(deserializeFilterExpr));
|
|
144
|
+
}
|
|
145
|
+
case 'or': {
|
|
146
|
+
const exprs = record['exprs'];
|
|
147
|
+
if (!Array.isArray(exprs)) throw new Error('Invalid or filter: missing exprs array');
|
|
148
|
+
return MongoOrExpr.of(exprs.map(deserializeFilterExpr));
|
|
149
|
+
}
|
|
150
|
+
case 'not': {
|
|
151
|
+
const expr = record['expr'];
|
|
152
|
+
if (!expr || typeof expr !== 'object') throw new Error('Invalid not filter: missing expr');
|
|
153
|
+
return new MongoNotExpr(deserializeFilterExpr(expr));
|
|
154
|
+
}
|
|
155
|
+
case 'exists': {
|
|
156
|
+
const data = validate(ExistsFilterJson, json, 'exists filter');
|
|
157
|
+
return new MongoExistsExpr(data.field, data.exists);
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`Unknown filter expression kind: ${kind}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function deserializeDdlCommand(json: unknown): AnyMongoDdlCommand {
|
|
165
|
+
const record = json as Record<string, unknown>;
|
|
166
|
+
const kind = record['kind'] as string;
|
|
167
|
+
switch (kind) {
|
|
168
|
+
case 'createIndex': {
|
|
169
|
+
const data = validate(CreateIndexJson, json, 'createIndex command');
|
|
170
|
+
return new CreateIndexCommand(data.collection, data.keys, {
|
|
171
|
+
unique: data.unique,
|
|
172
|
+
sparse: data.sparse,
|
|
173
|
+
expireAfterSeconds: data.expireAfterSeconds,
|
|
174
|
+
partialFilterExpression: data.partialFilterExpression,
|
|
175
|
+
name: data.name,
|
|
176
|
+
wildcardProjection: data.wildcardProjection as Record<string, 0 | 1> | undefined,
|
|
177
|
+
collation: data.collation,
|
|
178
|
+
weights: data.weights as Record<string, number> | undefined,
|
|
179
|
+
default_language: data.default_language,
|
|
180
|
+
language_override: data.language_override,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
case 'dropIndex': {
|
|
184
|
+
const data = validate(DropIndexJson, json, 'dropIndex command');
|
|
185
|
+
return new DropIndexCommand(data.collection, data.name);
|
|
186
|
+
}
|
|
187
|
+
case 'createCollection': {
|
|
188
|
+
const data = validate(CreateCollectionJson, json, 'createCollection command');
|
|
189
|
+
return new CreateCollectionCommand(data.collection, {
|
|
190
|
+
validator: data.validator,
|
|
191
|
+
validationLevel: data.validationLevel,
|
|
192
|
+
validationAction: data.validationAction,
|
|
193
|
+
capped: data.capped,
|
|
194
|
+
size: data.size,
|
|
195
|
+
max: data.max,
|
|
196
|
+
timeseries: data.timeseries as CreateCollectionCommand['timeseries'],
|
|
197
|
+
collation: data.collation,
|
|
198
|
+
changeStreamPreAndPostImages: data.changeStreamPreAndPostImages as
|
|
199
|
+
| { enabled: boolean }
|
|
200
|
+
| undefined,
|
|
201
|
+
clusteredIndex: data.clusteredIndex as CreateCollectionCommand['clusteredIndex'],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
case 'dropCollection': {
|
|
205
|
+
const data = validate(DropCollectionJson, json, 'dropCollection command');
|
|
206
|
+
return new DropCollectionCommand(data.collection);
|
|
207
|
+
}
|
|
208
|
+
case 'collMod': {
|
|
209
|
+
const data = validate(CollModJson, json, 'collMod command');
|
|
210
|
+
return new CollModCommand(data.collection, {
|
|
211
|
+
validator: data.validator,
|
|
212
|
+
validationLevel: data.validationLevel,
|
|
213
|
+
validationAction: data.validationAction,
|
|
214
|
+
changeStreamPreAndPostImages: data.changeStreamPreAndPostImages as
|
|
215
|
+
| { enabled: boolean }
|
|
216
|
+
| undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
default:
|
|
220
|
+
throw new Error(`Unknown DDL command kind: ${kind}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function deserializeInspectionCommand(json: unknown): AnyMongoInspectionCommand {
|
|
225
|
+
const record = json as Record<string, unknown>;
|
|
226
|
+
const kind = record['kind'] as string;
|
|
227
|
+
switch (kind) {
|
|
228
|
+
case 'listIndexes': {
|
|
229
|
+
const data = validate(ListIndexesJson, json, 'listIndexes command');
|
|
230
|
+
return new ListIndexesCommand(data.collection);
|
|
231
|
+
}
|
|
232
|
+
case 'listCollections': {
|
|
233
|
+
validate(ListCollectionsJson, json, 'listCollections command');
|
|
234
|
+
return new ListCollectionsCommand();
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Unknown inspection command kind: ${kind}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function deserializeCheck(json: unknown): MongoMigrationCheck {
|
|
242
|
+
const data = validate(CheckJson, json, 'migration check');
|
|
243
|
+
return {
|
|
244
|
+
description: data.description,
|
|
245
|
+
source: deserializeInspectionCommand(data.source),
|
|
246
|
+
filter: deserializeFilterExpr(data.filter),
|
|
247
|
+
expect: data.expect,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function deserializeStep(json: unknown): MongoMigrationStep {
|
|
252
|
+
const data = validate(StepJson, json, 'migration step');
|
|
253
|
+
return {
|
|
254
|
+
description: data.description,
|
|
255
|
+
command: deserializeDdlCommand(data.command),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function deserializeMongoOp(json: unknown): MongoMigrationPlanOperation {
|
|
260
|
+
const data = validate(OperationJson, json, 'migration operation');
|
|
261
|
+
return {
|
|
262
|
+
id: data.id,
|
|
263
|
+
label: data.label,
|
|
264
|
+
operationClass: data.operationClass as MigrationOperationClass,
|
|
265
|
+
precheck: data.precheck.map(deserializeCheck),
|
|
266
|
+
execute: data.execute.map(deserializeStep),
|
|
267
|
+
postcheck: data.postcheck.map(deserializeCheck),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function deserializeMongoOps(json: readonly unknown[]): MongoMigrationPlanOperation[] {
|
|
272
|
+
return json.map(deserializeMongoOp);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function serializeMongoOps(ops: readonly MongoMigrationPlanOperation[]): string {
|
|
276
|
+
return JSON.stringify(ops, null, 2);
|
|
277
|
+
}
|