@prisma-next/family-mongo 0.7.0 → 0.8.0-dev.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.
- package/README.md +22 -9
- package/dist/control-adapter.d.mts +75 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control.d.mts +60 -16
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +257 -276
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +131 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +54 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/mongo-contract-serializer-Co3EaTVj.mjs +98 -0
- package/dist/mongo-contract-serializer-Co3EaTVj.mjs.map +1 -0
- package/dist/schema-verify.d.mts +22 -2
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +1 -1
- package/dist/verify-mongo-schema-BL7t9YTB.mjs +592 -0
- package/dist/verify-mongo-schema-BL7t9YTB.mjs.map +1 -0
- package/package.json +20 -22
- package/src/core/contract-to-schema.ts +84 -0
- package/src/core/control-adapter.ts +97 -0
- package/src/core/control-instance.ts +275 -272
- package/src/core/control-target-descriptor.ts +26 -0
- package/src/core/control-types.ts +6 -4
- package/src/core/ir/mongo-contract-serializer-base.ts +124 -0
- package/src/core/ir/mongo-contract-serializer.ts +18 -0
- package/src/core/ir/mongo-schema-verifier-base.ts +87 -0
- package/src/core/operation-preview.ts +131 -0
- 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/control-adapter.ts +1 -0
- package/src/exports/control.ts +8 -1
- package/src/exports/ir.ts +3 -0
- package/src/exports/schema-verify.ts +4 -2
- package/src/core/mongo-target-descriptor.ts +0 -180
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { introspectSchema } from '@prisma-next/adapter-mongo/control';
|
|
2
1
|
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
3
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
4
3
|
import type {
|
|
@@ -22,291 +21,67 @@ import {
|
|
|
22
21
|
} from '@prisma-next/framework-components/control';
|
|
23
22
|
import { assertDescriptorSelfConsistency } from '@prisma-next/migration-tools/spaces';
|
|
24
23
|
import type { MongoContract } from '@prisma-next/mongo-contract';
|
|
25
|
-
import { validateMongoContract } from '@prisma-next/mongo-contract';
|
|
26
24
|
import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
|
|
27
|
-
import {
|
|
28
|
-
formatMongoOperations,
|
|
29
|
-
initMarker,
|
|
30
|
-
readAllMarkers,
|
|
31
|
-
readMarker,
|
|
32
|
-
updateMarker,
|
|
33
|
-
} from '@prisma-next/target-mongo/control';
|
|
34
|
-
import { verifyMongoSchema } from '@prisma-next/target-mongo/schema-verify';
|
|
35
25
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
36
|
-
import type {
|
|
26
|
+
import type { MongoControlAdapter, MongoControlAdapterDescriptor } from './control-adapter';
|
|
37
27
|
import type { MongoControlExtensionDescriptor } from './control-types';
|
|
28
|
+
import { MongoContractSerializer } from './ir/mongo-contract-serializer';
|
|
29
|
+
import { mongoOperationsToPreview } from './operation-preview';
|
|
38
30
|
import { mongoSchemaToView } from './schema-to-view';
|
|
31
|
+
import { verifyMongoSchema } from './schema-verify/verify-mongo-schema';
|
|
39
32
|
|
|
40
33
|
export interface MongoControlFamilyInstance
|
|
41
34
|
extends ControlFamilyInstance<'mongo', MongoSchemaIR>,
|
|
42
35
|
SchemaViewCapable<MongoSchemaIR>,
|
|
43
36
|
OperationPreviewCapable {
|
|
37
|
+
/**
|
|
38
|
+
* Validates the JSON contract envelope structurally and returns it
|
|
39
|
+
* cast to the framework `Contract` shape. The per-target serializer
|
|
40
|
+
* (held on the Mongo target descriptor) does the class-form wrap for
|
|
41
|
+
* downstream consumers; the family only needs the validated data.
|
|
42
|
+
*/
|
|
44
43
|
validateContract(contractJson: unknown): Contract;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return mongoDriver.db;
|
|
46
|
+
function deserializeMongoContract(contractJson: unknown): MongoContract {
|
|
47
|
+
// Structural validation only — the per-target serializer wraps the
|
|
48
|
+
// result in a class-form `MongoTargetContract` for downstream
|
|
49
|
+
// consumers (CLI, runner). The family-instance methods only read
|
|
50
|
+
// hash/target fields off the validated shape, so the unwrapped
|
|
51
|
+
// `MongoContract` is sufficient here and avoids a family→target
|
|
52
|
+
// runtime dep.
|
|
53
|
+
return new MongoContractSerializer().deserializeContract(contractJson);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const { driver, contract: rawContract, expectedTargetId, contractPath, configPath } = options;
|
|
76
|
-
const startTime = Date.now();
|
|
77
|
-
|
|
78
|
-
const validated = validateMongoContract<MongoContract>(rawContract);
|
|
79
|
-
const contract = validated.contract;
|
|
80
|
-
|
|
81
|
-
const contractStorageHash = contract.storage.storageHash;
|
|
82
|
-
const contractProfileHash = contract.profileHash;
|
|
83
|
-
const contractTarget = contract.target;
|
|
84
|
-
|
|
85
|
-
const baseOpts = {
|
|
86
|
-
contractStorageHash,
|
|
87
|
-
contractProfileHash,
|
|
88
|
-
expectedTargetId,
|
|
89
|
-
contractPath,
|
|
90
|
-
...ifDefined('configPath', configPath),
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (contractTarget !== expectedTargetId) {
|
|
94
|
-
return buildVerifyResult({
|
|
95
|
-
...baseOpts,
|
|
96
|
-
ok: false,
|
|
97
|
-
code: VERIFY_CODE_TARGET_MISMATCH,
|
|
98
|
-
summary: 'Target mismatch',
|
|
99
|
-
actualTargetId: contractTarget,
|
|
100
|
-
totalTime: Date.now() - startTime,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const db = extractDb(driver);
|
|
105
|
-
const marker = await readMarker(db, APP_SPACE_ID);
|
|
106
|
-
|
|
107
|
-
if (!marker) {
|
|
108
|
-
return buildVerifyResult({
|
|
109
|
-
...baseOpts,
|
|
110
|
-
ok: false,
|
|
111
|
-
code: VERIFY_CODE_MARKER_MISSING,
|
|
112
|
-
summary: 'Marker missing',
|
|
113
|
-
totalTime: Date.now() - startTime,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (marker.storageHash !== contractStorageHash) {
|
|
118
|
-
return buildVerifyResult({
|
|
119
|
-
...baseOpts,
|
|
120
|
-
ok: false,
|
|
121
|
-
code: VERIFY_CODE_HASH_MISMATCH,
|
|
122
|
-
summary: 'Hash mismatch',
|
|
123
|
-
marker,
|
|
124
|
-
totalTime: Date.now() - startTime,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (contractProfileHash && marker.profileHash !== contractProfileHash) {
|
|
129
|
-
return buildVerifyResult({
|
|
130
|
-
...baseOpts,
|
|
131
|
-
ok: false,
|
|
132
|
-
code: VERIFY_CODE_HASH_MISMATCH,
|
|
133
|
-
summary: 'Hash mismatch',
|
|
134
|
-
marker,
|
|
135
|
-
totalTime: Date.now() - startTime,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return buildVerifyResult({
|
|
140
|
-
...baseOpts,
|
|
141
|
-
ok: true,
|
|
142
|
-
summary: 'Database matches contract',
|
|
143
|
-
marker,
|
|
144
|
-
totalTime: Date.now() - startTime,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async schemaVerify(options: {
|
|
149
|
-
readonly driver: ControlDriverInstance<'mongo', string>;
|
|
150
|
-
readonly contract: unknown;
|
|
151
|
-
readonly strict: boolean;
|
|
152
|
-
readonly contractPath: string;
|
|
153
|
-
readonly configPath?: string;
|
|
154
|
-
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;
|
|
155
|
-
}): Promise<VerifyDatabaseSchemaResult> {
|
|
156
|
-
const { driver, contract: rawContract, strict, contractPath, configPath } = options;
|
|
157
|
-
|
|
158
|
-
const validated = validateMongoContract<MongoContract>(rawContract);
|
|
159
|
-
const contract = validated.contract;
|
|
160
|
-
|
|
161
|
-
const db = extractDb(driver);
|
|
162
|
-
const liveIR = await introspectSchema(db);
|
|
163
|
-
|
|
164
|
-
return verifyMongoSchema({
|
|
165
|
-
contract,
|
|
166
|
-
schema: liveIR,
|
|
167
|
-
strict,
|
|
168
|
-
frameworkComponents: options.frameworkComponents,
|
|
169
|
-
context: {
|
|
170
|
-
contractPath,
|
|
171
|
-
...ifDefined('configPath', configPath),
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
schemaVerifyAgainstSchema(options: {
|
|
177
|
-
readonly contract: unknown;
|
|
178
|
-
readonly schema: MongoSchemaIR;
|
|
179
|
-
readonly strict: boolean;
|
|
180
|
-
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;
|
|
181
|
-
}): VerifyDatabaseSchemaResult {
|
|
182
|
-
const validated = validateMongoContract<MongoContract>(options.contract);
|
|
183
|
-
return verifyMongoSchema({
|
|
184
|
-
contract: validated.contract,
|
|
185
|
-
schema: options.schema,
|
|
186
|
-
strict: options.strict,
|
|
187
|
-
frameworkComponents: options.frameworkComponents,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async sign(options: {
|
|
192
|
-
readonly driver: ControlDriverInstance<'mongo', string>;
|
|
193
|
-
readonly contract: unknown;
|
|
194
|
-
readonly contractPath: string;
|
|
195
|
-
readonly configPath?: string;
|
|
196
|
-
}): Promise<SignDatabaseResult> {
|
|
197
|
-
const { driver, contract: rawContract, contractPath, configPath } = options;
|
|
198
|
-
const startTime = Date.now();
|
|
199
|
-
|
|
200
|
-
const validated = validateMongoContract<MongoContract>(rawContract);
|
|
201
|
-
const contract = validated.contract;
|
|
202
|
-
|
|
203
|
-
const contractStorageHash = contract.storage.storageHash;
|
|
204
|
-
const contractProfileHash = contract.profileHash;
|
|
205
|
-
|
|
206
|
-
const db = extractDb(driver);
|
|
207
|
-
|
|
208
|
-
const existingMarker = await readMarker(db, APP_SPACE_ID);
|
|
209
|
-
|
|
210
|
-
let markerCreated = false;
|
|
211
|
-
let markerUpdated = false;
|
|
212
|
-
let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
|
|
213
|
-
|
|
214
|
-
if (!existingMarker) {
|
|
215
|
-
await initMarker(db, APP_SPACE_ID, {
|
|
216
|
-
storageHash: contractStorageHash,
|
|
217
|
-
profileHash: contractProfileHash,
|
|
218
|
-
});
|
|
219
|
-
markerCreated = true;
|
|
220
|
-
} else {
|
|
221
|
-
const storageHashMatches = existingMarker.storageHash === contractStorageHash;
|
|
222
|
-
const profileHashMatches = existingMarker.profileHash === contractProfileHash;
|
|
223
|
-
|
|
224
|
-
if (!storageHashMatches || !profileHashMatches) {
|
|
225
|
-
previousHashes = {
|
|
226
|
-
storageHash: existingMarker.storageHash,
|
|
227
|
-
profileHash: existingMarker.profileHash,
|
|
228
|
-
};
|
|
229
|
-
const updated = await updateMarker(db, APP_SPACE_ID, existingMarker.storageHash, {
|
|
230
|
-
storageHash: contractStorageHash,
|
|
231
|
-
profileHash: contractProfileHash,
|
|
232
|
-
});
|
|
233
|
-
if (!updated) {
|
|
234
|
-
throw new Error('CAS conflict: marker was modified by another process during sign');
|
|
235
|
-
}
|
|
236
|
-
markerUpdated = true;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let summary: string;
|
|
241
|
-
if (markerCreated) {
|
|
242
|
-
summary = 'Database signed (marker created)';
|
|
243
|
-
} else if (markerUpdated) {
|
|
244
|
-
summary = `Database signed (marker updated from ${previousHashes?.storageHash ?? 'unknown'})`;
|
|
245
|
-
} else {
|
|
246
|
-
summary = 'Database already signed with this contract';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
ok: true,
|
|
251
|
-
summary,
|
|
252
|
-
contract: {
|
|
253
|
-
storageHash: contractStorageHash,
|
|
254
|
-
profileHash: contractProfileHash,
|
|
255
|
-
},
|
|
256
|
-
target: {
|
|
257
|
-
expected: contract.target,
|
|
258
|
-
actual: contract.target,
|
|
259
|
-
},
|
|
260
|
-
marker: {
|
|
261
|
-
created: markerCreated,
|
|
262
|
-
updated: markerUpdated,
|
|
263
|
-
...ifDefined('previous', previousHashes),
|
|
264
|
-
},
|
|
265
|
-
meta: {
|
|
266
|
-
contractPath,
|
|
267
|
-
...ifDefined('configPath', configPath),
|
|
268
|
-
},
|
|
269
|
-
timings: {
|
|
270
|
-
total: Date.now() - startTime,
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async readMarker(options: {
|
|
276
|
-
readonly driver: ControlDriverInstance<'mongo', string>;
|
|
277
|
-
readonly space: string;
|
|
278
|
-
}): Promise<ContractMarkerRecord | null> {
|
|
279
|
-
const db = extractDb(options.driver);
|
|
280
|
-
return readMarker(db, options.space);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async readAllMarkers(options: {
|
|
284
|
-
readonly driver: ControlDriverInstance<'mongo', string>;
|
|
285
|
-
}): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
286
|
-
const db = extractDb(options.driver);
|
|
287
|
-
return readAllMarkers(db);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async introspect(options: {
|
|
291
|
-
readonly driver: ControlDriverInstance<'mongo', string>;
|
|
292
|
-
readonly contract?: unknown;
|
|
293
|
-
}): Promise<MongoSchemaIR> {
|
|
294
|
-
const db = extractDb(options.driver);
|
|
295
|
-
return introspectSchema(db);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
toSchemaView(schema: MongoSchemaIR): CoreSchemaView {
|
|
299
|
-
return mongoSchemaToView(schema);
|
|
300
|
-
}
|
|
56
|
+
/**
|
|
57
|
+
* Family-method contract input. By the time control-plane methods
|
|
58
|
+
* (`verify`, `verifySchema`, `sign`, …) are invoked through the CLI
|
|
59
|
+
* control client (`client.ts`), the input has already been threaded
|
|
60
|
+
* through `familyInstance.validateContract`. The value is therefore a
|
|
61
|
+
* class-form `MongoTargetContract` (or a structurally-equivalent
|
|
62
|
+
* envelope post-deserialization) and must NOT be re-fed through
|
|
63
|
+
* structural validation (arktype rejects extra keys like `namespaces`).
|
|
64
|
+
*
|
|
65
|
+
* The parameter type on the framework SPI is `unknown` for variance
|
|
66
|
+
* reasons (so the family can express its own contract type without
|
|
67
|
+
* leaking it to the framework). This helper recovers the validated
|
|
68
|
+
* shape with a single narrow cast.
|
|
69
|
+
*/
|
|
70
|
+
function asValidatedMongoContract(contract: unknown): MongoContract {
|
|
71
|
+
return contract as MongoContract;
|
|
72
|
+
}
|
|
301
73
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
74
|
+
function isMongoControlAdapter(value: unknown): value is MongoControlAdapter<'mongo'> {
|
|
75
|
+
return (
|
|
76
|
+
typeof value === 'object' &&
|
|
77
|
+
value !== null &&
|
|
78
|
+
'readMarker' in value &&
|
|
79
|
+
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
|
|
80
|
+
'readAllMarkers' in value &&
|
|
81
|
+
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
|
|
82
|
+
'introspectSchema' in value &&
|
|
83
|
+
typeof (value as { introspectSchema: unknown }).introspectSchema === 'function'
|
|
84
|
+
);
|
|
310
85
|
}
|
|
311
86
|
|
|
312
87
|
function buildVerifyResult(opts: {
|
|
@@ -373,5 +148,233 @@ export function createMongoFamilyInstance(controlStack: ControlStack): MongoCont
|
|
|
373
148
|
});
|
|
374
149
|
}
|
|
375
150
|
}
|
|
376
|
-
|
|
151
|
+
|
|
152
|
+
// Mongo dispatch surface. Every wire-level operation routes through
|
|
153
|
+
// the adapter resolved from the control stack; the family carries no
|
|
154
|
+
// direct imports of target/adapter/driver internals. Mirrors the SQL
|
|
155
|
+
// family's `getControlAdapter()` helper.
|
|
156
|
+
const adapter = controlStack.adapter as MongoControlAdapterDescriptor<'mongo'> | undefined;
|
|
157
|
+
const getControlAdapter = (): MongoControlAdapter<'mongo'> => {
|
|
158
|
+
if (!adapter) {
|
|
159
|
+
throw new Error('Mongo family requires an adapter descriptor in ControlStack');
|
|
160
|
+
}
|
|
161
|
+
const controlAdapter = adapter.create(controlStack as ControlStack<'mongo', 'mongo'>);
|
|
162
|
+
if (!isMongoControlAdapter(controlAdapter)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, or introspectSchema)',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return controlAdapter;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// The family-level driver type is `ControlDriverInstance<'mongo', string>`,
|
|
171
|
+
// but the SPI methods are typed against `<'mongo', 'mongo'>`. Today's only
|
|
172
|
+
// Mongo target is `'mongo'`, so the runtime values are identical; the cast
|
|
173
|
+
// satisfies the structural type-system mismatch on `targetId`.
|
|
174
|
+
const asMongoDriver = (
|
|
175
|
+
driver: ControlDriverInstance<'mongo', string>,
|
|
176
|
+
): ControlDriverInstance<'mongo', 'mongo'> => driver as ControlDriverInstance<'mongo', 'mongo'>;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
familyId: 'mongo' as const,
|
|
180
|
+
|
|
181
|
+
validateContract(contractJson: unknown): Contract {
|
|
182
|
+
// The deserialized class form (MongoTargetContract, owned by
|
|
183
|
+
// target-mongo) and the framework Contract are structurally
|
|
184
|
+
// compatible — same fields, just a class instance on the storage
|
|
185
|
+
// envelope. The cast preserves the framework signature.
|
|
186
|
+
return deserializeMongoContract(contractJson) as unknown as Contract;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async verify(options): Promise<VerifyDatabaseResult> {
|
|
190
|
+
const { driver, contract: rawContract, expectedTargetId, contractPath, configPath } = options;
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
|
|
193
|
+
const contract = asValidatedMongoContract(rawContract);
|
|
194
|
+
|
|
195
|
+
const contractStorageHash = contract.storage.storageHash;
|
|
196
|
+
const contractProfileHash = contract.profileHash;
|
|
197
|
+
const contractTarget = contract.target;
|
|
198
|
+
|
|
199
|
+
const baseOpts = {
|
|
200
|
+
contractStorageHash,
|
|
201
|
+
contractProfileHash,
|
|
202
|
+
expectedTargetId,
|
|
203
|
+
contractPath,
|
|
204
|
+
...ifDefined('configPath', configPath),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (contractTarget !== expectedTargetId) {
|
|
208
|
+
return buildVerifyResult({
|
|
209
|
+
...baseOpts,
|
|
210
|
+
ok: false,
|
|
211
|
+
code: VERIFY_CODE_TARGET_MISMATCH,
|
|
212
|
+
summary: 'Target mismatch',
|
|
213
|
+
actualTargetId: contractTarget,
|
|
214
|
+
totalTime: Date.now() - startTime,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const marker = await getControlAdapter().readMarker(asMongoDriver(driver), APP_SPACE_ID);
|
|
219
|
+
|
|
220
|
+
if (!marker) {
|
|
221
|
+
return buildVerifyResult({
|
|
222
|
+
...baseOpts,
|
|
223
|
+
ok: false,
|
|
224
|
+
code: VERIFY_CODE_MARKER_MISSING,
|
|
225
|
+
summary: 'Marker missing',
|
|
226
|
+
totalTime: Date.now() - startTime,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (marker.storageHash !== contractStorageHash) {
|
|
231
|
+
return buildVerifyResult({
|
|
232
|
+
...baseOpts,
|
|
233
|
+
ok: false,
|
|
234
|
+
code: VERIFY_CODE_HASH_MISMATCH,
|
|
235
|
+
summary: 'Hash mismatch',
|
|
236
|
+
marker,
|
|
237
|
+
totalTime: Date.now() - startTime,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (contractProfileHash && marker.profileHash !== contractProfileHash) {
|
|
242
|
+
return buildVerifyResult({
|
|
243
|
+
...baseOpts,
|
|
244
|
+
ok: false,
|
|
245
|
+
code: VERIFY_CODE_HASH_MISMATCH,
|
|
246
|
+
summary: 'Hash mismatch',
|
|
247
|
+
marker,
|
|
248
|
+
totalTime: Date.now() - startTime,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return buildVerifyResult({
|
|
253
|
+
...baseOpts,
|
|
254
|
+
ok: true,
|
|
255
|
+
summary: 'Database matches contract',
|
|
256
|
+
marker,
|
|
257
|
+
totalTime: Date.now() - startTime,
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
verifySchema(options: {
|
|
262
|
+
readonly contract: unknown;
|
|
263
|
+
readonly schema: MongoSchemaIR;
|
|
264
|
+
readonly strict: boolean;
|
|
265
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;
|
|
266
|
+
}): VerifyDatabaseSchemaResult {
|
|
267
|
+
const contract = asValidatedMongoContract(options.contract);
|
|
268
|
+
return verifyMongoSchema({
|
|
269
|
+
contract,
|
|
270
|
+
schema: options.schema,
|
|
271
|
+
strict: options.strict,
|
|
272
|
+
frameworkComponents: options.frameworkComponents,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async sign(options): Promise<SignDatabaseResult> {
|
|
277
|
+
const { driver, contract: rawContract, contractPath, configPath } = options;
|
|
278
|
+
const startTime = Date.now();
|
|
279
|
+
|
|
280
|
+
const contract = asValidatedMongoContract(rawContract);
|
|
281
|
+
|
|
282
|
+
const contractStorageHash = contract.storage.storageHash;
|
|
283
|
+
const contractProfileHash = contract.profileHash;
|
|
284
|
+
|
|
285
|
+
const controlAdapter = getControlAdapter();
|
|
286
|
+
const mongoDriver = asMongoDriver(driver);
|
|
287
|
+
|
|
288
|
+
const existingMarker = await controlAdapter.readMarker(mongoDriver, APP_SPACE_ID);
|
|
289
|
+
|
|
290
|
+
let markerCreated = false;
|
|
291
|
+
let markerUpdated = false;
|
|
292
|
+
let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
|
|
293
|
+
|
|
294
|
+
if (!existingMarker) {
|
|
295
|
+
await controlAdapter.initMarker(mongoDriver, APP_SPACE_ID, {
|
|
296
|
+
storageHash: contractStorageHash,
|
|
297
|
+
profileHash: contractProfileHash,
|
|
298
|
+
});
|
|
299
|
+
markerCreated = true;
|
|
300
|
+
} else {
|
|
301
|
+
const storageHashMatches = existingMarker.storageHash === contractStorageHash;
|
|
302
|
+
const profileHashMatches = existingMarker.profileHash === contractProfileHash;
|
|
303
|
+
|
|
304
|
+
if (!storageHashMatches || !profileHashMatches) {
|
|
305
|
+
previousHashes = {
|
|
306
|
+
storageHash: existingMarker.storageHash,
|
|
307
|
+
profileHash: existingMarker.profileHash,
|
|
308
|
+
};
|
|
309
|
+
const updated = await controlAdapter.updateMarker(
|
|
310
|
+
mongoDriver,
|
|
311
|
+
APP_SPACE_ID,
|
|
312
|
+
existingMarker.storageHash,
|
|
313
|
+
{
|
|
314
|
+
storageHash: contractStorageHash,
|
|
315
|
+
profileHash: contractProfileHash,
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
if (!updated) {
|
|
319
|
+
throw new Error('CAS conflict: marker was modified by another process during sign');
|
|
320
|
+
}
|
|
321
|
+
markerUpdated = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let summary: string;
|
|
326
|
+
if (markerCreated) {
|
|
327
|
+
summary = 'Database signed (marker created)';
|
|
328
|
+
} else if (markerUpdated) {
|
|
329
|
+
summary = `Database signed (marker updated from ${previousHashes?.storageHash ?? 'unknown'})`;
|
|
330
|
+
} else {
|
|
331
|
+
summary = 'Database already signed with this contract';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
summary,
|
|
337
|
+
contract: {
|
|
338
|
+
storageHash: contractStorageHash,
|
|
339
|
+
profileHash: contractProfileHash,
|
|
340
|
+
},
|
|
341
|
+
target: {
|
|
342
|
+
expected: contract.target,
|
|
343
|
+
actual: contract.target,
|
|
344
|
+
},
|
|
345
|
+
marker: {
|
|
346
|
+
created: markerCreated,
|
|
347
|
+
updated: markerUpdated,
|
|
348
|
+
...ifDefined('previous', previousHashes),
|
|
349
|
+
},
|
|
350
|
+
meta: {
|
|
351
|
+
contractPath,
|
|
352
|
+
...ifDefined('configPath', configPath),
|
|
353
|
+
},
|
|
354
|
+
timings: {
|
|
355
|
+
total: Date.now() - startTime,
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async readMarker(options): Promise<ContractMarkerRecord | null> {
|
|
361
|
+
return getControlAdapter().readMarker(asMongoDriver(options.driver), options.space);
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async readAllMarkers(options): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
365
|
+
return getControlAdapter().readAllMarkers(asMongoDriver(options.driver));
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
async introspect(options): Promise<MongoSchemaIR> {
|
|
369
|
+
return getControlAdapter().introspectSchema(asMongoDriver(options.driver));
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
toSchemaView(schema: MongoSchemaIR): CoreSchemaView {
|
|
373
|
+
return mongoSchemaToView(schema);
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
|
|
377
|
+
return mongoOperationsToPreview(operations);
|
|
378
|
+
},
|
|
379
|
+
};
|
|
377
380
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContractSerializer,
|
|
3
|
+
MigratableTargetDescriptor,
|
|
4
|
+
SchemaVerifier,
|
|
5
|
+
} from '@prisma-next/framework-components/control';
|
|
6
|
+
import type { MongoContract } from '@prisma-next/mongo-contract';
|
|
7
|
+
import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
|
|
8
|
+
import type { MongoControlFamilyInstance } from './control-instance';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mongo target control descriptor type. Extends the framework's
|
|
12
|
+
* `MigratableTargetDescriptor` with two named SPI properties next to
|
|
13
|
+
* the existing `migrations` capability:
|
|
14
|
+
*
|
|
15
|
+
* - `contractSerializer` — JSON to class boundary for Mongo contracts.
|
|
16
|
+
* - `schemaVerifier` — per-target verifier walking the family contract
|
|
17
|
+
* against `MongoSchemaIR`.
|
|
18
|
+
*
|
|
19
|
+
* The descriptor itself is the aggregator; no extra `Target<TContract,
|
|
20
|
+
* TSchema>` interface is introduced.
|
|
21
|
+
*/
|
|
22
|
+
export interface MongoControlTargetDescriptor<TContract extends MongoContract = MongoContract>
|
|
23
|
+
extends MigratableTargetDescriptor<'mongo', 'mongo', MongoControlFamilyInstance> {
|
|
24
|
+
readonly contractSerializer: ContractSerializer<TContract>;
|
|
25
|
+
readonly schemaVerifier: SchemaVerifier<TContract, MongoSchemaIR>;
|
|
26
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
ContractSpace,
|
|
3
3
|
ControlExtensionDescriptor,
|
|
4
4
|
} from '@prisma-next/framework-components/control';
|
|
5
|
-
import type { MongoContract,
|
|
5
|
+
import type { MongoContract, MongoStorageShape } from '@prisma-next/mongo-contract';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Mongo-family extension descriptor.
|
|
@@ -14,10 +14,12 @@ import type { MongoContract, MongoStorage } from '@prisma-next/mongo-contract';
|
|
|
14
14
|
* The shape comes from `@prisma-next/framework-components/control`
|
|
15
15
|
* (`ContractSpace`) — contract-space identity is a framework concept,
|
|
16
16
|
* not a Mongo-specific one. The Mongo family specialises the generic
|
|
17
|
-
* to `MongoContract<
|
|
18
|
-
*
|
|
17
|
+
* to `MongoContract<MongoStorageShape>` so descriptor authors see a
|
|
18
|
+
* typed contract value over the raw-JSON envelope shape; the runtime
|
|
19
|
+
* in-memory class `MongoStorage` structurally satisfies the shape.
|
|
20
|
+
* Mirrors `SqlControlExtensionDescriptor`.
|
|
19
21
|
*/
|
|
20
22
|
export interface MongoControlExtensionDescriptor
|
|
21
23
|
extends ControlExtensionDescriptor<'mongo', 'mongo'> {
|
|
22
|
-
readonly contractSpace?: ContractSpace<MongoContract<
|
|
24
|
+
readonly contractSpace?: ContractSpace<MongoContract<MongoStorageShape>>;
|
|
23
25
|
}
|