@prisma-next/target-mongo 0.4.1 → 0.4.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 +4 -1
- package/dist/control.d.mts +43 -22
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +126 -110
- package/dist/control.mjs.map +1 -1
- package/dist/descriptor-meta-D9_5quQi.mjs +14 -0
- package/dist/descriptor-meta-D9_5quQi.mjs.map +1 -0
- package/dist/{migration-factories-gwi81C8u.mjs → migration-factories-CoNYWrd1.mjs} +3 -1
- package/dist/migration-factories-CoNYWrd1.mjs.map +1 -0
- package/dist/migration.d.mts +7 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +1 -1
- package/dist/{op-factory-call-BjNAcPSF.d.mts → op-factory-call--nK5dk8n.d.mts} +1 -1
- package/dist/{op-factory-call-BjNAcPSF.d.mts.map → op-factory-call--nK5dk8n.d.mts.map} +1 -1
- package/dist/pack.mjs +1 -11
- package/dist/pack.mjs.map +1 -1
- package/dist/runtime.d.mts +20 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +28 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/schema-verify.d.mts +22 -0
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +3 -0
- package/dist/verify-mongo-schema-Daa7BMJY.mjs +582 -0
- package/dist/verify-mongo-schema-Daa7BMJY.mjs.map +1 -0
- package/package.json +19 -13
- package/src/core/marker-ledger.ts +90 -20
- package/src/core/migration-factories.ts +8 -0
- package/src/core/mongo-ops-serializer.ts +0 -8
- package/src/core/mongo-planner.ts +8 -2
- package/src/core/mongo-runner.ts +105 -70
- package/src/core/planner-produced-migration.ts +0 -1
- package/src/core/render-typescript.ts +28 -16
- package/src/core/schema-diff.ts +402 -0
- package/src/core/schema-verify/canonicalize-introspection.ts +389 -0
- package/src/core/schema-verify/verify-mongo-schema.ts +60 -0
- package/src/exports/runtime.ts +38 -0
- package/src/exports/schema-verify.ts +2 -0
- package/dist/migration-factories-gwi81C8u.mjs.map +0 -1
package/src/core/mongo-runner.ts
CHANGED
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
MigrationRunnerExecutionChecks,
|
|
9
9
|
MigrationRunnerFailure,
|
|
10
10
|
MigrationRunnerResult,
|
|
11
|
+
OperationContext,
|
|
11
12
|
} from '@prisma-next/framework-components/control';
|
|
13
|
+
import type { MongoContract } from '@prisma-next/mongo-contract';
|
|
12
14
|
import type { MongoAdapter, MongoDriver } from '@prisma-next/mongo-lowering';
|
|
13
15
|
import type {
|
|
14
16
|
AnyMongoMigrationOperation,
|
|
@@ -19,31 +21,28 @@ import type {
|
|
|
19
21
|
MongoMigrationCheck,
|
|
20
22
|
MongoMigrationPlanOperation,
|
|
21
23
|
} from '@prisma-next/mongo-query-ast/control';
|
|
24
|
+
import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
|
|
22
25
|
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
|
-
|
|
35
26
|
import { FilterEvaluator } from './filter-evaluator';
|
|
36
27
|
import { deserializeMongoOps } from './mongo-ops-serializer';
|
|
28
|
+
import { verifyMongoSchema } from './schema-verify/verify-mongo-schema';
|
|
29
|
+
|
|
30
|
+
const READ_ONLY_CHECK_COMMAND_KINDS: ReadonlySet<string> = new Set(['aggregate', 'rawAggregate']);
|
|
37
31
|
|
|
38
32
|
export interface MarkerOperations {
|
|
39
33
|
readMarker(): Promise<ContractMarkerRecord | null>;
|
|
40
34
|
initMarker(destination: {
|
|
41
35
|
readonly storageHash: string;
|
|
42
36
|
readonly profileHash: string;
|
|
37
|
+
readonly invariants?: readonly string[];
|
|
43
38
|
}): Promise<void>;
|
|
44
39
|
updateMarker(
|
|
45
40
|
expectedFrom: string,
|
|
46
|
-
destination: {
|
|
41
|
+
destination: {
|
|
42
|
+
readonly storageHash: string;
|
|
43
|
+
readonly profileHash: string;
|
|
44
|
+
readonly invariants?: readonly string[];
|
|
45
|
+
},
|
|
47
46
|
): Promise<boolean>;
|
|
48
47
|
writeLedgerEntry(entry: {
|
|
49
48
|
readonly edgeId: string;
|
|
@@ -58,6 +57,21 @@ export interface MongoRunnerDependencies {
|
|
|
58
57
|
readonly adapter: MongoAdapter;
|
|
59
58
|
readonly driver: MongoDriver;
|
|
60
59
|
readonly markerOps: MarkerOperations;
|
|
60
|
+
readonly introspectSchema: () => Promise<MongoSchemaIR>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MongoMigrationRunnerExecuteOptions {
|
|
64
|
+
readonly plan: MigrationPlan;
|
|
65
|
+
readonly destinationContract: MongoContract;
|
|
66
|
+
readonly policy: MigrationOperationPolicy;
|
|
67
|
+
readonly callbacks?: {
|
|
68
|
+
onOperationStart?(op: MigrationPlanOperation): void;
|
|
69
|
+
onOperationComplete?(op: MigrationPlanOperation): void;
|
|
70
|
+
};
|
|
71
|
+
readonly executionChecks?: MigrationRunnerExecutionChecks;
|
|
72
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
|
|
73
|
+
readonly strictVerification?: boolean;
|
|
74
|
+
readonly context?: OperationContext;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
function runnerFailure(
|
|
@@ -75,17 +89,7 @@ function runnerFailure(
|
|
|
75
89
|
export class MongoMigrationRunner {
|
|
76
90
|
constructor(private readonly deps: MongoRunnerDependencies) {}
|
|
77
91
|
|
|
78
|
-
async execute(options: {
|
|
79
|
-
readonly plan: MigrationPlan;
|
|
80
|
-
readonly destinationContract: unknown;
|
|
81
|
-
readonly policy: MigrationOperationPolicy;
|
|
82
|
-
readonly callbacks?: {
|
|
83
|
-
onOperationStart?(op: MigrationPlanOperation): void;
|
|
84
|
-
onOperationComplete?(op: MigrationPlanOperation): void;
|
|
85
|
-
};
|
|
86
|
-
readonly executionChecks?: MigrationRunnerExecutionChecks;
|
|
87
|
-
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
|
|
88
|
-
}): Promise<MigrationRunnerResult> {
|
|
92
|
+
async execute(options: MongoMigrationRunnerExecuteOptions): Promise<MigrationRunnerResult> {
|
|
89
93
|
const { commandExecutor, inspectionExecutor, adapter, driver, markerOps } = this.deps;
|
|
90
94
|
const operations = deserializeMongoOps(options.plan.operations as readonly unknown[]);
|
|
91
95
|
|
|
@@ -176,49 +180,84 @@ export class MongoMigrationRunner {
|
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
const destination = options.plan.destination;
|
|
179
|
-
const profileHash =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
183
|
+
const profileHash = options.destinationContract.profileHash ?? destination.storageHash;
|
|
184
|
+
|
|
185
|
+
const incomingInvariants = options.plan.providedInvariants ?? [];
|
|
186
|
+
const existingInvariantSet = new Set(existingMarker?.invariants ?? []);
|
|
187
|
+
const incomingIsSubsetOfExisting = incomingInvariants.every((id) =>
|
|
188
|
+
existingInvariantSet.has(id),
|
|
189
|
+
);
|
|
190
|
+
const markerAlreadyAtDestination =
|
|
191
|
+
existingMarker !== null &&
|
|
192
|
+
existingMarker.storageHash === destination.storageHash &&
|
|
193
|
+
existingMarker.profileHash === profileHash;
|
|
194
|
+
|
|
195
|
+
// Skip marker/ledger writes (and schema verification) only when the apply
|
|
196
|
+
// is a true no-op: no operations executed, marker already at destination,
|
|
197
|
+
// and every incoming invariant is already in the stored set.
|
|
198
|
+
//
|
|
199
|
+
// Divergence from the SQL runners (postgres/sqlite): those runners gate
|
|
200
|
+
// the no-op skip on `isSelfEdge` (origin === destination) only, so a
|
|
201
|
+
// non-self-edge `db update` that introspects-as-no-op still writes a
|
|
202
|
+
// ledger entry. Mongo skips even those because the runner has no
|
|
203
|
+
// structural distinction between self-edge and re-apply — invariant-
|
|
204
|
+
// aware routing here does not yet differentiate between the two
|
|
205
|
+
// ledger semantics. If the SQL audit-trail behavior should hold for
|
|
206
|
+
// Mongo too, gate this `isNoOp` on a self-edge check (or, conversely,
|
|
207
|
+
// align the SQL runners to skip non-self-edge no-ops uniformly).
|
|
208
|
+
const isNoOp =
|
|
209
|
+
operationsExecuted === 0 && markerAlreadyAtDestination && incomingIsSubsetOfExisting;
|
|
210
|
+
|
|
211
|
+
if (!isNoOp) {
|
|
212
|
+
const liveSchema = await this.deps.introspectSchema();
|
|
213
|
+
const verifyResult = verifyMongoSchema({
|
|
214
|
+
contract: options.destinationContract,
|
|
215
|
+
schema: liveSchema,
|
|
216
|
+
strict: options.strictVerification ?? true,
|
|
217
|
+
frameworkComponents: options.frameworkComponents,
|
|
218
|
+
...(options.context ? { context: options.context } : {}),
|
|
195
219
|
});
|
|
196
|
-
if (!
|
|
197
|
-
return runnerFailure(
|
|
198
|
-
'
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
220
|
+
if (!verifyResult.ok) {
|
|
221
|
+
return runnerFailure('SCHEMA_VERIFY_FAILED', verifyResult.summary, {
|
|
222
|
+
why: 'The resulting database schema does not satisfy the destination contract.',
|
|
223
|
+
meta: { issues: verifyResult.schema.issues },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (existingMarker) {
|
|
228
|
+
const updated = await markerOps.updateMarker(existingMarker.storageHash, {
|
|
229
|
+
storageHash: destination.storageHash,
|
|
230
|
+
profileHash,
|
|
231
|
+
invariants: incomingInvariants,
|
|
232
|
+
});
|
|
233
|
+
if (!updated) {
|
|
234
|
+
return runnerFailure(
|
|
235
|
+
'MARKER_CAS_FAILURE',
|
|
236
|
+
'Marker was modified by another process during migration execution.',
|
|
237
|
+
{
|
|
238
|
+
meta: {
|
|
239
|
+
expectedStorageHash: existingMarker.storageHash,
|
|
240
|
+
destinationStorageHash: destination.storageHash,
|
|
241
|
+
},
|
|
204
242
|
},
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
await markerOps.initMarker({
|
|
247
|
+
storageHash: destination.storageHash,
|
|
248
|
+
profileHash,
|
|
249
|
+
invariants: incomingInvariants,
|
|
250
|
+
});
|
|
207
251
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
252
|
+
|
|
253
|
+
const originHash = existingMarker?.storageHash ?? '';
|
|
254
|
+
await markerOps.writeLedgerEntry({
|
|
255
|
+
edgeId: `${originHash}->${destination.storageHash}`,
|
|
256
|
+
from: originHash,
|
|
257
|
+
to: destination.storageHash,
|
|
212
258
|
});
|
|
213
259
|
}
|
|
214
260
|
|
|
215
|
-
const originHash = existingMarker?.storageHash ?? '';
|
|
216
|
-
await markerOps.writeLedgerEntry({
|
|
217
|
-
edgeId: `${originHash}->${destination.storageHash}`,
|
|
218
|
-
from: originHash,
|
|
219
|
-
to: destination.storageHash,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
261
|
return ok({ operationsPlanned: operations.length, operationsExecuted });
|
|
223
262
|
}
|
|
224
263
|
|
|
@@ -259,7 +298,7 @@ export class MongoMigrationRunner {
|
|
|
259
298
|
}
|
|
260
299
|
|
|
261
300
|
for (const plan of op.run) {
|
|
262
|
-
const wireCommand = adapter.lower(plan);
|
|
301
|
+
const wireCommand = await adapter.lower(plan, {});
|
|
263
302
|
for await (const _ of driver.execute(wireCommand)) {
|
|
264
303
|
/* consume */
|
|
265
304
|
}
|
|
@@ -307,7 +346,7 @@ export class MongoMigrationRunner {
|
|
|
307
346
|
},
|
|
308
347
|
);
|
|
309
348
|
}
|
|
310
|
-
const wireCommand = adapter.lower(check.source);
|
|
349
|
+
const wireCommand = await adapter.lower(check.source, {});
|
|
311
350
|
let matchFound = false;
|
|
312
351
|
for await (const row of driver.execute<Record<string, unknown>>(wireCommand)) {
|
|
313
352
|
if (filterEvaluator.evaluate(check.filter, row)) {
|
|
@@ -375,13 +414,9 @@ export class MongoMigrationRunner {
|
|
|
375
414
|
): MigrationRunnerResult | undefined {
|
|
376
415
|
const origin = plan.origin ?? null;
|
|
377
416
|
if (!origin) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
'Database already has a contract marker but the plan has no origin. This would silently overwrite the existing marker.',
|
|
382
|
-
{ meta: { markerStorageHash: marker.storageHash } },
|
|
383
|
-
);
|
|
384
|
-
}
|
|
417
|
+
// No origin assertion on the plan — the caller has done its own
|
|
418
|
+
// correctness check (typically `db update` via live-schema
|
|
419
|
+
// introspection) and does not rely on marker continuity.
|
|
385
420
|
return undefined;
|
|
386
421
|
}
|
|
387
422
|
|
|
@@ -3,16 +3,31 @@ import { type ImportRequirement, jsonToTsSource, renderImports } from '@prisma-n
|
|
|
3
3
|
import type { OpFactoryCall } from './op-factory-call';
|
|
4
4
|
|
|
5
5
|
export interface RenderMigrationMeta {
|
|
6
|
-
readonly from: string;
|
|
6
|
+
readonly from: string | null;
|
|
7
7
|
readonly to: string;
|
|
8
|
-
readonly kind?: string;
|
|
9
8
|
readonly labels?: readonly string[];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Always-present base imports for the rendered scaffold:
|
|
13
|
+
*
|
|
14
|
+
* - `Migration` from `@prisma-next/family-mongo/migration` — the
|
|
15
|
+
* user-facing Mongo `Migration` base; subclasses don't need to
|
|
16
|
+
* redeclare `targetId` or thread family/target generics.
|
|
17
|
+
* - `MigrationCLI` from `@prisma-next/cli/migration-cli` — the
|
|
18
|
+
* migration-file CLI entrypoint that loads `prisma-next.config.ts`,
|
|
19
|
+
* assembles a `ControlStack`, and instantiates the migration class.
|
|
20
|
+
* The migration file owns this dependency directly: pulling CLI
|
|
21
|
+
* machinery in at script run time is acceptable because the script's
|
|
22
|
+
* whole purpose is to be invoked from the project that owns the
|
|
23
|
+
* config. (Mirrors the postgres facade pattern; pulling `MigrationCLI`
|
|
24
|
+
* into `@prisma-next/family-mongo/migration` so a Mongo migration only
|
|
25
|
+
* needs one import is tracked separately as a follow-up.)
|
|
26
|
+
*/
|
|
27
|
+
const BASE_IMPORTS: readonly ImportRequirement[] = [
|
|
28
|
+
{ moduleSpecifier: '@prisma-next/family-mongo/migration', symbol: 'Migration' },
|
|
29
|
+
{ moduleSpecifier: '@prisma-next/cli/migration-cli', symbol: 'MigrationCLI' },
|
|
30
|
+
];
|
|
16
31
|
|
|
17
32
|
/**
|
|
18
33
|
* Render a list of Mongo `OpFactoryCall`s as a `migration.ts`
|
|
@@ -20,16 +35,16 @@ const BASE_IMPORT: ImportRequirement = {
|
|
|
20
35
|
* `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and
|
|
21
36
|
* implements the abstract `operations` and `describe` members. `meta` is
|
|
22
37
|
* always rendered — `describe()` is part of the `Migration` contract, so
|
|
23
|
-
* even an empty stub must satisfy it; callers pass
|
|
24
|
-
* migration-new scaffold.
|
|
38
|
+
* even an empty stub must satisfy it; callers pass `from: null` for a
|
|
39
|
+
* baseline `migration-new` scaffold (and a real `to` hash either way).
|
|
25
40
|
*
|
|
26
41
|
* The walk is polymorphic: each call node contributes its own
|
|
27
42
|
* `renderTypeScript()` expression and declares its own
|
|
28
43
|
* `importRequirements()`. The top-level renderer aggregates imports
|
|
29
44
|
* across all nodes and emits one `import { … } from "…"` line per module.
|
|
30
|
-
* The `Migration`
|
|
31
|
-
*
|
|
32
|
-
*
|
|
45
|
+
* The `Migration` and `MigrationCLI` imports are always emitted — they're
|
|
46
|
+
* structural to the rendered scaffold (extends `Migration`, calls
|
|
47
|
+
* `MigrationCLI.run`), not driven by any node.
|
|
33
48
|
*/
|
|
34
49
|
export function renderCallsToTypeScript(
|
|
35
50
|
calls: ReadonlyArray<OpFactoryCall>,
|
|
@@ -52,13 +67,13 @@ export function renderCallsToTypeScript(
|
|
|
52
67
|
'}',
|
|
53
68
|
'',
|
|
54
69
|
'export default M;',
|
|
55
|
-
'
|
|
70
|
+
'MigrationCLI.run(import.meta.url, M);',
|
|
56
71
|
'',
|
|
57
72
|
].join('\n');
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
function buildImports(calls: ReadonlyArray<OpFactoryCall>): string {
|
|
61
|
-
const requirements: ImportRequirement[] = [
|
|
76
|
+
const requirements: ImportRequirement[] = [...BASE_IMPORTS];
|
|
62
77
|
for (const call of calls) {
|
|
63
78
|
for (const req of call.importRequirements()) {
|
|
64
79
|
requirements.push(req);
|
|
@@ -73,9 +88,6 @@ function buildDescribeMethod(meta: RenderMigrationMeta): string {
|
|
|
73
88
|
lines.push(' return {');
|
|
74
89
|
lines.push(` from: ${JSON.stringify(meta.from)},`);
|
|
75
90
|
lines.push(` to: ${JSON.stringify(meta.to)},`);
|
|
76
|
-
if (meta.kind) {
|
|
77
|
-
lines.push(` kind: ${JSON.stringify(meta.kind)},`);
|
|
78
|
-
}
|
|
79
91
|
if (meta.labels && meta.labels.length > 0) {
|
|
80
92
|
lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
|
|
81
93
|
}
|