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

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,4 +1,5 @@
1
1
  import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
+ import { errorRunnerFailed } from '@prisma-next/errors/execution';
2
3
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
3
4
  import type {
4
5
  MigrationOperationPolicy,
@@ -8,13 +9,29 @@ import type {
8
9
  MigrationRunnerFailure,
9
10
  MigrationRunnerResult,
10
11
  } from '@prisma-next/framework-components/control';
12
+ import type { MongoAdapter, MongoDriver } from '@prisma-next/mongo-lowering';
11
13
  import type {
14
+ AnyMongoMigrationOperation,
15
+ MongoDataTransformCheck,
16
+ MongoDataTransformOperation,
12
17
  MongoDdlCommandVisitor,
13
18
  MongoInspectionCommandVisitor,
14
19
  MongoMigrationCheck,
15
20
  MongoMigrationPlanOperation,
16
21
  } from '@prisma-next/mongo-query-ast/control';
17
22
  import { notOk, ok } from '@prisma-next/utils/result';
23
+
24
+ const READ_ONLY_CHECK_COMMAND_KINDS: ReadonlySet<string> = new Set(['aggregate', 'rawAggregate']);
25
+
26
+ function hasProfileHash(value: unknown): value is { readonly profileHash: string } {
27
+ return (
28
+ typeof value === 'object' &&
29
+ value !== null &&
30
+ Object.hasOwn(value, 'profileHash') &&
31
+ typeof (value as { profileHash: unknown }).profileHash === 'string'
32
+ );
33
+ }
34
+
18
35
  import { FilterEvaluator } from './filter-evaluator';
19
36
  import { deserializeMongoOps } from './mongo-ops-serializer';
20
37
 
@@ -38,6 +55,8 @@ export interface MarkerOperations {
38
55
  export interface MongoRunnerDependencies {
39
56
  readonly commandExecutor: MongoDdlCommandVisitor<Promise<void>>;
40
57
  readonly inspectionExecutor: MongoInspectionCommandVisitor<Promise<Record<string, unknown>[]>>;
58
+ readonly adapter: MongoAdapter;
59
+ readonly driver: MongoDriver;
41
60
  readonly markerOps: MarkerOperations;
42
61
  }
43
62
 
@@ -67,7 +86,7 @@ export class MongoMigrationRunner {
67
86
  readonly executionChecks?: MigrationRunnerExecutionChecks;
68
87
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
69
88
  }): Promise<MigrationRunnerResult> {
70
- const { commandExecutor, inspectionExecutor, markerOps } = this.deps;
89
+ const { commandExecutor, inspectionExecutor, adapter, driver, markerOps } = this.deps;
71
90
  const operations = deserializeMongoOps(options.plan.operations as readonly unknown[]);
72
91
 
73
92
  const policyCheck = this.enforcePolicyCompatibility(options.policy, operations);
@@ -90,9 +109,26 @@ export class MongoMigrationRunner {
90
109
  for (const operation of operations) {
91
110
  options.callbacks?.onOperationStart?.(operation);
92
111
  try {
112
+ if (operation.operationClass === 'data') {
113
+ const result = await this.executeDataTransform(
114
+ operation as MongoDataTransformOperation,
115
+ adapter,
116
+ driver,
117
+ filterEvaluator,
118
+ runIdempotency,
119
+ runPrechecks,
120
+ runPostchecks,
121
+ );
122
+ if (result.failure) return result.failure;
123
+ if (result.executed) operationsExecuted += 1;
124
+ continue;
125
+ }
126
+
127
+ const ddlOp = operation as MongoMigrationPlanOperation;
128
+
93
129
  if (runPostchecks && runIdempotency) {
94
130
  const allSatisfied = await this.allChecksSatisfied(
95
- operation.postcheck,
131
+ ddlOp.postcheck,
96
132
  inspectionExecutor,
97
133
  filterEvaluator,
98
134
  );
@@ -101,7 +137,7 @@ export class MongoMigrationRunner {
101
137
 
102
138
  if (runPrechecks) {
103
139
  const precheckResult = await this.evaluateChecks(
104
- operation.precheck,
140
+ ddlOp.precheck,
105
141
  inspectionExecutor,
106
142
  filterEvaluator,
107
143
  );
@@ -114,13 +150,13 @@ export class MongoMigrationRunner {
114
150
  }
115
151
  }
116
152
 
117
- for (const step of operation.execute) {
153
+ for (const step of ddlOp.execute) {
118
154
  await step.command.accept(commandExecutor);
119
155
  }
120
156
 
121
157
  if (runPostchecks) {
122
158
  const postcheckResult = await this.evaluateChecks(
123
- operation.postcheck,
159
+ ddlOp.postcheck,
124
160
  inspectionExecutor,
125
161
  filterEvaluator,
126
162
  );
@@ -140,8 +176,9 @@ export class MongoMigrationRunner {
140
176
  }
141
177
 
142
178
  const destination = options.plan.destination;
143
- const contract = options.destinationContract as { profileHash?: string };
144
- const profileHash = contract.profileHash ?? destination.storageHash;
179
+ const profileHash = hasProfileHash(options.destinationContract)
180
+ ? options.destinationContract.profileHash
181
+ : destination.storageHash;
145
182
 
146
183
  if (
147
184
  operationsExecuted === 0 &&
@@ -185,6 +222,105 @@ export class MongoMigrationRunner {
185
222
  return ok({ operationsPlanned: operations.length, operationsExecuted });
186
223
  }
187
224
 
225
+ private async executeDataTransform(
226
+ op: MongoDataTransformOperation,
227
+ adapter: MongoAdapter,
228
+ driver: MongoDriver,
229
+ filterEvaluator: FilterEvaluator,
230
+ runIdempotency: boolean,
231
+ runPrechecks: boolean,
232
+ runPostchecks: boolean,
233
+ ): Promise<{ executed: boolean; failure?: MigrationRunnerResult }> {
234
+ if (runPostchecks && runIdempotency && op.postcheck.length > 0) {
235
+ const allSatisfied = await this.evaluateDataTransformChecks(
236
+ op.postcheck,
237
+ adapter,
238
+ driver,
239
+ filterEvaluator,
240
+ );
241
+ if (allSatisfied) return { executed: false };
242
+ }
243
+
244
+ if (runPrechecks && op.precheck.length > 0) {
245
+ const passed = await this.evaluateDataTransformChecks(
246
+ op.precheck,
247
+ adapter,
248
+ driver,
249
+ filterEvaluator,
250
+ );
251
+ if (!passed) {
252
+ return {
253
+ executed: false,
254
+ failure: runnerFailure('PRECHECK_FAILED', `Operation ${op.id} failed during precheck`, {
255
+ meta: { operationId: op.id, name: op.name },
256
+ }),
257
+ };
258
+ }
259
+ }
260
+
261
+ for (const plan of op.run) {
262
+ const wireCommand = adapter.lower(plan);
263
+ for await (const _ of driver.execute(wireCommand)) {
264
+ /* consume */
265
+ }
266
+ }
267
+
268
+ if (runPostchecks && op.postcheck.length > 0) {
269
+ const passed = await this.evaluateDataTransformChecks(
270
+ op.postcheck,
271
+ adapter,
272
+ driver,
273
+ filterEvaluator,
274
+ );
275
+ if (!passed) {
276
+ return {
277
+ executed: false,
278
+ failure: runnerFailure('POSTCHECK_FAILED', `Operation ${op.id} failed during postcheck`, {
279
+ meta: { operationId: op.id, name: op.name },
280
+ }),
281
+ };
282
+ }
283
+ }
284
+
285
+ return { executed: true };
286
+ }
287
+
288
+ private async evaluateDataTransformChecks(
289
+ checks: readonly MongoDataTransformCheck[],
290
+ adapter: MongoAdapter,
291
+ driver: MongoDriver,
292
+ filterEvaluator: FilterEvaluator,
293
+ ): Promise<boolean> {
294
+ for (const check of checks) {
295
+ const commandKind = check.source.command.kind;
296
+ if (!READ_ONLY_CHECK_COMMAND_KINDS.has(commandKind)) {
297
+ throw errorRunnerFailed(
298
+ `Data-transform check rejected: command kind "${commandKind}" is not read-only`,
299
+ {
300
+ why: 'Data-transform checks must use aggregate or rawAggregate commands so the pre/postcheck path cannot mutate the database.',
301
+ fix: 'Author the check.source as an aggregate pipeline (or rawAggregate) rather than a DML write command.',
302
+ meta: {
303
+ checkDescription: check.description,
304
+ commandKind,
305
+ collection: check.source.collection,
306
+ },
307
+ },
308
+ );
309
+ }
310
+ const wireCommand = adapter.lower(check.source);
311
+ let matchFound = false;
312
+ for await (const row of driver.execute<Record<string, unknown>>(wireCommand)) {
313
+ if (filterEvaluator.evaluate(check.filter, row)) {
314
+ matchFound = true;
315
+ break;
316
+ }
317
+ }
318
+ const passed = check.expect === 'exists' ? matchFound : !matchFound;
319
+ if (!passed) return false;
320
+ }
321
+ return true;
322
+ }
323
+
188
324
  private async evaluateChecks(
189
325
  checks: readonly MongoMigrationCheck[],
190
326
  inspectionExecutor: MongoInspectionCommandVisitor<Promise<Record<string, unknown>[]>>,
@@ -212,7 +348,7 @@ export class MongoMigrationRunner {
212
348
 
213
349
  private enforcePolicyCompatibility(
214
350
  policy: MigrationOperationPolicy,
215
- operations: readonly MongoMigrationPlanOperation[],
351
+ operations: readonly AnyMongoMigrationOperation[],
216
352
  ): MigrationRunnerResult | undefined {
217
353
  const allowedClasses = new Set(policy.allowedOperationClasses);
218
354
  for (const operation of operations) {
@@ -3,6 +3,7 @@ export {
3
3
  collMod,
4
4
  createCollection,
5
5
  createIndex,
6
+ dataTransform,
6
7
  dropCollection,
7
8
  dropIndex,
8
9
  setValidation,
@@ -1 +0,0 @@
1
- {"version":3,"file":"migration-factories-Brzz-QGG.mjs","names":[],"sources":["../src/core/migration-factories.ts"],"sourcesContent":["import type { MongoIndexKey } from '@prisma-next/mongo-query-ast/control';\nimport {\n buildIndexOpId,\n CollModCommand,\n type CollModOptions,\n CreateCollectionCommand,\n type CreateCollectionOptions,\n CreateIndexCommand,\n type CreateIndexOptions,\n DropCollectionCommand,\n DropIndexCommand,\n defaultMongoIndexName,\n keysToKeySpec,\n ListCollectionsCommand,\n ListIndexesCommand,\n MongoAndExpr,\n MongoFieldFilter,\n type MongoMigrationPlanOperation,\n} from '@prisma-next/mongo-query-ast/control';\nimport type { CollModMeta } from './op-factory-call';\n\nfunction formatKeys(keys: ReadonlyArray<MongoIndexKey>): string {\n return keys.map((k) => `${k.field}:${k.direction}`).join(', ');\n}\n\nfunction isTextIndex(keys: ReadonlyArray<MongoIndexKey>): boolean {\n return keys.some((k) => k.direction === 'text');\n}\n\nfunction keyFilter(keys: ReadonlyArray<MongoIndexKey>) {\n return isTextIndex(keys)\n ? MongoFieldFilter.eq('key._fts', 'text')\n : MongoFieldFilter.eq('key', keysToKeySpec(keys));\n}\n\nexport function createIndex(\n collection: string,\n keys: ReadonlyArray<MongoIndexKey>,\n options?: CreateIndexOptions,\n): MongoMigrationPlanOperation {\n const name = defaultMongoIndexName(keys);\n const filter = keyFilter(keys);\n const fullFilter = options?.unique\n ? MongoAndExpr.of([filter, MongoFieldFilter.eq('unique', true)])\n : filter;\n\n return {\n id: buildIndexOpId('create', collection, keys),\n label: `Create index on ${collection} (${formatKeys(keys)})`,\n operationClass: 'additive',\n precheck: [\n {\n description: `index does not already exist on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'notExists',\n },\n ],\n execute: [\n {\n description: `create index on ${collection}`,\n command: new CreateIndexCommand(collection, keys, {\n ...options,\n unique: options?.unique ?? undefined,\n name,\n }),\n },\n ],\n postcheck: [\n {\n description: `index exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter: fullFilter,\n expect: 'exists',\n },\n ],\n };\n}\n\nexport function dropIndex(\n collection: string,\n keys: ReadonlyArray<MongoIndexKey>,\n): MongoMigrationPlanOperation {\n const indexName = defaultMongoIndexName(keys);\n const filter = keyFilter(keys);\n\n return {\n id: buildIndexOpId('drop', collection, keys),\n label: `Drop index on ${collection} (${formatKeys(keys)})`,\n operationClass: 'destructive',\n precheck: [\n {\n description: `index exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'exists',\n },\n ],\n execute: [\n {\n description: `drop index on ${collection}`,\n command: new DropIndexCommand(collection, indexName),\n },\n ],\n postcheck: [\n {\n description: `index no longer exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'notExists',\n },\n ],\n };\n}\n\nexport function createCollection(\n collection: string,\n options?: CreateCollectionOptions,\n): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.create`,\n label: `Create collection ${collection}`,\n operationClass: 'additive',\n precheck: [\n {\n description: `collection ${collection} does not exist`,\n source: new ListCollectionsCommand(),\n filter: MongoFieldFilter.eq('name', collection),\n expect: 'notExists',\n },\n ],\n execute: [\n {\n description: `create collection ${collection}`,\n command: new CreateCollectionCommand(collection, options),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function dropCollection(collection: string): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.drop`,\n label: `Drop collection ${collection}`,\n operationClass: 'destructive',\n precheck: [],\n execute: [\n {\n description: `drop collection ${collection}`,\n command: new DropCollectionCommand(collection),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function setValidation(\n collection: string,\n schema: Record<string, unknown>,\n options?: { validationLevel?: 'strict' | 'moderate'; validationAction?: 'error' | 'warn' },\n): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.setValidation`,\n label: `Set validation on ${collection}`,\n operationClass: 'destructive',\n precheck: [],\n execute: [\n {\n description: `set validation on ${collection}`,\n command: new CollModCommand(collection, {\n validator: { $jsonSchema: schema },\n validationLevel: options?.validationLevel,\n validationAction: options?.validationAction,\n }),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function collMod(\n collection: string,\n options: CollModOptions,\n meta?: CollModMeta,\n): MongoMigrationPlanOperation {\n const hasValidator = options.validator != null && Object.keys(options.validator).length > 0;\n\n return {\n id: meta?.id ?? `collection.${collection}.collMod`,\n label: meta?.label ?? `Modify collection ${collection}`,\n operationClass: meta?.operationClass ?? 'destructive',\n precheck:\n options.validator != null\n ? [\n {\n description: `collection ${collection} exists`,\n source: new ListCollectionsCommand(),\n filter: MongoFieldFilter.eq('name', collection),\n expect: 'exists' as const,\n },\n ]\n : [],\n execute: [\n {\n description: `modify ${collection}`,\n command: new CollModCommand(collection, options),\n },\n ],\n postcheck: hasValidator\n ? [\n {\n description: `validator applied on ${collection}`,\n source: new ListCollectionsCommand(),\n filter: MongoAndExpr.of([\n MongoFieldFilter.eq('name', collection),\n ...(options.validationLevel\n ? [MongoFieldFilter.eq('options.validationLevel', options.validationLevel)]\n : []),\n ...(options.validationAction\n ? [MongoFieldFilter.eq('options.validationAction', options.validationAction)]\n : []),\n ]),\n expect: 'exists' as const,\n },\n ]\n : [],\n };\n}\n\nexport function validatedCollection(\n name: string,\n schema: Record<string, unknown>,\n indexes: ReadonlyArray<{ keys: MongoIndexKey[]; unique?: boolean }>,\n): MongoMigrationPlanOperation[] {\n return [\n createCollection(name, {\n validator: { $jsonSchema: schema },\n validationLevel: 'strict',\n validationAction: 'error',\n }),\n ...indexes.map((idx) => createIndex(name, idx.keys, { unique: idx.unique })),\n ];\n}\n"],"mappings":"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"}