@prisma-next/target-mongo 0.4.0-dev.8 → 0.4.1

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.
@@ -1,76 +1,77 @@
1
1
  import { MongoSchemaCollection, MongoSchemaIndex } from "@prisma-next/mongo-schema-ir";
2
- import { CollModOptions, CreateCollectionOptions, CreateIndexOptions, MongoIndexKey } from "@prisma-next/mongo-query-ast/control";
3
- import { MigrationOperationClass } from "@prisma-next/framework-components/control";
2
+ import { CollModOptions, CreateCollectionOptions, CreateIndexOptions, MongoIndexKey, MongoMigrationPlanOperation } from "@prisma-next/mongo-query-ast/control";
3
+ import { ImportRequirement, TsExpression } from "@prisma-next/ts-render";
4
+ import { MigrationOperationClass, OpFactoryCall } from "@prisma-next/framework-components/control";
4
5
 
5
6
  //#region src/core/op-factory-call.d.ts
7
+
6
8
  interface CollModMeta {
7
9
  readonly id?: string;
8
10
  readonly label?: string;
9
11
  readonly operationClass?: MigrationOperationClass;
10
12
  }
11
- interface OpFactoryCallVisitor<R> {
12
- createIndex(call: CreateIndexCall): R;
13
- dropIndex(call: DropIndexCall): R;
14
- createCollection(call: CreateCollectionCall): R;
15
- dropCollection(call: DropCollectionCall): R;
16
- collMod(call: CollModCall): R;
17
- }
18
- declare abstract class OpFactoryCallNode {
19
- abstract readonly factory: string;
13
+ declare abstract class OpFactoryCallNode extends TsExpression implements OpFactoryCall {
14
+ abstract readonly factoryName: string;
20
15
  abstract readonly operationClass: MigrationOperationClass;
21
16
  abstract readonly label: string;
22
- abstract accept<R>(visitor: OpFactoryCallVisitor<R>): R;
17
+ abstract toOp(): MongoMigrationPlanOperation;
18
+ importRequirements(): readonly ImportRequirement[];
23
19
  protected freeze(): void;
24
20
  }
25
21
  declare class CreateIndexCall extends OpFactoryCallNode {
26
- readonly factory: "createIndex";
22
+ readonly factoryName: "createIndex";
27
23
  readonly operationClass: "additive";
28
24
  readonly collection: string;
29
25
  readonly keys: ReadonlyArray<MongoIndexKey>;
30
26
  readonly options: CreateIndexOptions | undefined;
31
27
  readonly label: string;
32
28
  constructor(collection: string, keys: ReadonlyArray<MongoIndexKey>, options?: CreateIndexOptions);
33
- accept<R>(visitor: OpFactoryCallVisitor<R>): R;
29
+ toOp(): MongoMigrationPlanOperation;
30
+ renderTypeScript(): string;
34
31
  }
35
32
  declare class DropIndexCall extends OpFactoryCallNode {
36
- readonly factory: "dropIndex";
33
+ readonly factoryName: "dropIndex";
37
34
  readonly operationClass: "destructive";
38
35
  readonly collection: string;
39
36
  readonly keys: ReadonlyArray<MongoIndexKey>;
40
37
  readonly label: string;
41
38
  constructor(collection: string, keys: ReadonlyArray<MongoIndexKey>);
42
- accept<R>(visitor: OpFactoryCallVisitor<R>): R;
39
+ toOp(): MongoMigrationPlanOperation;
40
+ renderTypeScript(): string;
43
41
  }
44
42
  declare class CreateCollectionCall extends OpFactoryCallNode {
45
- readonly factory: "createCollection";
43
+ readonly factoryName: "createCollection";
46
44
  readonly operationClass: "additive";
47
45
  readonly collection: string;
48
46
  readonly options: CreateCollectionOptions | undefined;
49
47
  readonly label: string;
50
48
  constructor(collection: string, options?: CreateCollectionOptions);
51
- accept<R>(visitor: OpFactoryCallVisitor<R>): R;
49
+ toOp(): MongoMigrationPlanOperation;
50
+ renderTypeScript(): string;
52
51
  }
53
52
  declare class DropCollectionCall extends OpFactoryCallNode {
54
- readonly factory: "dropCollection";
53
+ readonly factoryName: "dropCollection";
55
54
  readonly operationClass: "destructive";
56
55
  readonly collection: string;
57
56
  readonly label: string;
58
57
  constructor(collection: string);
59
- accept<R>(visitor: OpFactoryCallVisitor<R>): R;
58
+ toOp(): MongoMigrationPlanOperation;
59
+ renderTypeScript(): string;
60
60
  }
61
61
  declare class CollModCall extends OpFactoryCallNode {
62
- readonly factory: "collMod";
62
+ readonly factoryName: "collMod";
63
63
  readonly collection: string;
64
64
  readonly options: CollModOptions;
65
65
  readonly meta: CollModMeta | undefined;
66
66
  readonly operationClass: MigrationOperationClass;
67
67
  readonly label: string;
68
68
  constructor(collection: string, options: CollModOptions, meta?: CollModMeta);
69
- accept<R>(visitor: OpFactoryCallVisitor<R>): R;
69
+ toOp(): MongoMigrationPlanOperation;
70
+ renderTypeScript(): string;
70
71
  }
71
- type OpFactoryCall = CreateIndexCall | DropIndexCall | CreateCollectionCall | DropCollectionCall | CollModCall;
72
+ type OpFactoryCall$1 = CreateIndexCall | DropIndexCall | CreateCollectionCall | DropCollectionCall | CollModCall;
72
73
  declare function schemaIndexToCreateIndexOptions(index: MongoSchemaIndex): CreateIndexOptions;
73
74
  declare function schemaCollectionToCreateCollectionOptions(coll: MongoSchemaCollection): CreateCollectionOptions | undefined;
74
75
  //#endregion
75
- export { DropCollectionCall as a, OpFactoryCallVisitor as c, CreateIndexCall as i, schemaCollectionToCreateCollectionOptions as l, CollModMeta as n, DropIndexCall as o, CreateCollectionCall as r, OpFactoryCall as s, CollModCall as t, schemaIndexToCreateIndexOptions as u };
76
- //# sourceMappingURL=op-factory-call-CfPGebEH.d.mts.map
76
+ export { DropCollectionCall as a, schemaCollectionToCreateCollectionOptions as c, CreateIndexCall as i, schemaIndexToCreateIndexOptions as l, CollModMeta as n, DropIndexCall as o, CreateCollectionCall as r, OpFactoryCall$1 as s, CollModCall as t };
77
+ //# sourceMappingURL=op-factory-call-BjNAcPSF.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"op-factory-call-BjNAcPSF.d.mts","names":[],"sources":["../src/core/op-factory-call.ts"],"sourcesContent":[],"mappings":";;;;;;;AAwEqC,UA3BpB,WAAA,CA2BoB;EAAiB,SAAA,EAAA,CAAA,EAAA,MAAA;EAgCzC,SAAA,KAAA,CAAA,EAAc,MAAA;EAII,SAAA,cAAA,CAAA,EA5DH,uBA4DG;;uBAvDhB,iBAAA,SAA0B,YAAA,YAAwB,aA0DX,CAAA;EAAd,kBAAA,WAAA,EAAA,MAAA;EAQ9B,kBAAA,cAAA,EAhE0B,uBAgE1B;EAfyB,kBAAA,KAAA,EAAA,MAAA;EAAiB,SAAA,IAAA,CAAA,CAAA,EA/CjC,2BA+CiC;EAwBvC,kBAAA,CAAA,CAAA,EAAA,SArEoB,iBAqEC,EAAA;EAId,UAAA,MAAA,CAAA,CAAA,EAAA,IAAA;;AAWV,cAvEG,eAAA,SAAwB,iBAAA,CAuE3B;EAfgC,SAAA,WAAA,EAAA,aAAA;EAAiB,SAAA,cAAA,EAAA,UAAA;EA0B9C,SAAA,UAAA,EAAmB,MAAA;EAsBnB,SAAA,IAAA,EApGI,aAoGQ,CApGM,aAoGN,CAAA;EAGL,SAAA,OAAA,EAtGA,kBAsGA,GAAA,SAAA;EACH,SAAA,KAAA,EAAA,MAAA;EACU,WAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAnGjB,aAmGiB,CAnGH,aAmGG,CAAA,EAAA,OAAA,CAAA,EAlGb,kBAkGa;EAGgB,IAAA,CAAA,CAAA,EA3FjC,2BA2FiC;EAAuB,gBAAA,CAAA,CAAA,EAAA,MAAA;;AARjC,cAxEpB,aAAA,SAAsB,iBAAA,CAwEF;EAAiB,SAAA,WAAA,EAAA,WAAA;EA6BtC,SAAA,cAAa,EAAA,aAAA;EACrB,SAAA,UAAA,EAAA,MAAA;EACA,SAAA,IAAA,EAnGa,aAmGb,CAnG2B,aAmG3B,CAAA;EACA,SAAA,KAAA,EAAA,MAAA;EACA,WAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAlGoC,aAkGpC,CAlGkD,aAkGlD,CAAA;EACA,IAAA,CAAA,CAAA,EA3FM,2BA2FN;EAAW,gBAAA,CAAA,CAAA,EAAA,MAAA;AAEf;AAcgB,cAlGH,oBAAA,SAA6B,iBAAA,CAmGlC;;;;oBA/FY;;4CAGwB;UAQlC;;;cAWG,kBAAA,SAA2B,iBAAA;;;;;;UAa9B;;;cASG,WAAA,SAAoB,iBAAA;;;oBAGb;iBACH;2BACU;;2CAGgB,uBAAuB;UAUxD;;;KAWE,eAAA,GACR,kBACA,gBACA,uBACA,qBACA;iBAEY,+BAAA,QAAuC,mBAAmB;iBAc1D,yCAAA,OACR,wBACL"}
package/package.json CHANGED
@@ -1,21 +1,23 @@
1
1
  {
2
2
  "name": "@prisma-next/target-mongo",
3
- "version": "0.4.0-dev.8",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "MongoDB target pack for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.1.29",
9
9
  "mongodb": "^6.16.0",
10
- "@prisma-next/contract": "0.4.0-dev.8",
11
- "@prisma-next/framework-components": "0.4.0-dev.8",
12
- "@prisma-next/mongo-contract": "0.4.0-dev.8",
13
- "@prisma-next/mongo-query-ast": "0.4.0-dev.8",
14
- "@prisma-next/migration-tools": "0.4.0-dev.8",
15
- "@prisma-next/errors": "0.4.0-dev.8",
16
- "@prisma-next/mongo-value": "0.4.0-dev.8",
17
- "@prisma-next/mongo-schema-ir": "0.4.0-dev.8",
18
- "@prisma-next/utils": "0.4.0-dev.8"
10
+ "@prisma-next/contract": "0.4.1",
11
+ "@prisma-next/migration-tools": "0.4.1",
12
+ "@prisma-next/framework-components": "0.4.1",
13
+ "@prisma-next/errors": "0.4.1",
14
+ "@prisma-next/ts-render": "0.4.1",
15
+ "@prisma-next/mongo-lowering": "0.4.1",
16
+ "@prisma-next/mongo-contract": "0.4.1",
17
+ "@prisma-next/mongo-query-ast": "0.4.1",
18
+ "@prisma-next/utils": "0.4.1",
19
+ "@prisma-next/mongo-schema-ir": "0.4.1",
20
+ "@prisma-next/mongo-value": "0.4.1"
19
21
  },
20
22
  "devDependencies": {
21
23
  "mongodb-memory-server": "10.4.3",
@@ -1,4 +1,9 @@
1
- import type { MongoIndexKey } from '@prisma-next/mongo-query-ast/control';
1
+ import type {
2
+ MongoDataTransformCheck,
3
+ MongoDataTransformOperation,
4
+ MongoFilterExpr,
5
+ MongoIndexKey,
6
+ } from '@prisma-next/mongo-query-ast/control';
2
7
  import {
3
8
  buildIndexOpId,
4
9
  CollModCommand,
@@ -14,11 +19,74 @@ import {
14
19
  ListCollectionsCommand,
15
20
  ListIndexesCommand,
16
21
  MongoAndExpr,
22
+ MongoExistsExpr,
17
23
  MongoFieldFilter,
18
24
  type MongoMigrationPlanOperation,
19
25
  } from '@prisma-next/mongo-query-ast/control';
26
+ import type { MongoQueryPlan } from '@prisma-next/mongo-query-ast/execution';
20
27
  import type { CollModMeta } from './op-factory-call';
21
28
 
29
+ interface Buildable {
30
+ build(): MongoQueryPlan;
31
+ }
32
+
33
+ function isBuildable(value: unknown): value is Buildable {
34
+ return (
35
+ typeof value === 'object' &&
36
+ value !== null &&
37
+ 'build' in value &&
38
+ typeof (value as { build: unknown }).build === 'function'
39
+ );
40
+ }
41
+
42
+ function resolveQuery(value: MongoQueryPlan | Buildable): MongoQueryPlan {
43
+ return isBuildable(value) ? value.build() : value;
44
+ }
45
+
46
+ // Every MongoDB document carries `_id`, so `exists('_id')` is equivalent to
47
+ // "match all". The filter AST has no identity/always-true expression.
48
+ const MATCH_ALL_FILTER: MongoFilterExpr = MongoExistsExpr.exists('_id');
49
+
50
+ export function dataTransform(
51
+ name: string,
52
+ options: {
53
+ check?: {
54
+ source: () => MongoQueryPlan | Buildable;
55
+ filter?: MongoFilterExpr;
56
+ expect?: 'exists' | 'notExists';
57
+ description?: string;
58
+ };
59
+ run: () => MongoQueryPlan | Buildable;
60
+ },
61
+ ): MongoDataTransformOperation {
62
+ let precheck: readonly MongoDataTransformCheck[] = [];
63
+ let postcheck: readonly MongoDataTransformCheck[] = [];
64
+
65
+ if (options.check) {
66
+ const source = resolveQuery(options.check.source());
67
+ const filter = options.check.filter ?? MATCH_ALL_FILTER;
68
+ const description = options.check.description ?? `Check for data transform: ${name}`;
69
+ const precheckExpect = options.check.expect ?? 'exists';
70
+ const postcheckExpect: 'exists' | 'notExists' =
71
+ precheckExpect === 'exists' ? 'notExists' : 'exists';
72
+
73
+ precheck = [{ description, source, filter, expect: precheckExpect }];
74
+ postcheck = [{ description, source, filter, expect: postcheckExpect }];
75
+ }
76
+
77
+ const run: MongoQueryPlan[] = [resolveQuery(options.run())];
78
+
79
+ return {
80
+ id: `data_transform.${name}`,
81
+ label: `Data transform: ${name}`,
82
+ operationClass: 'data',
83
+ name,
84
+ precheck,
85
+ run,
86
+ postcheck,
87
+ };
88
+ }
89
+
22
90
  function formatKeys(keys: ReadonlyArray<MongoIndexKey>): string {
23
91
  return keys.map((k) => `${k.field}:${k.direction}`).join(', ');
24
92
  }
@@ -1,7 +1,9 @@
1
+ import type { PlanMeta } from '@prisma-next/contract/types';
1
2
  import type { MigrationOperationClass } from '@prisma-next/framework-components/control';
2
3
  import {
3
4
  type AnyMongoDdlCommand,
4
5
  type AnyMongoInspectionCommand,
6
+ type AnyMongoMigrationOperation,
5
7
  CollModCommand,
6
8
  CreateCollectionCommand,
7
9
  CreateIndexCommand,
@@ -10,6 +12,8 @@ import {
10
12
  ListCollectionsCommand,
11
13
  ListIndexesCommand,
12
14
  MongoAndExpr,
15
+ type MongoDataTransformCheck,
16
+ type MongoDataTransformOperation,
13
17
  MongoExistsExpr,
14
18
  MongoFieldFilter,
15
19
  type MongoFilterExpr,
@@ -19,6 +23,30 @@ import {
19
23
  MongoNotExpr,
20
24
  MongoOrExpr,
21
25
  } from '@prisma-next/mongo-query-ast/control';
26
+ import {
27
+ AggregateCommand,
28
+ type AnyMongoCommand,
29
+ MongoAddFieldsStage,
30
+ MongoLimitStage,
31
+ MongoLookupStage,
32
+ MongoMatchStage,
33
+ MongoMergeStage,
34
+ type MongoPipelineStage,
35
+ MongoProjectStage,
36
+ type MongoQueryPlan,
37
+ MongoSortStage,
38
+ type MongoUpdatePipelineStage,
39
+ RawAggregateCommand,
40
+ RawDeleteManyCommand,
41
+ RawDeleteOneCommand,
42
+ RawFindOneAndDeleteCommand,
43
+ RawFindOneAndUpdateCommand,
44
+ RawInsertManyCommand,
45
+ RawInsertOneCommand,
46
+ RawUpdateManyCommand,
47
+ RawUpdateOneCommand,
48
+ } from '@prisma-next/mongo-query-ast/execution';
49
+ import { ifDefined } from '@prisma-next/utils/defined';
22
50
  import { type } from 'arktype';
23
51
 
24
52
  const IndexKeyDirection = type('1 | -1 | "text" | "2dsphere" | "2d" | "hashed"');
@@ -97,6 +125,97 @@ const ExistsFilterJson = type({
97
125
  exists: 'boolean',
98
126
  });
99
127
 
128
+ // ============================================================================
129
+ // DML command schemas
130
+ // ============================================================================
131
+
132
+ const RawInsertOneJson = type({
133
+ kind: '"rawInsertOne"',
134
+ collection: 'string',
135
+ document: 'Record<string, unknown>',
136
+ });
137
+
138
+ const RawInsertManyJson = type({
139
+ kind: '"rawInsertMany"',
140
+ collection: 'string',
141
+ documents: 'Record<string, unknown>[]',
142
+ });
143
+
144
+ const RawUpdateOneJson = type({
145
+ kind: '"rawUpdateOne"',
146
+ collection: 'string',
147
+ filter: 'Record<string, unknown>',
148
+ update: 'Record<string, unknown> | Record<string, unknown>[]',
149
+ });
150
+
151
+ const RawUpdateManyJson = type({
152
+ kind: '"rawUpdateMany"',
153
+ collection: 'string',
154
+ filter: 'Record<string, unknown>',
155
+ update: 'Record<string, unknown> | Record<string, unknown>[]',
156
+ });
157
+
158
+ const RawDeleteOneJson = type({
159
+ kind: '"rawDeleteOne"',
160
+ collection: 'string',
161
+ filter: 'Record<string, unknown>',
162
+ });
163
+
164
+ const RawDeleteManyJson = type({
165
+ kind: '"rawDeleteMany"',
166
+ collection: 'string',
167
+ filter: 'Record<string, unknown>',
168
+ });
169
+
170
+ const RawAggregateJson = type({
171
+ kind: '"rawAggregate"',
172
+ collection: 'string',
173
+ pipeline: 'Record<string, unknown>[]',
174
+ });
175
+
176
+ const RawFindOneAndUpdateJson = type({
177
+ kind: '"rawFindOneAndUpdate"',
178
+ collection: 'string',
179
+ filter: 'Record<string, unknown>',
180
+ update: 'Record<string, unknown> | Record<string, unknown>[]',
181
+ upsert: 'boolean',
182
+ });
183
+
184
+ const RawFindOneAndDeleteJson = type({
185
+ kind: '"rawFindOneAndDelete"',
186
+ collection: 'string',
187
+ filter: 'Record<string, unknown>',
188
+ });
189
+
190
+ const TypedAggregateJson = type({
191
+ kind: '"aggregate"',
192
+ collection: 'string',
193
+ pipeline: 'Record<string, unknown>[]',
194
+ });
195
+
196
+ const PlanMetaJson = type({
197
+ target: 'string',
198
+ storageHash: 'string',
199
+ lane: 'string',
200
+ paramDescriptors: 'unknown[]',
201
+ 'targetFamily?': 'string',
202
+ 'profileHash?': 'string',
203
+ 'annotations?': 'Record<string, unknown>',
204
+ 'refs?': 'Record<string, unknown>',
205
+ 'projection?': 'Record<string, string> | string[]',
206
+ 'projectionTypes?': 'Record<string, string>',
207
+ });
208
+
209
+ const QueryPlanJson = type({
210
+ collection: 'string',
211
+ command: 'Record<string, unknown>',
212
+ meta: PlanMetaJson,
213
+ });
214
+
215
+ // ============================================================================
216
+ // DDL check/step schemas
217
+ // ============================================================================
218
+
100
219
  const CheckJson = type({
101
220
  description: 'string',
102
221
  source: 'Record<string, unknown>',
@@ -109,7 +228,7 @@ const StepJson = type({
109
228
  command: 'Record<string, unknown>',
110
229
  });
111
230
 
112
- const OperationJson = type({
231
+ const DdlOperationJson = type({
113
232
  id: 'string',
114
233
  label: 'string',
115
234
  operationClass: '"additive" | "widening" | "destructive"',
@@ -118,6 +237,23 @@ const OperationJson = type({
118
237
  postcheck: 'Record<string, unknown>[]',
119
238
  });
120
239
 
240
+ const DataTransformCheckJson = type({
241
+ description: 'string',
242
+ source: 'Record<string, unknown>',
243
+ filter: 'Record<string, unknown>',
244
+ expect: '"exists" | "notExists"',
245
+ });
246
+
247
+ const DataTransformOperationJson = type({
248
+ id: 'string',
249
+ label: 'string',
250
+ operationClass: '"data"',
251
+ name: 'string',
252
+ precheck: 'Record<string, unknown>[]',
253
+ run: 'Record<string, unknown>[]',
254
+ postcheck: 'Record<string, unknown>[]',
255
+ });
256
+
121
257
  function validate<T>(schema: { assert: (data: unknown) => T }, data: unknown, context: string): T {
122
258
  try {
123
259
  return schema.assert(data);
@@ -161,6 +297,151 @@ function deserializeFilterExpr(json: unknown): MongoFilterExpr {
161
297
  }
162
298
  }
163
299
 
300
+ // ============================================================================
301
+ // Pipeline stage deserialization
302
+ // ============================================================================
303
+
304
+ export function deserializePipelineStage(json: unknown): MongoPipelineStage {
305
+ const record = json as Record<string, unknown>;
306
+ const kind = record['kind'] as string;
307
+ switch (kind) {
308
+ case 'match':
309
+ return new MongoMatchStage(deserializeFilterExpr(record['filter']));
310
+ case 'limit':
311
+ return new MongoLimitStage(record['limit'] as number);
312
+ case 'sort':
313
+ return new MongoSortStage(record['sort'] as Record<string, 1 | -1>);
314
+ case 'project':
315
+ return new MongoProjectStage(record['projection'] as Record<string, 0 | 1>);
316
+ case 'addFields':
317
+ return new MongoAddFieldsStage(record['fields'] as Record<string, never>);
318
+ case 'lookup': {
319
+ const opts: {
320
+ from: string;
321
+ as: string;
322
+ localField?: string;
323
+ foreignField?: string;
324
+ pipeline?: ReadonlyArray<MongoPipelineStage>;
325
+ let_?: Record<string, never>;
326
+ } = {
327
+ from: record['from'] as string,
328
+ as: record['as'] as string,
329
+ };
330
+ if (record['localField'] !== undefined) opts.localField = record['localField'] as string;
331
+ if (record['foreignField'] !== undefined)
332
+ opts.foreignField = record['foreignField'] as string;
333
+ if (record['pipeline'] !== undefined)
334
+ opts.pipeline = (record['pipeline'] as unknown[]).map(deserializePipelineStage);
335
+ if (record['let_'] !== undefined) opts.let_ = record['let_'] as Record<string, never>;
336
+ return new MongoLookupStage(opts);
337
+ }
338
+ case 'merge': {
339
+ const opts: {
340
+ into: string | { db: string; coll: string };
341
+ on?: string | ReadonlyArray<string>;
342
+ whenMatched?: string | ReadonlyArray<MongoUpdatePipelineStage>;
343
+ whenNotMatched?: string;
344
+ } = {
345
+ into: record['into'] as string | { db: string; coll: string },
346
+ };
347
+ if (record['on'] !== undefined) opts.on = record['on'] as string | string[];
348
+ if (record['whenMatched'] !== undefined) {
349
+ const wm = record['whenMatched'];
350
+ opts.whenMatched =
351
+ typeof wm === 'string'
352
+ ? wm
353
+ : ((wm as unknown[]).map(deserializePipelineStage) as MongoUpdatePipelineStage[]);
354
+ }
355
+ if (record['whenNotMatched'] !== undefined)
356
+ opts.whenNotMatched = record['whenNotMatched'] as string;
357
+ return new MongoMergeStage(opts);
358
+ }
359
+ default:
360
+ throw new Error(`Unknown pipeline stage kind: ${kind}`);
361
+ }
362
+ }
363
+
364
+ // ============================================================================
365
+ // DML command deserialization
366
+ // ============================================================================
367
+
368
+ export function deserializeDmlCommand(json: unknown): AnyMongoCommand {
369
+ const record = json as Record<string, unknown>;
370
+ const kind = record['kind'] as string;
371
+ switch (kind) {
372
+ case 'rawInsertOne': {
373
+ const data = validate(RawInsertOneJson, json, 'rawInsertOne command');
374
+ return new RawInsertOneCommand(data.collection, data.document);
375
+ }
376
+ case 'rawInsertMany': {
377
+ const data = validate(RawInsertManyJson, json, 'rawInsertMany command');
378
+ return new RawInsertManyCommand(data.collection, data.documents);
379
+ }
380
+ case 'rawUpdateOne': {
381
+ const data = validate(RawUpdateOneJson, json, 'rawUpdateOne command');
382
+ return new RawUpdateOneCommand(data.collection, data.filter, data.update);
383
+ }
384
+ case 'rawUpdateMany': {
385
+ const data = validate(RawUpdateManyJson, json, 'rawUpdateMany command');
386
+ return new RawUpdateManyCommand(data.collection, data.filter, data.update);
387
+ }
388
+ case 'rawDeleteOne': {
389
+ const data = validate(RawDeleteOneJson, json, 'rawDeleteOne command');
390
+ return new RawDeleteOneCommand(data.collection, data.filter);
391
+ }
392
+ case 'rawDeleteMany': {
393
+ const data = validate(RawDeleteManyJson, json, 'rawDeleteMany command');
394
+ return new RawDeleteManyCommand(data.collection, data.filter);
395
+ }
396
+ case 'rawAggregate': {
397
+ const data = validate(RawAggregateJson, json, 'rawAggregate command');
398
+ return new RawAggregateCommand(data.collection, data.pipeline);
399
+ }
400
+ case 'rawFindOneAndUpdate': {
401
+ const data = validate(RawFindOneAndUpdateJson, json, 'rawFindOneAndUpdate command');
402
+ return new RawFindOneAndUpdateCommand(data.collection, data.filter, data.update, data.upsert);
403
+ }
404
+ case 'rawFindOneAndDelete': {
405
+ const data = validate(RawFindOneAndDeleteJson, json, 'rawFindOneAndDelete command');
406
+ return new RawFindOneAndDeleteCommand(data.collection, data.filter);
407
+ }
408
+ case 'aggregate': {
409
+ const data = validate(TypedAggregateJson, json, 'aggregate command');
410
+ const pipeline = data.pipeline.map(deserializePipelineStage);
411
+ return new AggregateCommand(data.collection, pipeline);
412
+ }
413
+ default:
414
+ throw new Error(`Unknown DML command kind: ${kind}`);
415
+ }
416
+ }
417
+
418
+ // ============================================================================
419
+ // MongoQueryPlan deserialization
420
+ // ============================================================================
421
+
422
+ export function deserializeMongoQueryPlan(json: unknown): MongoQueryPlan {
423
+ const data = validate(QueryPlanJson, json, 'Mongo query plan');
424
+ const command = deserializeDmlCommand(data.command);
425
+ const m = data.meta;
426
+ const meta: PlanMeta = {
427
+ target: m.target,
428
+ storageHash: m.storageHash,
429
+ lane: m.lane,
430
+ paramDescriptors: m.paramDescriptors as PlanMeta['paramDescriptors'],
431
+ ...ifDefined('targetFamily', m.targetFamily),
432
+ ...ifDefined('profileHash', m.profileHash),
433
+ ...ifDefined('annotations', m.annotations),
434
+ ...ifDefined('refs', m.refs),
435
+ ...ifDefined('projection', m.projection),
436
+ ...ifDefined('projectionTypes', m.projectionTypes),
437
+ };
438
+ return { collection: data.collection, command, meta };
439
+ }
440
+
441
+ // ============================================================================
442
+ // DDL command deserialization
443
+ // ============================================================================
444
+
164
445
  function deserializeDdlCommand(json: unknown): AnyMongoDdlCommand {
165
446
  const record = json as Record<string, unknown>;
166
447
  const kind = record['kind'] as string;
@@ -256,8 +537,16 @@ function deserializeStep(json: unknown): MongoMigrationStep {
256
537
  };
257
538
  }
258
539
 
259
- export function deserializeMongoOp(json: unknown): MongoMigrationPlanOperation {
260
- const data = validate(OperationJson, json, 'migration operation');
540
+ function isDataTransformJson(json: unknown): boolean {
541
+ return (
542
+ typeof json === 'object' &&
543
+ json !== null &&
544
+ (json as Record<string, unknown>)['operationClass'] === 'data'
545
+ );
546
+ }
547
+
548
+ function deserializeDdlOp(json: unknown): MongoMigrationPlanOperation {
549
+ const data = validate(DdlOperationJson, json, 'migration operation');
261
550
  return {
262
551
  id: data.id,
263
552
  label: data.label,
@@ -268,10 +557,40 @@ export function deserializeMongoOp(json: unknown): MongoMigrationPlanOperation {
268
557
  };
269
558
  }
270
559
 
271
- export function deserializeMongoOps(json: readonly unknown[]): MongoMigrationPlanOperation[] {
560
+ function deserializeDataTransformCheck(json: unknown): MongoDataTransformCheck {
561
+ const data = validate(DataTransformCheckJson, json, 'data transform check');
562
+ return {
563
+ description: data.description,
564
+ source: deserializeMongoQueryPlan(data.source),
565
+ filter: deserializeFilterExpr(data.filter),
566
+ expect: data.expect,
567
+ };
568
+ }
569
+
570
+ function deserializeDataTransformOp(json: unknown): MongoDataTransformOperation {
571
+ const data = validate(DataTransformOperationJson, json, 'data transform operation');
572
+ return {
573
+ id: data.id,
574
+ label: data.label,
575
+ operationClass: 'data',
576
+ name: data.name,
577
+ precheck: data.precheck.map(deserializeDataTransformCheck),
578
+ run: data.run.map(deserializeMongoQueryPlan),
579
+ postcheck: data.postcheck.map(deserializeDataTransformCheck),
580
+ };
581
+ }
582
+
583
+ export function deserializeMongoOp(json: unknown): AnyMongoMigrationOperation {
584
+ if (isDataTransformJson(json)) {
585
+ return deserializeDataTransformOp(json);
586
+ }
587
+ return deserializeDdlOp(json);
588
+ }
589
+
590
+ export function deserializeMongoOps(json: readonly unknown[]): AnyMongoMigrationOperation[] {
272
591
  return json.map(deserializeMongoOp);
273
592
  }
274
593
 
275
- export function serializeMongoOps(ops: readonly MongoMigrationPlanOperation[]): string {
594
+ export function serializeMongoOps(ops: readonly AnyMongoMigrationOperation[]): string {
276
595
  return JSON.stringify(ops, null, 2);
277
596
  }
@@ -243,12 +243,11 @@ export class MongoMigrationPlanner implements MigrationPlanner<'mongo', 'mongo'>
243
243
  /**
244
244
  * Produce an empty `migration.ts` authoring surface for `migration new`.
245
245
  *
246
- * Mongo is a class-flow target, so the "empty migration" is a
247
- * `PlannerProducedMongoMigration` with no operations; `renderTypeScript()`
248
- * emits a stub class with the correct `from`/`to` metadata that the user
249
- * then fills in with operations. The contract path on the context is
250
- * unused — Mongo's emitted source does not import from the generated
251
- * contract `.d.ts`.
246
+ * The "empty migration" is a `PlannerProducedMongoMigration` with no
247
+ * operations; `renderTypeScript()` emits a stub class with the correct
248
+ * `from`/`to` metadata that the user then fills in with operations. The
249
+ * contract path on the context is unused Mongo's emitted source does
250
+ * not import from the generated contract `.d.ts`.
252
251
  */
253
252
  emptyMigration(context: MigrationScaffoldContext): MigrationPlanWithAuthoringSurface {
254
253
  return new PlannerProducedMongoMigration([], {