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