@prisma-next/family-mongo 0.12.0-dev.2 → 0.12.0-dev.20
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/dist/control-adapter.d.mts +9 -1
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control.d.mts +2 -2
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +5 -7
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +5 -1
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +9 -0
- package/dist/ir.mjs.map +1 -1
- package/dist/runtime.d.mts +2 -0
- package/dist/runtime.mjs +2 -0
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{verify-mongo-schema-Bhxvdah3.mjs → verify-mongo-schema-BXJqoE59.mjs} +156 -86
- package/dist/verify-mongo-schema-BXJqoE59.mjs.map +1 -0
- package/package.json +16 -15
- package/src/core/control-adapter.ts +18 -2
- package/src/core/control-instance.ts +10 -21
- package/src/core/default-namespace.ts +4 -0
- package/src/core/ir/mongo-schema-verifier-base.ts +21 -2
- package/src/core/schema-diff.ts +148 -84
- package/src/core/schema-verify/mongo-control-verify-emit.ts +53 -0
- package/src/core/schema-verify/verifier-disposition.ts +42 -0
- package/src/core/schema-verify/verify-mongo-schema.ts +20 -2
- package/src/exports/runtime.ts +4 -0
- package/dist/verify-mongo-schema-Bhxvdah3.mjs.map +0 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
2
6
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
7
|
import type {
|
|
4
8
|
ControlDriverInstance,
|
|
@@ -76,19 +80,6 @@ function asValidatedMongoContract(contract: unknown): MongoContract {
|
|
|
76
80
|
return contract as MongoContract;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
function isMongoControlAdapter(value: unknown): value is MongoControlAdapter<'mongo'> {
|
|
80
|
-
return (
|
|
81
|
-
typeof value === 'object' &&
|
|
82
|
-
value !== null &&
|
|
83
|
-
'readMarker' in value &&
|
|
84
|
-
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
|
|
85
|
-
'readAllMarkers' in value &&
|
|
86
|
-
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
|
|
87
|
-
'introspectSchema' in value &&
|
|
88
|
-
typeof (value as { introspectSchema: unknown }).introspectSchema === 'function'
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
83
|
function buildVerifyResult(opts: {
|
|
93
84
|
ok: boolean;
|
|
94
85
|
code?: string;
|
|
@@ -164,13 +155,7 @@ export function createMongoFamilyInstance(controlStack: ControlStack): MongoCont
|
|
|
164
155
|
if (!adapter) {
|
|
165
156
|
throw new Error('Mongo family requires an adapter descriptor in ControlStack');
|
|
166
157
|
}
|
|
167
|
-
|
|
168
|
-
if (!isMongoControlAdapter(controlAdapter)) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, or introspectSchema)',
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
return controlAdapter;
|
|
158
|
+
return adapter.create(controlStack as ControlStack<'mongo', 'mongo'>);
|
|
174
159
|
};
|
|
175
160
|
|
|
176
161
|
// The family-level driver type is `ControlDriverInstance<'mongo', string>`,
|
|
@@ -371,6 +356,10 @@ export function createMongoFamilyInstance(controlStack: ControlStack): MongoCont
|
|
|
371
356
|
return getControlAdapter().readAllMarkers(asMongoDriver(options.driver));
|
|
372
357
|
},
|
|
373
358
|
|
|
359
|
+
async readLedger(options): Promise<readonly LedgerEntryRecord[]> {
|
|
360
|
+
return getControlAdapter().readLedger(asMongoDriver(options.driver), options.space);
|
|
361
|
+
},
|
|
362
|
+
|
|
374
363
|
async introspect(options): Promise<MongoSchemaIR> {
|
|
375
364
|
return getControlAdapter().introspectSchema(asMongoDriver(options.driver));
|
|
376
365
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import { effectiveControlPolicy } from '@prisma-next/contract/types';
|
|
1
3
|
import type {
|
|
2
4
|
SchemaIssue,
|
|
3
5
|
SchemaVerifier,
|
|
@@ -5,7 +7,8 @@ import type {
|
|
|
5
7
|
SchemaVerifyResult,
|
|
6
8
|
} from '@prisma-next/framework-components/control';
|
|
7
9
|
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
8
|
-
import
|
|
10
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
11
|
+
import type { MongoCollection, MongoStorage } from '@prisma-next/mongo-contract';
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Mongo family `SchemaVerifier` abstract base. Commits the Mongo family
|
|
@@ -29,7 +32,7 @@ import type { MongoStorage } from '@prisma-next/mongo-contract';
|
|
|
29
32
|
* list when no extensions exist over the Mongo family alphabet.
|
|
30
33
|
*/
|
|
31
34
|
export abstract class MongoSchemaVerifierBase<
|
|
32
|
-
TContract extends { readonly storage: MongoStorage },
|
|
35
|
+
TContract extends { readonly storage: MongoStorage; readonly defaultControl?: ControlPolicy },
|
|
33
36
|
TSchema,
|
|
34
37
|
> implements SchemaVerifier<TContract, TSchema>
|
|
35
38
|
{
|
|
@@ -40,6 +43,22 @@ export abstract class MongoSchemaVerifierBase<
|
|
|
40
43
|
return { ok: issues.length === 0, issues };
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
protected effectiveCollectionControlPolicy(
|
|
47
|
+
contract: TContract,
|
|
48
|
+
collection: MongoCollection | undefined,
|
|
49
|
+
): ControlPolicy {
|
|
50
|
+
return effectiveControlPolicy(collection?.control, contract.defaultControl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected collectionControlPolicyForName(
|
|
54
|
+
contract: TContract,
|
|
55
|
+
collectionName: string,
|
|
56
|
+
): ControlPolicy {
|
|
57
|
+
const namespace = contract.storage.namespaces[UNBOUND_NAMESPACE_ID];
|
|
58
|
+
const collection = namespace?.collections[collectionName];
|
|
59
|
+
return this.effectiveCollectionControlPolicy(contract, collection);
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
protected verifyCommonMongoSchema(
|
|
44
63
|
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
45
64
|
): readonly SchemaIssue[] {
|
package/src/core/schema-diff.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
1
2
|
import type {
|
|
2
3
|
SchemaIssue,
|
|
3
4
|
SchemaVerificationNode,
|
|
@@ -9,11 +10,13 @@ import type {
|
|
|
9
10
|
MongoSchemaIR,
|
|
10
11
|
} from '@prisma-next/mongo-schema-ir';
|
|
11
12
|
import { canonicalize, deepEqual } from '@prisma-next/mongo-schema-ir';
|
|
13
|
+
import { emitMongoIssueAndNodeUnderControlPolicy } from './schema-verify/mongo-control-verify-emit';
|
|
12
14
|
|
|
13
15
|
export function diffMongoSchemas(
|
|
14
16
|
live: MongoSchemaIR,
|
|
15
17
|
expected: MongoSchemaIR,
|
|
16
18
|
strict: boolean,
|
|
19
|
+
collectionControlPolicy: (collectionName: string) => ControlPolicy,
|
|
17
20
|
): {
|
|
18
21
|
root: SchemaVerificationNode;
|
|
19
22
|
issues: SchemaIssue[];
|
|
@@ -32,54 +35,70 @@ export function diffMongoSchemas(
|
|
|
32
35
|
const expectedColl = expected.collection(name);
|
|
33
36
|
|
|
34
37
|
if (!liveColl && expectedColl) {
|
|
35
|
-
|
|
38
|
+
const controlPolicy = collectionControlPolicy(name);
|
|
39
|
+
const issue: SchemaIssue = {
|
|
36
40
|
kind: 'missing_table',
|
|
37
41
|
table: name,
|
|
38
42
|
message: `Collection "${name}" is missing from the database`,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
};
|
|
44
|
+
const disposition = emitMongoIssueAndNodeUnderControlPolicy(
|
|
45
|
+
controlPolicy,
|
|
46
|
+
issue,
|
|
47
|
+
{
|
|
48
|
+
status: 'fail',
|
|
49
|
+
kind: 'collection',
|
|
50
|
+
name,
|
|
51
|
+
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${name}`,
|
|
52
|
+
code: 'MISSING_COLLECTION',
|
|
53
|
+
message: `Collection "${name}" is missing`,
|
|
54
|
+
expected: name,
|
|
55
|
+
actual: null,
|
|
56
|
+
children: [],
|
|
57
|
+
},
|
|
58
|
+
issues,
|
|
59
|
+
collectionChildren,
|
|
60
|
+
);
|
|
61
|
+
if (disposition === 'fail') fail++;
|
|
62
|
+
else if (disposition === 'warn') warn++;
|
|
52
63
|
continue;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
if (liveColl && !expectedColl) {
|
|
56
|
-
const
|
|
57
|
-
|
|
67
|
+
const controlPolicy = collectionControlPolicy(name);
|
|
68
|
+
const issue: SchemaIssue = {
|
|
58
69
|
kind: 'extra_table',
|
|
59
70
|
table: name,
|
|
60
71
|
message: `Extra collection "${name}" exists in the database but not in the contract`,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
};
|
|
73
|
+
const baseStatus = strict ? 'fail' : 'warn';
|
|
74
|
+
const disposition = emitMongoIssueAndNodeUnderControlPolicy(
|
|
75
|
+
controlPolicy,
|
|
76
|
+
issue,
|
|
77
|
+
{
|
|
78
|
+
status: baseStatus,
|
|
79
|
+
kind: 'collection',
|
|
80
|
+
name,
|
|
81
|
+
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${name}`,
|
|
82
|
+
code: 'EXTRA_COLLECTION',
|
|
83
|
+
message: `Extra collection "${name}" found`,
|
|
84
|
+
expected: null,
|
|
85
|
+
actual: name,
|
|
86
|
+
children: [],
|
|
87
|
+
},
|
|
88
|
+
issues,
|
|
89
|
+
collectionChildren,
|
|
90
|
+
);
|
|
91
|
+
if (disposition === 'fail') fail++;
|
|
92
|
+
else if (disposition === 'warn') warn++;
|
|
75
93
|
continue;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
const lc = liveColl as MongoSchemaCollection;
|
|
79
97
|
const ec = expectedColl as MongoSchemaCollection;
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
98
|
+
const controlPolicy = collectionControlPolicy(name);
|
|
99
|
+
const indexChildren = diffIndexes(name, lc, ec, strict, controlPolicy, issues);
|
|
100
|
+
const validatorChildren = diffValidator(name, lc, ec, strict, controlPolicy, issues);
|
|
101
|
+
const optionsChildren = diffOptions(name, lc, ec, strict, controlPolicy, issues);
|
|
83
102
|
const children = [...indexChildren, ...validatorChildren, ...optionsChildren];
|
|
84
103
|
|
|
85
104
|
const worstStatus = children.reduce<'pass' | 'warn' | 'fail'>(
|
|
@@ -156,6 +175,7 @@ function diffIndexes(
|
|
|
156
175
|
live: MongoSchemaCollection,
|
|
157
176
|
expected: MongoSchemaCollection,
|
|
158
177
|
strict: boolean,
|
|
178
|
+
collectionControlPolicy: ControlPolicy,
|
|
159
179
|
issues: SchemaIssue[],
|
|
160
180
|
): SchemaVerificationNode[] {
|
|
161
181
|
const nodes: SchemaVerificationNode[] = [];
|
|
@@ -179,46 +199,58 @@ function diffIndexes(
|
|
|
179
199
|
children: [],
|
|
180
200
|
});
|
|
181
201
|
} else {
|
|
182
|
-
|
|
202
|
+
const issue: SchemaIssue = {
|
|
183
203
|
kind: 'index_mismatch',
|
|
184
204
|
table: collName,
|
|
185
205
|
indexOrConstraint: formatIndexName(idx),
|
|
186
206
|
message: `Index ${formatIndexName(idx)} missing on collection "${collName}"`,
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
};
|
|
208
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
209
|
+
collectionControlPolicy,
|
|
210
|
+
issue,
|
|
211
|
+
{
|
|
212
|
+
status: 'fail',
|
|
213
|
+
kind: 'index',
|
|
214
|
+
name: formatIndexName(idx),
|
|
215
|
+
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${collName}.indexes`,
|
|
216
|
+
code: 'MISSING_INDEX',
|
|
217
|
+
message: `Index ${formatIndexName(idx)} missing`,
|
|
218
|
+
expected: key,
|
|
219
|
+
actual: null,
|
|
220
|
+
children: [],
|
|
221
|
+
},
|
|
222
|
+
issues,
|
|
223
|
+
nodes,
|
|
224
|
+
);
|
|
199
225
|
}
|
|
200
226
|
}
|
|
201
227
|
|
|
202
228
|
for (const [key, idx] of liveLookup) {
|
|
203
229
|
if (!expectedLookup.has(key)) {
|
|
204
|
-
const
|
|
205
|
-
issues.push({
|
|
230
|
+
const issue: SchemaIssue = {
|
|
206
231
|
kind: 'extra_index',
|
|
207
232
|
table: collName,
|
|
208
233
|
indexOrConstraint: formatIndexName(idx),
|
|
209
234
|
message: `Extra index ${formatIndexName(idx)} on collection "${collName}"`,
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
};
|
|
236
|
+
const baseStatus = strict ? 'fail' : 'warn';
|
|
237
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
238
|
+
collectionControlPolicy,
|
|
239
|
+
issue,
|
|
240
|
+
{
|
|
241
|
+
status: baseStatus,
|
|
242
|
+
kind: 'index',
|
|
243
|
+
name: formatIndexName(idx),
|
|
244
|
+
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${collName}.indexes`,
|
|
245
|
+
code: 'EXTRA_INDEX',
|
|
246
|
+
message: `Extra index ${formatIndexName(idx)}`,
|
|
247
|
+
expected: null,
|
|
248
|
+
actual: key,
|
|
249
|
+
children: [],
|
|
250
|
+
},
|
|
251
|
+
issues,
|
|
252
|
+
nodes,
|
|
253
|
+
);
|
|
222
254
|
}
|
|
223
255
|
}
|
|
224
256
|
|
|
@@ -230,17 +262,21 @@ function diffValidator(
|
|
|
230
262
|
live: MongoSchemaCollection,
|
|
231
263
|
expected: MongoSchemaCollection,
|
|
232
264
|
strict: boolean,
|
|
265
|
+
collectionControlPolicy: ControlPolicy,
|
|
233
266
|
issues: SchemaIssue[],
|
|
234
267
|
): SchemaVerificationNode[] {
|
|
235
268
|
if (!live.validator && !expected.validator) return [];
|
|
236
269
|
|
|
237
270
|
if (expected.validator && !live.validator) {
|
|
238
|
-
|
|
271
|
+
const issue: SchemaIssue = {
|
|
239
272
|
kind: 'type_missing',
|
|
240
273
|
table: collName,
|
|
241
274
|
message: `Validator missing on collection "${collName}"`,
|
|
242
|
-
}
|
|
243
|
-
|
|
275
|
+
};
|
|
276
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
277
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
278
|
+
collectionControlPolicy,
|
|
279
|
+
issue,
|
|
244
280
|
{
|
|
245
281
|
status: 'fail',
|
|
246
282
|
kind: 'validator',
|
|
@@ -252,19 +288,25 @@ function diffValidator(
|
|
|
252
288
|
actual: null,
|
|
253
289
|
children: [],
|
|
254
290
|
},
|
|
255
|
-
|
|
291
|
+
issues,
|
|
292
|
+
nodes,
|
|
293
|
+
);
|
|
294
|
+
return nodes;
|
|
256
295
|
}
|
|
257
296
|
|
|
258
297
|
if (!expected.validator && live.validator) {
|
|
259
|
-
const
|
|
260
|
-
issues.push({
|
|
298
|
+
const issue: SchemaIssue = {
|
|
261
299
|
kind: 'extra_validator',
|
|
262
300
|
table: collName,
|
|
263
301
|
message: `Extra validator on collection "${collName}"`,
|
|
264
|
-
}
|
|
265
|
-
|
|
302
|
+
};
|
|
303
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
304
|
+
const baseStatus = strict ? 'fail' : 'warn';
|
|
305
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
306
|
+
collectionControlPolicy,
|
|
307
|
+
issue,
|
|
266
308
|
{
|
|
267
|
-
status,
|
|
309
|
+
status: baseStatus,
|
|
268
310
|
kind: 'validator',
|
|
269
311
|
name: 'validator',
|
|
270
312
|
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${collName}.validator`,
|
|
@@ -274,7 +316,10 @@ function diffValidator(
|
|
|
274
316
|
actual: canonicalize(live.validator.jsonSchema),
|
|
275
317
|
children: [],
|
|
276
318
|
},
|
|
277
|
-
|
|
319
|
+
issues,
|
|
320
|
+
nodes,
|
|
321
|
+
);
|
|
322
|
+
return nodes;
|
|
278
323
|
}
|
|
279
324
|
|
|
280
325
|
const liveVal = live.validator as NonNullable<typeof live.validator>;
|
|
@@ -287,14 +332,17 @@ function diffValidator(
|
|
|
287
332
|
liveVal.validationLevel !== expectedVal.validationLevel ||
|
|
288
333
|
liveVal.validationAction !== expectedVal.validationAction
|
|
289
334
|
) {
|
|
290
|
-
|
|
335
|
+
const issue: SchemaIssue = {
|
|
291
336
|
kind: 'type_mismatch',
|
|
292
337
|
table: collName,
|
|
293
338
|
expected: expectedSchema,
|
|
294
339
|
actual: liveSchema,
|
|
295
340
|
message: `Validator mismatch on collection "${collName}"`,
|
|
296
|
-
}
|
|
297
|
-
|
|
341
|
+
};
|
|
342
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
343
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
344
|
+
collectionControlPolicy,
|
|
345
|
+
issue,
|
|
298
346
|
{
|
|
299
347
|
status: 'fail',
|
|
300
348
|
kind: 'validator',
|
|
@@ -314,7 +362,10 @@ function diffValidator(
|
|
|
314
362
|
},
|
|
315
363
|
children: [],
|
|
316
364
|
},
|
|
317
|
-
|
|
365
|
+
issues,
|
|
366
|
+
nodes,
|
|
367
|
+
);
|
|
368
|
+
return nodes;
|
|
318
369
|
}
|
|
319
370
|
|
|
320
371
|
return [
|
|
@@ -337,21 +388,25 @@ function diffOptions(
|
|
|
337
388
|
live: MongoSchemaCollection,
|
|
338
389
|
expected: MongoSchemaCollection,
|
|
339
390
|
strict: boolean,
|
|
391
|
+
collectionControlPolicy: ControlPolicy,
|
|
340
392
|
issues: SchemaIssue[],
|
|
341
393
|
): SchemaVerificationNode[] {
|
|
342
394
|
if (!live.options && !expected.options) return [];
|
|
343
395
|
|
|
344
396
|
if (!expected.options && live.options) {
|
|
345
|
-
const
|
|
346
|
-
issues.push({
|
|
397
|
+
const issue: SchemaIssue = {
|
|
347
398
|
kind: 'type_mismatch',
|
|
348
399
|
table: collName,
|
|
349
400
|
actual: canonicalize(live.options),
|
|
350
401
|
message: `Extra collection options on "${collName}"`,
|
|
351
|
-
}
|
|
352
|
-
|
|
402
|
+
};
|
|
403
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
404
|
+
const baseStatus = strict ? 'fail' : 'warn';
|
|
405
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
406
|
+
collectionControlPolicy,
|
|
407
|
+
issue,
|
|
353
408
|
{
|
|
354
|
-
status,
|
|
409
|
+
status: baseStatus,
|
|
355
410
|
kind: 'options',
|
|
356
411
|
name: 'options',
|
|
357
412
|
contractPath: `storage.namespaces.${UNBOUND_NAMESPACE_ID}.collections.${collName}.options`,
|
|
@@ -361,7 +416,10 @@ function diffOptions(
|
|
|
361
416
|
actual: live.options,
|
|
362
417
|
children: [],
|
|
363
418
|
},
|
|
364
|
-
|
|
419
|
+
issues,
|
|
420
|
+
nodes,
|
|
421
|
+
);
|
|
422
|
+
return nodes;
|
|
365
423
|
}
|
|
366
424
|
|
|
367
425
|
if (deepEqual(live.options, expected.options)) {
|
|
@@ -380,14 +438,17 @@ function diffOptions(
|
|
|
380
438
|
];
|
|
381
439
|
}
|
|
382
440
|
|
|
383
|
-
|
|
441
|
+
const issue: SchemaIssue = {
|
|
384
442
|
kind: 'type_mismatch',
|
|
385
443
|
table: collName,
|
|
386
444
|
expected: canonicalize(expected.options),
|
|
387
445
|
actual: canonicalize(live.options),
|
|
388
446
|
message: `Collection options mismatch on "${collName}"`,
|
|
389
|
-
}
|
|
390
|
-
|
|
447
|
+
};
|
|
448
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
449
|
+
emitMongoIssueAndNodeUnderControlPolicy(
|
|
450
|
+
collectionControlPolicy,
|
|
451
|
+
issue,
|
|
391
452
|
{
|
|
392
453
|
status: 'fail',
|
|
393
454
|
kind: 'options',
|
|
@@ -399,5 +460,8 @@ function diffOptions(
|
|
|
399
460
|
actual: live.options,
|
|
400
461
|
children: [],
|
|
401
462
|
},
|
|
402
|
-
|
|
463
|
+
issues,
|
|
464
|
+
nodes,
|
|
465
|
+
);
|
|
466
|
+
return nodes;
|
|
403
467
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
SchemaIssue,
|
|
4
|
+
SchemaVerificationNode,
|
|
5
|
+
VerifierOutcome,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
import { verifierDisposition } from './verifier-disposition';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reconciles a control-policy disposition with the Mongo family's strict-mode
|
|
11
|
+
* contract for live-only extras — the single point where `strict` and the
|
|
12
|
+
* control policy meet.
|
|
13
|
+
*
|
|
14
|
+
* The control policy decides first; only a `fail` is reconciled against the
|
|
15
|
+
* caller's base node status. Call sites stamp a live-only extra with
|
|
16
|
+
* `strict ? 'fail' : 'warn'` and a declared missing/mismatch with `fail`, so
|
|
17
|
+
* this one step encodes the whole matrix:
|
|
18
|
+
*
|
|
19
|
+
* | live-vs-declared | strict | non-strict |
|
|
20
|
+
* |-------------------------------------|----------|------------|
|
|
21
|
+
* | declared missing / mismatch | fail | fail |
|
|
22
|
+
* | live-only extra (managed/tolerated) | fail | warn |
|
|
23
|
+
* | live-only extra (external) | suppress (extras ignored, both modes) |
|
|
24
|
+
* | anything (observed) | warn (both modes) |
|
|
25
|
+
*
|
|
26
|
+
* `tolerated` no longer diverges from `managed` on a non-strict extra index:
|
|
27
|
+
* both soften to `warn`, because the softening comes from the base status the
|
|
28
|
+
* caller already computed from `strict`, not from per-policy special-casing.
|
|
29
|
+
*/
|
|
30
|
+
function reconcileMongoOutcome(
|
|
31
|
+
controlPolicy: ControlPolicy,
|
|
32
|
+
issueKind: SchemaIssue['kind'],
|
|
33
|
+
baseStatus: SchemaVerificationNode['status'],
|
|
34
|
+
): VerifierOutcome {
|
|
35
|
+
const disposition = verifierDisposition(controlPolicy, issueKind);
|
|
36
|
+
return disposition === 'fail' ? baseStatus : disposition;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function emitMongoIssueAndNodeUnderControlPolicy(
|
|
40
|
+
controlPolicy: ControlPolicy,
|
|
41
|
+
issue: SchemaIssue,
|
|
42
|
+
node: SchemaVerificationNode,
|
|
43
|
+
issues: SchemaIssue[],
|
|
44
|
+
nodes: SchemaVerificationNode[],
|
|
45
|
+
): VerifierOutcome {
|
|
46
|
+
const outcome = reconcileMongoOutcome(controlPolicy, issue.kind, node.status);
|
|
47
|
+
if (outcome === 'suppress') {
|
|
48
|
+
return 'suppress';
|
|
49
|
+
}
|
|
50
|
+
issues.push(issue);
|
|
51
|
+
nodes.push({ ...node, status: outcome });
|
|
52
|
+
return outcome;
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
SchemaIssue,
|
|
4
|
+
VerifierIssueCategory,
|
|
5
|
+
VerifierOutcome,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
import { dispositionForCategory } from '@prisma-next/framework-components/control';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classifies the verifier issue kinds the Mongo schema differ emits into the
|
|
11
|
+
* target-neutral categories the framework grades. Mongo only emits the kinds
|
|
12
|
+
* listed below (missing/extra collections, missing/extra indexes, missing/extra
|
|
13
|
+
* validators, and validator/options mismatches coded as `type_mismatch`); any
|
|
14
|
+
* other kind never reaches this classifier and is graded conservatively as a
|
|
15
|
+
* declared-incompatible divergence. Mongo owns this mapping rather than
|
|
16
|
+
* importing the SQL classifier — the two families share only the framework's
|
|
17
|
+
* category grading.
|
|
18
|
+
*/
|
|
19
|
+
export function classifyMongoVerifierIssueKind(kind: SchemaIssue['kind']): VerifierIssueCategory {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case 'extra_table':
|
|
22
|
+
return 'extraTopLevelObject';
|
|
23
|
+
case 'extra_index':
|
|
24
|
+
case 'extra_validator':
|
|
25
|
+
return 'extraAuxiliary';
|
|
26
|
+
case 'missing_table':
|
|
27
|
+
case 'type_missing':
|
|
28
|
+
return 'declaredMissing';
|
|
29
|
+
case 'index_mismatch':
|
|
30
|
+
case 'type_mismatch':
|
|
31
|
+
return 'declaredIncompatible';
|
|
32
|
+
default:
|
|
33
|
+
return 'declaredIncompatible';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function verifierDisposition(
|
|
38
|
+
controlPolicy: ControlPolicy,
|
|
39
|
+
issueKind: SchemaIssue['kind'],
|
|
40
|
+
): VerifierOutcome {
|
|
41
|
+
return dispositionForCategory(controlPolicy, classifyMongoVerifierIssueKind(issueKind));
|
|
42
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { type ControlPolicy, effectiveControlPolicy } from '@prisma-next/contract/types';
|
|
1
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
2
3
|
import type {
|
|
3
4
|
OperationContext,
|
|
4
5
|
VerifyDatabaseSchemaResult,
|
|
5
6
|
} from '@prisma-next/framework-components/control';
|
|
6
7
|
import { VERIFY_CODE_SCHEMA_FAILURE } from '@prisma-next/framework-components/control';
|
|
7
|
-
import
|
|
8
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
9
|
+
import type { MongoCollection, MongoContract } from '@prisma-next/mongo-contract';
|
|
8
10
|
import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
|
|
9
11
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
10
12
|
import { contractToMongoSchemaIR } from '../contract-to-schema';
|
|
@@ -35,7 +37,13 @@ export function verifyMongoSchema(options: VerifyMongoSchemaOptions): VerifyData
|
|
|
35
37
|
schema,
|
|
36
38
|
expectedIR,
|
|
37
39
|
);
|
|
38
|
-
const
|
|
40
|
+
const collectionControlPolicy = resolveMongoCollectionControlPolicy(contract);
|
|
41
|
+
const { root, issues, counts } = diffMongoSchemas(
|
|
42
|
+
canonicalLive,
|
|
43
|
+
canonicalExpected,
|
|
44
|
+
strict,
|
|
45
|
+
collectionControlPolicy,
|
|
46
|
+
);
|
|
39
47
|
|
|
40
48
|
const ok = counts.fail === 0;
|
|
41
49
|
const profileHash = typeof contract.profileHash === 'string' ? contract.profileHash : '';
|
|
@@ -58,3 +66,13 @@ export function verifyMongoSchema(options: VerifyMongoSchemaOptions): VerifyData
|
|
|
58
66
|
timings: { total: Date.now() - startTime },
|
|
59
67
|
};
|
|
60
68
|
}
|
|
69
|
+
|
|
70
|
+
function resolveMongoCollectionControlPolicy(
|
|
71
|
+
contract: MongoContract,
|
|
72
|
+
): (collectionName: string) => ControlPolicy {
|
|
73
|
+
const namespace = contract.storage.namespaces[UNBOUND_NAMESPACE_ID];
|
|
74
|
+
const collections: Record<string, MongoCollection> = namespace?.collections ?? {};
|
|
75
|
+
const defaultControl = contract.defaultControl;
|
|
76
|
+
return (collectionName: string) =>
|
|
77
|
+
effectiveControlPolicy(collections[collectionName]?.control, defaultControl);
|
|
78
|
+
}
|