@prisma-next/target-mongo 0.3.0 → 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.
- package/README.md +37 -0
- 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 +20 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +3 -0
- package/dist/op-factory-call-CfPGebEH.d.mts +76 -0
- package/dist/op-factory-call-CfPGebEH.d.mts.map +1 -0
- package/package.json +13 -6
- 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 +244 -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 +9 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
|
+
import type {
|
|
4
|
+
MigrationOperationPolicy,
|
|
5
|
+
MigrationPlan,
|
|
6
|
+
MigrationPlanOperation,
|
|
7
|
+
MigrationRunnerExecutionChecks,
|
|
8
|
+
MigrationRunnerFailure,
|
|
9
|
+
MigrationRunnerResult,
|
|
10
|
+
} from '@prisma-next/framework-components/control';
|
|
11
|
+
import type {
|
|
12
|
+
MongoDdlCommandVisitor,
|
|
13
|
+
MongoInspectionCommandVisitor,
|
|
14
|
+
MongoMigrationCheck,
|
|
15
|
+
MongoMigrationPlanOperation,
|
|
16
|
+
} from '@prisma-next/mongo-query-ast/control';
|
|
17
|
+
import { notOk, ok } from '@prisma-next/utils/result';
|
|
18
|
+
import { FilterEvaluator } from './filter-evaluator';
|
|
19
|
+
import { deserializeMongoOps } from './mongo-ops-serializer';
|
|
20
|
+
|
|
21
|
+
export interface MarkerOperations {
|
|
22
|
+
readMarker(): Promise<ContractMarkerRecord | null>;
|
|
23
|
+
initMarker(destination: {
|
|
24
|
+
readonly storageHash: string;
|
|
25
|
+
readonly profileHash: string;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
updateMarker(
|
|
28
|
+
expectedFrom: string,
|
|
29
|
+
destination: { readonly storageHash: string; readonly profileHash: string },
|
|
30
|
+
): Promise<boolean>;
|
|
31
|
+
writeLedgerEntry(entry: {
|
|
32
|
+
readonly edgeId: string;
|
|
33
|
+
readonly from: string;
|
|
34
|
+
readonly to: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MongoRunnerDependencies {
|
|
39
|
+
readonly commandExecutor: MongoDdlCommandVisitor<Promise<void>>;
|
|
40
|
+
readonly inspectionExecutor: MongoInspectionCommandVisitor<Promise<Record<string, unknown>[]>>;
|
|
41
|
+
readonly markerOps: MarkerOperations;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runnerFailure(
|
|
45
|
+
code: string,
|
|
46
|
+
summary: string,
|
|
47
|
+
opts?: { why?: string; meta?: Record<string, unknown> },
|
|
48
|
+
): MigrationRunnerResult {
|
|
49
|
+
return notOk<MigrationRunnerFailure>({
|
|
50
|
+
code,
|
|
51
|
+
summary,
|
|
52
|
+
...opts,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class MongoMigrationRunner {
|
|
57
|
+
constructor(private readonly deps: MongoRunnerDependencies) {}
|
|
58
|
+
|
|
59
|
+
async execute(options: {
|
|
60
|
+
readonly plan: MigrationPlan;
|
|
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 { commandExecutor, inspectionExecutor, markerOps } = this.deps;
|
|
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 markerOps.readMarker();
|
|
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 filterEvaluator = new FilterEvaluator();
|
|
87
|
+
|
|
88
|
+
let operationsExecuted = 0;
|
|
89
|
+
|
|
90
|
+
for (const operation of operations) {
|
|
91
|
+
options.callbacks?.onOperationStart?.(operation);
|
|
92
|
+
try {
|
|
93
|
+
if (runPostchecks && runIdempotency) {
|
|
94
|
+
const allSatisfied = await this.allChecksSatisfied(
|
|
95
|
+
operation.postcheck,
|
|
96
|
+
inspectionExecutor,
|
|
97
|
+
filterEvaluator,
|
|
98
|
+
);
|
|
99
|
+
if (allSatisfied) continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (runPrechecks) {
|
|
103
|
+
const precheckResult = await this.evaluateChecks(
|
|
104
|
+
operation.precheck,
|
|
105
|
+
inspectionExecutor,
|
|
106
|
+
filterEvaluator,
|
|
107
|
+
);
|
|
108
|
+
if (!precheckResult) {
|
|
109
|
+
return runnerFailure(
|
|
110
|
+
'PRECHECK_FAILED',
|
|
111
|
+
`Operation ${operation.id} failed during precheck`,
|
|
112
|
+
{ meta: { operationId: operation.id } },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const step of operation.execute) {
|
|
118
|
+
await step.command.accept(commandExecutor);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (runPostchecks) {
|
|
122
|
+
const postcheckResult = await this.evaluateChecks(
|
|
123
|
+
operation.postcheck,
|
|
124
|
+
inspectionExecutor,
|
|
125
|
+
filterEvaluator,
|
|
126
|
+
);
|
|
127
|
+
if (!postcheckResult) {
|
|
128
|
+
return runnerFailure(
|
|
129
|
+
'POSTCHECK_FAILED',
|
|
130
|
+
`Operation ${operation.id} failed during postcheck`,
|
|
131
|
+
{ meta: { operationId: operation.id } },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
operationsExecuted += 1;
|
|
137
|
+
} finally {
|
|
138
|
+
options.callbacks?.onOperationComplete?.(operation);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const destination = options.plan.destination;
|
|
143
|
+
const contract = options.destinationContract as { profileHash?: string };
|
|
144
|
+
const profileHash = contract.profileHash ?? destination.storageHash;
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
operationsExecuted === 0 &&
|
|
148
|
+
existingMarker?.storageHash === destination.storageHash &&
|
|
149
|
+
existingMarker.profileHash === profileHash
|
|
150
|
+
) {
|
|
151
|
+
return ok({ operationsPlanned: operations.length, operationsExecuted });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (existingMarker) {
|
|
155
|
+
const updated = await markerOps.updateMarker(existingMarker.storageHash, {
|
|
156
|
+
storageHash: destination.storageHash,
|
|
157
|
+
profileHash,
|
|
158
|
+
});
|
|
159
|
+
if (!updated) {
|
|
160
|
+
return runnerFailure(
|
|
161
|
+
'MARKER_CAS_FAILURE',
|
|
162
|
+
'Marker was modified by another process during migration execution.',
|
|
163
|
+
{
|
|
164
|
+
meta: {
|
|
165
|
+
expectedStorageHash: existingMarker.storageHash,
|
|
166
|
+
destinationStorageHash: destination.storageHash,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
await markerOps.initMarker({
|
|
173
|
+
storageHash: destination.storageHash,
|
|
174
|
+
profileHash,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const originHash = existingMarker?.storageHash ?? '';
|
|
179
|
+
await markerOps.writeLedgerEntry({
|
|
180
|
+
edgeId: `${originHash}->${destination.storageHash}`,
|
|
181
|
+
from: originHash,
|
|
182
|
+
to: destination.storageHash,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return ok({ operationsPlanned: operations.length, operationsExecuted });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async evaluateChecks(
|
|
189
|
+
checks: readonly MongoMigrationCheck[],
|
|
190
|
+
inspectionExecutor: MongoInspectionCommandVisitor<Promise<Record<string, unknown>[]>>,
|
|
191
|
+
filterEvaluator: FilterEvaluator,
|
|
192
|
+
): Promise<boolean> {
|
|
193
|
+
for (const check of checks) {
|
|
194
|
+
const documents = await check.source.accept(inspectionExecutor);
|
|
195
|
+
const matchFound = documents.some((doc) =>
|
|
196
|
+
filterEvaluator.evaluate(check.filter, doc as Record<string, unknown>),
|
|
197
|
+
);
|
|
198
|
+
const passed = check.expect === 'exists' ? matchFound : !matchFound;
|
|
199
|
+
if (!passed) return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async allChecksSatisfied(
|
|
205
|
+
checks: readonly MongoMigrationCheck[],
|
|
206
|
+
inspectionExecutor: MongoInspectionCommandVisitor<Promise<Record<string, unknown>[]>>,
|
|
207
|
+
filterEvaluator: FilterEvaluator,
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
if (checks.length === 0) return false;
|
|
210
|
+
return this.evaluateChecks(checks, inspectionExecutor, filterEvaluator);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private enforcePolicyCompatibility(
|
|
214
|
+
policy: MigrationOperationPolicy,
|
|
215
|
+
operations: readonly MongoMigrationPlanOperation[],
|
|
216
|
+
): MigrationRunnerResult | undefined {
|
|
217
|
+
const allowedClasses = new Set(policy.allowedOperationClasses);
|
|
218
|
+
for (const operation of operations) {
|
|
219
|
+
if (!allowedClasses.has(operation.operationClass)) {
|
|
220
|
+
return runnerFailure(
|
|
221
|
+
'POLICY_VIOLATION',
|
|
222
|
+
`Operation ${operation.id} has class "${operation.operationClass}" which is not allowed by policy.`,
|
|
223
|
+
{
|
|
224
|
+
why: `Policy only allows: ${[...allowedClasses].join(', ')}.`,
|
|
225
|
+
meta: {
|
|
226
|
+
operationId: operation.id,
|
|
227
|
+
operationClass: operation.operationClass,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private ensureMarkerCompatibility(
|
|
237
|
+
marker: ContractMarkerRecord | null,
|
|
238
|
+
plan: MigrationPlan,
|
|
239
|
+
): MigrationRunnerResult | undefined {
|
|
240
|
+
const origin = plan.origin ?? null;
|
|
241
|
+
if (!origin) {
|
|
242
|
+
if (marker) {
|
|
243
|
+
return runnerFailure(
|
|
244
|
+
'MARKER_ORIGIN_MISMATCH',
|
|
245
|
+
'Database already has a contract marker but the plan has no origin. This would silently overwrite the existing marker.',
|
|
246
|
+
{ meta: { markerStorageHash: marker.storageHash } },
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!marker) {
|
|
253
|
+
return runnerFailure(
|
|
254
|
+
'MARKER_ORIGIN_MISMATCH',
|
|
255
|
+
`Missing contract marker: expected origin storage hash ${origin.storageHash}.`,
|
|
256
|
+
{ meta: { expectedOriginStorageHash: origin.storageHash } },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (marker.storageHash !== origin.storageHash) {
|
|
261
|
+
return runnerFailure(
|
|
262
|
+
'MARKER_ORIGIN_MISMATCH',
|
|
263
|
+
`Existing contract marker (${marker.storageHash}) does not match plan origin (${origin.storageHash}).`,
|
|
264
|
+
{
|
|
265
|
+
meta: {
|
|
266
|
+
markerStorageHash: marker.storageHash,
|
|
267
|
+
expectedOriginStorageHash: origin.storageHash,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { MigrationOperationClass } from '@prisma-next/framework-components/control';
|
|
2
|
+
import type {
|
|
3
|
+
CollModOptions,
|
|
4
|
+
CreateCollectionOptions,
|
|
5
|
+
CreateIndexOptions,
|
|
6
|
+
MongoIndexKey,
|
|
7
|
+
} from '@prisma-next/mongo-query-ast/control';
|
|
8
|
+
import type {
|
|
9
|
+
MongoSchemaCollection,
|
|
10
|
+
MongoSchemaCollectionOptions,
|
|
11
|
+
MongoSchemaIndex,
|
|
12
|
+
MongoSchemaValidator,
|
|
13
|
+
} from '@prisma-next/mongo-schema-ir';
|
|
14
|
+
|
|
15
|
+
export interface CollModMeta {
|
|
16
|
+
readonly id?: string;
|
|
17
|
+
readonly label?: string;
|
|
18
|
+
readonly operationClass?: MigrationOperationClass;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OpFactoryCallVisitor<R> {
|
|
22
|
+
createIndex(call: CreateIndexCall): R;
|
|
23
|
+
dropIndex(call: DropIndexCall): R;
|
|
24
|
+
createCollection(call: CreateCollectionCall): R;
|
|
25
|
+
dropCollection(call: DropCollectionCall): R;
|
|
26
|
+
collMod(call: CollModCall): R;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
abstract class OpFactoryCallNode {
|
|
30
|
+
abstract readonly factory: string;
|
|
31
|
+
abstract readonly operationClass: MigrationOperationClass;
|
|
32
|
+
abstract readonly label: string;
|
|
33
|
+
abstract accept<R>(visitor: OpFactoryCallVisitor<R>): R;
|
|
34
|
+
|
|
35
|
+
protected freeze(): void {
|
|
36
|
+
Object.freeze(this);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatKeys(keys: ReadonlyArray<MongoIndexKey>): string {
|
|
41
|
+
return keys.map((k) => `${k.field}:${k.direction}`).join(', ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class CreateIndexCall extends OpFactoryCallNode {
|
|
45
|
+
readonly factory = 'createIndex' as const;
|
|
46
|
+
readonly operationClass = 'additive' as const;
|
|
47
|
+
readonly collection: string;
|
|
48
|
+
readonly keys: ReadonlyArray<MongoIndexKey>;
|
|
49
|
+
readonly options: CreateIndexOptions | undefined;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
collection: string,
|
|
54
|
+
keys: ReadonlyArray<MongoIndexKey>,
|
|
55
|
+
options?: CreateIndexOptions,
|
|
56
|
+
) {
|
|
57
|
+
super();
|
|
58
|
+
this.collection = collection;
|
|
59
|
+
this.keys = keys;
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.label = `Create index on ${collection} (${formatKeys(keys)})`;
|
|
62
|
+
this.freeze();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
accept<R>(visitor: OpFactoryCallVisitor<R>): R {
|
|
66
|
+
return visitor.createIndex(this);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class DropIndexCall extends OpFactoryCallNode {
|
|
71
|
+
readonly factory = 'dropIndex' as const;
|
|
72
|
+
readonly operationClass = 'destructive' as const;
|
|
73
|
+
readonly collection: string;
|
|
74
|
+
readonly keys: ReadonlyArray<MongoIndexKey>;
|
|
75
|
+
readonly label: string;
|
|
76
|
+
|
|
77
|
+
constructor(collection: string, keys: ReadonlyArray<MongoIndexKey>) {
|
|
78
|
+
super();
|
|
79
|
+
this.collection = collection;
|
|
80
|
+
this.keys = keys;
|
|
81
|
+
this.label = `Drop index on ${collection} (${formatKeys(keys)})`;
|
|
82
|
+
this.freeze();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
accept<R>(visitor: OpFactoryCallVisitor<R>): R {
|
|
86
|
+
return visitor.dropIndex(this);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class CreateCollectionCall extends OpFactoryCallNode {
|
|
91
|
+
readonly factory = 'createCollection' as const;
|
|
92
|
+
readonly operationClass = 'additive' as const;
|
|
93
|
+
readonly collection: string;
|
|
94
|
+
readonly options: CreateCollectionOptions | undefined;
|
|
95
|
+
readonly label: string;
|
|
96
|
+
|
|
97
|
+
constructor(collection: string, options?: CreateCollectionOptions) {
|
|
98
|
+
super();
|
|
99
|
+
this.collection = collection;
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.label = `Create collection ${collection}`;
|
|
102
|
+
this.freeze();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
accept<R>(visitor: OpFactoryCallVisitor<R>): R {
|
|
106
|
+
return visitor.createCollection(this);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class DropCollectionCall extends OpFactoryCallNode {
|
|
111
|
+
readonly factory = 'dropCollection' as const;
|
|
112
|
+
readonly operationClass = 'destructive' as const;
|
|
113
|
+
readonly collection: string;
|
|
114
|
+
readonly label: string;
|
|
115
|
+
|
|
116
|
+
constructor(collection: string) {
|
|
117
|
+
super();
|
|
118
|
+
this.collection = collection;
|
|
119
|
+
this.label = `Drop collection ${collection}`;
|
|
120
|
+
this.freeze();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
accept<R>(visitor: OpFactoryCallVisitor<R>): R {
|
|
124
|
+
return visitor.dropCollection(this);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class CollModCall extends OpFactoryCallNode {
|
|
129
|
+
readonly factory = 'collMod' as const;
|
|
130
|
+
readonly collection: string;
|
|
131
|
+
readonly options: CollModOptions;
|
|
132
|
+
readonly meta: CollModMeta | undefined;
|
|
133
|
+
readonly operationClass: MigrationOperationClass;
|
|
134
|
+
readonly label: string;
|
|
135
|
+
|
|
136
|
+
constructor(collection: string, options: CollModOptions, meta?: CollModMeta) {
|
|
137
|
+
super();
|
|
138
|
+
this.collection = collection;
|
|
139
|
+
this.options = options;
|
|
140
|
+
this.meta = meta;
|
|
141
|
+
this.operationClass = meta?.operationClass ?? 'destructive';
|
|
142
|
+
this.label = meta?.label ?? `Modify collection ${collection}`;
|
|
143
|
+
this.freeze();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
accept<R>(visitor: OpFactoryCallVisitor<R>): R {
|
|
147
|
+
return visitor.collMod(this);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type OpFactoryCall =
|
|
152
|
+
| CreateIndexCall
|
|
153
|
+
| DropIndexCall
|
|
154
|
+
| CreateCollectionCall
|
|
155
|
+
| DropCollectionCall
|
|
156
|
+
| CollModCall;
|
|
157
|
+
|
|
158
|
+
export function schemaIndexToCreateIndexOptions(index: MongoSchemaIndex): CreateIndexOptions {
|
|
159
|
+
return {
|
|
160
|
+
unique: index.unique || undefined,
|
|
161
|
+
sparse: index.sparse,
|
|
162
|
+
expireAfterSeconds: index.expireAfterSeconds,
|
|
163
|
+
partialFilterExpression: index.partialFilterExpression,
|
|
164
|
+
wildcardProjection: index.wildcardProjection,
|
|
165
|
+
collation: index.collation,
|
|
166
|
+
weights: index.weights,
|
|
167
|
+
default_language: index.default_language,
|
|
168
|
+
language_override: index.language_override,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function schemaCollectionToCreateCollectionOptions(
|
|
173
|
+
coll: MongoSchemaCollection,
|
|
174
|
+
): CreateCollectionOptions | undefined {
|
|
175
|
+
const opts: MongoSchemaCollectionOptions | undefined = coll.options;
|
|
176
|
+
const validator: MongoSchemaValidator | undefined = coll.validator;
|
|
177
|
+
if (!opts && !validator) return undefined;
|
|
178
|
+
return {
|
|
179
|
+
capped: opts?.capped ? true : undefined,
|
|
180
|
+
size: opts?.capped?.size,
|
|
181
|
+
max: opts?.capped?.max,
|
|
182
|
+
timeseries: opts?.timeseries,
|
|
183
|
+
collation: opts?.collation,
|
|
184
|
+
clusteredIndex: opts?.clusteredIndex
|
|
185
|
+
? {
|
|
186
|
+
key: { _id: 1 } as Record<string, number>,
|
|
187
|
+
unique: true as boolean,
|
|
188
|
+
...(opts.clusteredIndex.name != null ? { name: opts.clusteredIndex.name } : {}),
|
|
189
|
+
}
|
|
190
|
+
: undefined,
|
|
191
|
+
validator: validator ? { $jsonSchema: validator.jsonSchema } : undefined,
|
|
192
|
+
validationLevel: validator?.validationLevel,
|
|
193
|
+
validationAction: validator?.validationAction,
|
|
194
|
+
changeStreamPreAndPostImages: opts?.changeStreamPreAndPostImages,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MongoMigrationPlanOperation } from '@prisma-next/mongo-query-ast/control';
|
|
2
|
+
import {
|
|
3
|
+
collMod,
|
|
4
|
+
createCollection,
|
|
5
|
+
createIndex,
|
|
6
|
+
dropCollection,
|
|
7
|
+
dropIndex,
|
|
8
|
+
} from './migration-factories';
|
|
9
|
+
import type {
|
|
10
|
+
CollModCall,
|
|
11
|
+
CreateCollectionCall,
|
|
12
|
+
CreateIndexCall,
|
|
13
|
+
DropCollectionCall,
|
|
14
|
+
DropIndexCall,
|
|
15
|
+
OpFactoryCall,
|
|
16
|
+
OpFactoryCallVisitor,
|
|
17
|
+
} from './op-factory-call';
|
|
18
|
+
|
|
19
|
+
const renderVisitor: OpFactoryCallVisitor<MongoMigrationPlanOperation> = {
|
|
20
|
+
createIndex(call: CreateIndexCall) {
|
|
21
|
+
return createIndex(call.collection, call.keys, call.options);
|
|
22
|
+
},
|
|
23
|
+
dropIndex(call: DropIndexCall) {
|
|
24
|
+
return dropIndex(call.collection, call.keys);
|
|
25
|
+
},
|
|
26
|
+
createCollection(call: CreateCollectionCall) {
|
|
27
|
+
return createCollection(call.collection, call.options);
|
|
28
|
+
},
|
|
29
|
+
dropCollection(call: DropCollectionCall) {
|
|
30
|
+
return dropCollection(call.collection);
|
|
31
|
+
},
|
|
32
|
+
collMod(call: CollModCall) {
|
|
33
|
+
return collMod(call.collection, call.options, call.meta);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function renderOps(calls: ReadonlyArray<OpFactoryCall>): MongoMigrationPlanOperation[] {
|
|
38
|
+
return calls.map((call) => call.accept(renderVisitor));
|
|
39
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollModCall,
|
|
3
|
+
CreateCollectionCall,
|
|
4
|
+
CreateIndexCall,
|
|
5
|
+
DropCollectionCall,
|
|
6
|
+
DropIndexCall,
|
|
7
|
+
OpFactoryCall,
|
|
8
|
+
OpFactoryCallVisitor,
|
|
9
|
+
} from './op-factory-call';
|
|
10
|
+
|
|
11
|
+
export interface RenderMigrationMeta {
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
readonly kind?: string;
|
|
15
|
+
readonly labels?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function renderTypeScript(
|
|
19
|
+
calls: ReadonlyArray<OpFactoryCall>,
|
|
20
|
+
meta?: RenderMigrationMeta,
|
|
21
|
+
): string {
|
|
22
|
+
const factoryNames = collectFactoryNames(calls);
|
|
23
|
+
const imports = buildImports(factoryNames);
|
|
24
|
+
const planBody = calls.map((c) => c.accept(renderCallVisitor)).join(',\n');
|
|
25
|
+
const describeMethod = meta ? buildDescribeMethod(meta) : '';
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
imports,
|
|
29
|
+
'',
|
|
30
|
+
'class M extends Migration {',
|
|
31
|
+
describeMethod,
|
|
32
|
+
' override plan() {',
|
|
33
|
+
' return [',
|
|
34
|
+
indent(planBody, 6),
|
|
35
|
+
' ];',
|
|
36
|
+
' }',
|
|
37
|
+
'}',
|
|
38
|
+
'',
|
|
39
|
+
'export default M;',
|
|
40
|
+
'Migration.run(import.meta.url, M);',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectFactoryNames(calls: ReadonlyArray<OpFactoryCall>): string[] {
|
|
46
|
+
const names = new Set<string>();
|
|
47
|
+
for (const call of calls) {
|
|
48
|
+
names.add(call.factory);
|
|
49
|
+
}
|
|
50
|
+
return [...names].sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildImports(factoryNames: string[]): string {
|
|
54
|
+
const lines = ["import { Migration } from '@prisma-next/family-mongo/migration';"];
|
|
55
|
+
if (factoryNames.length > 0) {
|
|
56
|
+
lines.push(`import { ${factoryNames.join(', ')} } from '@prisma-next/target-mongo/migration';`);
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildDescribeMethod(meta: RenderMigrationMeta): string {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push(' override describe() {');
|
|
64
|
+
lines.push(' return {');
|
|
65
|
+
lines.push(` from: ${JSON.stringify(meta.from)},`);
|
|
66
|
+
lines.push(` to: ${JSON.stringify(meta.to)},`);
|
|
67
|
+
if (meta.kind) {
|
|
68
|
+
lines.push(` kind: ${JSON.stringify(meta.kind)},`);
|
|
69
|
+
}
|
|
70
|
+
if (meta.labels && meta.labels.length > 0) {
|
|
71
|
+
lines.push(` labels: ${renderLiteral(meta.labels)},`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(' };');
|
|
74
|
+
lines.push(' }');
|
|
75
|
+
lines.push('');
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const renderCallVisitor: OpFactoryCallVisitor<string> = {
|
|
80
|
+
createIndex(call: CreateIndexCall) {
|
|
81
|
+
return call.options
|
|
82
|
+
? `createIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)}, ${renderLiteral(call.options)})`
|
|
83
|
+
: `createIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)})`;
|
|
84
|
+
},
|
|
85
|
+
dropIndex(call: DropIndexCall) {
|
|
86
|
+
return `dropIndex(${renderLiteral(call.collection)}, ${renderLiteral(call.keys)})`;
|
|
87
|
+
},
|
|
88
|
+
createCollection(call: CreateCollectionCall) {
|
|
89
|
+
return call.options
|
|
90
|
+
? `createCollection(${renderLiteral(call.collection)}, ${renderLiteral(call.options)})`
|
|
91
|
+
: `createCollection(${renderLiteral(call.collection)})`;
|
|
92
|
+
},
|
|
93
|
+
dropCollection(call: DropCollectionCall) {
|
|
94
|
+
return `dropCollection(${renderLiteral(call.collection)})`;
|
|
95
|
+
},
|
|
96
|
+
collMod(call: CollModCall) {
|
|
97
|
+
return call.meta
|
|
98
|
+
? `collMod(${renderLiteral(call.collection)}, ${renderLiteral(call.options)}, ${renderLiteral(call.meta)})`
|
|
99
|
+
: `collMod(${renderLiteral(call.collection)}, ${renderLiteral(call.options)})`;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function renderLiteral(value: unknown): string {
|
|
104
|
+
if (value === undefined) return 'undefined';
|
|
105
|
+
if (value === null) return 'null';
|
|
106
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
107
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
if (value.length === 0) return '[]';
|
|
110
|
+
const items = value.map((v) => renderLiteral(v));
|
|
111
|
+
const singleLine = `[${items.join(', ')}]`;
|
|
112
|
+
if (singleLine.length <= 80) return singleLine;
|
|
113
|
+
return `[\n${items.map((i) => ` ${i}`).join(',\n')},\n]`;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === 'object') {
|
|
116
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
117
|
+
if (entries.length === 0) return '{}';
|
|
118
|
+
const items = entries.map(([k, v]) => `${renderKey(k)}: ${renderLiteral(v)}`);
|
|
119
|
+
const singleLine = `{ ${items.join(', ')} }`;
|
|
120
|
+
if (singleLine.length <= 80) return singleLine;
|
|
121
|
+
return `{\n${items.map((i) => ` ${i}`).join(',\n')},\n}`;
|
|
122
|
+
}
|
|
123
|
+
return String(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderKey(key: string): string {
|
|
127
|
+
if (key === '__proto__') return JSON.stringify(key);
|
|
128
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function indent(text: string, spaces: number): string {
|
|
132
|
+
const pad = ' '.repeat(spaces);
|
|
133
|
+
return text
|
|
134
|
+
.split('\n')
|
|
135
|
+
.map((line) => (line.trim() ? `${pad}${line}` : line))
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
package/src/exports/control.ts
CHANGED
|
@@ -1 +1,26 @@
|
|
|
1
|
+
export { contractToMongoSchemaIR } from '../core/contract-to-schema';
|
|
2
|
+
export { formatMongoOperations } from '../core/ddl-formatter';
|
|
3
|
+
export { FilterEvaluator } from '../core/filter-evaluator';
|
|
1
4
|
export { initMarker, readMarker, updateMarker, writeLedgerEntry } from '../core/marker-ledger';
|
|
5
|
+
export {
|
|
6
|
+
deserializeMongoOp,
|
|
7
|
+
deserializeMongoOps,
|
|
8
|
+
serializeMongoOps,
|
|
9
|
+
} from '../core/mongo-ops-serializer';
|
|
10
|
+
export type { PlanCallsResult } from '../core/mongo-planner';
|
|
11
|
+
export { MongoMigrationPlanner } from '../core/mongo-planner';
|
|
12
|
+
export type { MarkerOperations, MongoRunnerDependencies } from '../core/mongo-runner';
|
|
13
|
+
export { MongoMigrationRunner } from '../core/mongo-runner';
|
|
14
|
+
export type { CollModMeta, OpFactoryCall, OpFactoryCallVisitor } from '../core/op-factory-call';
|
|
15
|
+
export {
|
|
16
|
+
CollModCall,
|
|
17
|
+
CreateCollectionCall,
|
|
18
|
+
CreateIndexCall,
|
|
19
|
+
DropCollectionCall,
|
|
20
|
+
DropIndexCall,
|
|
21
|
+
schemaCollectionToCreateCollectionOptions,
|
|
22
|
+
schemaIndexToCreateIndexOptions,
|
|
23
|
+
} from '../core/op-factory-call';
|
|
24
|
+
export { renderOps } from '../core/render-ops';
|
|
25
|
+
export type { RenderMigrationMeta } from '../core/render-typescript';
|
|
26
|
+
export { renderTypeScript } from '../core/render-typescript';
|