@prisma-next/adapter-mongo 0.4.0-dev.1 → 0.4.0-dev.2

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,277 +0,0 @@
1
- import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
2
- import type {
3
- ControlDriverInstance,
4
- MigrationOperationPolicy,
5
- MigrationPlan,
6
- MigrationPlanOperation,
7
- MigrationRunner,
8
- MigrationRunnerExecutionChecks,
9
- MigrationRunnerFailure,
10
- MigrationRunnerResult,
11
- } from '@prisma-next/framework-components/control';
12
- import type {
13
- MongoMigrationCheck,
14
- MongoMigrationPlanOperation,
15
- } from '@prisma-next/mongo-query-ast/control';
16
- import {
17
- initMarker,
18
- readMarker,
19
- updateMarker,
20
- writeLedgerEntry,
21
- } from '@prisma-next/target-mongo/control';
22
- import { notOk, ok } from '@prisma-next/utils/result';
23
- import type { Db } from 'mongodb';
24
- import { MongoCommandExecutor, MongoInspectionExecutor } from './command-executor';
25
- import { FilterEvaluator } from './filter-evaluator';
26
- import type { MongoControlDriverInstance } from './mongo-control-driver';
27
- import { deserializeMongoOps } from './mongo-ops-serializer';
28
-
29
- function runnerFailure(
30
- code: string,
31
- summary: string,
32
- opts?: { why?: string; meta?: Record<string, unknown> },
33
- ): MigrationRunnerResult {
34
- return notOk<MigrationRunnerFailure>({
35
- code,
36
- summary,
37
- ...opts,
38
- });
39
- }
40
-
41
- function isMongoControlDriverInstance(
42
- driver: ControlDriverInstance<'mongo', 'mongo'>,
43
- ): driver is MongoControlDriverInstance {
44
- return 'db' in driver && driver.db != null;
45
- }
46
-
47
- function extractDb(driver: ControlDriverInstance<'mongo', 'mongo'>): Db {
48
- if (!isMongoControlDriverInstance(driver)) {
49
- throw new Error(
50
- 'Mongo control driver does not expose a db property. ' +
51
- 'Use mongoControlDriver.create() from `@prisma-next/driver-mongo/control`.',
52
- );
53
- }
54
- return driver.db;
55
- }
56
-
57
- export class MongoMigrationRunner implements MigrationRunner<'mongo', 'mongo'> {
58
- async execute(options: {
59
- readonly plan: MigrationPlan;
60
- readonly driver: ControlDriverInstance<'mongo', 'mongo'>;
61
- readonly destinationContract: unknown;
62
- readonly policy: MigrationOperationPolicy;
63
- readonly callbacks?: {
64
- onOperationStart?(op: MigrationPlanOperation): void;
65
- onOperationComplete?(op: MigrationPlanOperation): void;
66
- };
67
- readonly executionChecks?: MigrationRunnerExecutionChecks;
68
- readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
69
- }): Promise<MigrationRunnerResult> {
70
- const db = extractDb(options.driver);
71
- const operations = deserializeMongoOps(options.plan.operations as readonly unknown[]);
72
-
73
- const policyCheck = this.enforcePolicyCompatibility(options.policy, operations);
74
- if (policyCheck) return policyCheck;
75
-
76
- const existingMarker = await readMarker(db);
77
-
78
- const markerCheck = this.ensureMarkerCompatibility(existingMarker, options.plan);
79
- if (markerCheck) return markerCheck;
80
-
81
- const checks = options.executionChecks;
82
- const runPrechecks = checks?.prechecks !== false;
83
- const runPostchecks = checks?.postchecks !== false;
84
- const runIdempotency = checks?.idempotencyChecks !== false;
85
-
86
- const commandExecutor = new MongoCommandExecutor(db);
87
- const inspectionExecutor = new MongoInspectionExecutor(db);
88
- const filterEvaluator = new FilterEvaluator();
89
-
90
- let operationsExecuted = 0;
91
-
92
- for (const operation of operations) {
93
- options.callbacks?.onOperationStart?.(operation);
94
- try {
95
- if (runPostchecks && runIdempotency) {
96
- const allSatisfied = await this.allChecksSatisfied(
97
- operation.postcheck,
98
- inspectionExecutor,
99
- filterEvaluator,
100
- );
101
- if (allSatisfied) continue;
102
- }
103
-
104
- if (runPrechecks) {
105
- const precheckResult = await this.evaluateChecks(
106
- operation.precheck,
107
- inspectionExecutor,
108
- filterEvaluator,
109
- );
110
- if (!precheckResult) {
111
- return runnerFailure(
112
- 'PRECHECK_FAILED',
113
- `Operation ${operation.id} failed during precheck`,
114
- { meta: { operationId: operation.id } },
115
- );
116
- }
117
- }
118
-
119
- for (const step of operation.execute) {
120
- await step.command.accept(commandExecutor);
121
- }
122
-
123
- if (runPostchecks) {
124
- const postcheckResult = await this.evaluateChecks(
125
- operation.postcheck,
126
- inspectionExecutor,
127
- filterEvaluator,
128
- );
129
- if (!postcheckResult) {
130
- return runnerFailure(
131
- 'POSTCHECK_FAILED',
132
- `Operation ${operation.id} failed during postcheck`,
133
- { meta: { operationId: operation.id } },
134
- );
135
- }
136
- }
137
-
138
- operationsExecuted += 1;
139
- } finally {
140
- options.callbacks?.onOperationComplete?.(operation);
141
- }
142
- }
143
-
144
- const destination = options.plan.destination;
145
- const contract = options.destinationContract as { profileHash?: string };
146
- const profileHash = contract.profileHash ?? destination.storageHash;
147
-
148
- if (
149
- operationsExecuted === 0 &&
150
- existingMarker?.storageHash === destination.storageHash &&
151
- existingMarker.profileHash === profileHash
152
- ) {
153
- return ok({ operationsPlanned: operations.length, operationsExecuted });
154
- }
155
-
156
- if (existingMarker) {
157
- const updated = await updateMarker(db, existingMarker.storageHash, {
158
- storageHash: destination.storageHash,
159
- profileHash,
160
- });
161
- if (!updated) {
162
- return runnerFailure(
163
- 'MARKER_CAS_FAILURE',
164
- 'Marker was modified by another process during migration execution.',
165
- {
166
- meta: {
167
- expectedStorageHash: existingMarker.storageHash,
168
- destinationStorageHash: destination.storageHash,
169
- },
170
- },
171
- );
172
- }
173
- } else {
174
- await initMarker(db, {
175
- storageHash: destination.storageHash,
176
- profileHash,
177
- });
178
- }
179
-
180
- const originHash = existingMarker?.storageHash ?? '';
181
- await writeLedgerEntry(db, {
182
- edgeId: `${originHash}->${destination.storageHash}`,
183
- from: originHash,
184
- to: destination.storageHash,
185
- });
186
-
187
- return ok({ operationsPlanned: operations.length, operationsExecuted });
188
- }
189
-
190
- private async evaluateChecks(
191
- checks: readonly MongoMigrationCheck[],
192
- inspectionExecutor: MongoInspectionExecutor,
193
- filterEvaluator: FilterEvaluator,
194
- ): Promise<boolean> {
195
- for (const check of checks) {
196
- const documents = await check.source.accept(inspectionExecutor);
197
- const matchFound = documents.some((doc) =>
198
- filterEvaluator.evaluate(check.filter, doc as Record<string, unknown>),
199
- );
200
- const passed = check.expect === 'exists' ? matchFound : !matchFound;
201
- if (!passed) return false;
202
- }
203
- return true;
204
- }
205
-
206
- private async allChecksSatisfied(
207
- checks: readonly MongoMigrationCheck[],
208
- inspectionExecutor: MongoInspectionExecutor,
209
- filterEvaluator: FilterEvaluator,
210
- ): Promise<boolean> {
211
- if (checks.length === 0) return false;
212
- return this.evaluateChecks(checks, inspectionExecutor, filterEvaluator);
213
- }
214
-
215
- private enforcePolicyCompatibility(
216
- policy: MigrationOperationPolicy,
217
- operations: readonly MongoMigrationPlanOperation[],
218
- ): MigrationRunnerResult | undefined {
219
- const allowedClasses = new Set(policy.allowedOperationClasses);
220
- for (const operation of operations) {
221
- if (!allowedClasses.has(operation.operationClass)) {
222
- return runnerFailure(
223
- 'POLICY_VIOLATION',
224
- `Operation ${operation.id} has class "${operation.operationClass}" which is not allowed by policy.`,
225
- {
226
- why: `Policy only allows: ${[...allowedClasses].join(', ')}.`,
227
- meta: {
228
- operationId: operation.id,
229
- operationClass: operation.operationClass,
230
- },
231
- },
232
- );
233
- }
234
- }
235
- return undefined;
236
- }
237
-
238
- private ensureMarkerCompatibility(
239
- marker: Awaited<ReturnType<typeof readMarker>>,
240
- plan: MigrationPlan,
241
- ): MigrationRunnerResult | undefined {
242
- const origin = plan.origin ?? null;
243
- if (!origin) {
244
- if (marker) {
245
- return runnerFailure(
246
- 'MARKER_ORIGIN_MISMATCH',
247
- 'Database already has a contract marker but the plan has no origin. This would silently overwrite the existing marker.',
248
- { meta: { markerStorageHash: marker.storageHash } },
249
- );
250
- }
251
- return undefined;
252
- }
253
-
254
- if (!marker) {
255
- return runnerFailure(
256
- 'MARKER_ORIGIN_MISMATCH',
257
- `Missing contract marker: expected origin storage hash ${origin.storageHash}.`,
258
- { meta: { expectedOriginStorageHash: origin.storageHash } },
259
- );
260
- }
261
-
262
- if (marker.storageHash !== origin.storageHash) {
263
- return runnerFailure(
264
- 'MARKER_ORIGIN_MISMATCH',
265
- `Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`,
266
- {
267
- meta: {
268
- markerStorageHash: marker.storageHash,
269
- expectedOriginStorageHash: origin.storageHash,
270
- },
271
- },
272
- );
273
- }
274
-
275
- return undefined;
276
- }
277
- }