@prisma-next/target-mongo 0.5.0-dev.4 → 0.5.0-dev.40

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 (39) hide show
  1. package/README.md +2 -0
  2. package/dist/control.d.mts +40 -19
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +100 -103
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/descriptor-meta-D9_5quQi.mjs +14 -0
  7. package/dist/descriptor-meta-D9_5quQi.mjs.map +1 -0
  8. package/dist/{migration-factories-gwi81C8u.mjs → migration-factories-CoNYWrd1.mjs} +3 -1
  9. package/dist/migration-factories-CoNYWrd1.mjs.map +1 -0
  10. package/dist/migration.d.mts +7 -1
  11. package/dist/migration.d.mts.map +1 -1
  12. package/dist/migration.mjs +1 -1
  13. package/dist/{op-factory-call-BjNAcPSF.d.mts → op-factory-call--nK5dk8n.d.mts} +1 -1
  14. package/dist/{op-factory-call-BjNAcPSF.d.mts.map → op-factory-call--nK5dk8n.d.mts.map} +1 -1
  15. package/dist/pack.mjs +1 -11
  16. package/dist/pack.mjs.map +1 -1
  17. package/dist/runtime.d.mts +20 -0
  18. package/dist/runtime.d.mts.map +1 -0
  19. package/dist/runtime.mjs +28 -0
  20. package/dist/runtime.mjs.map +1 -0
  21. package/dist/schema-verify.d.mts +22 -0
  22. package/dist/schema-verify.d.mts.map +1 -0
  23. package/dist/schema-verify.mjs +3 -0
  24. package/dist/verify-mongo-schema-Daa7BMJY.mjs +582 -0
  25. package/dist/verify-mongo-schema-Daa7BMJY.mjs.map +1 -0
  26. package/package.json +18 -12
  27. package/src/core/marker-ledger.ts +90 -20
  28. package/src/core/migration-factories.ts +8 -0
  29. package/src/core/mongo-ops-serializer.ts +0 -8
  30. package/src/core/mongo-planner.ts +8 -2
  31. package/src/core/mongo-runner.ts +105 -70
  32. package/src/core/planner-produced-migration.ts +0 -1
  33. package/src/core/render-typescript.ts +3 -7
  34. package/src/core/schema-diff.ts +402 -0
  35. package/src/core/schema-verify/canonicalize-introspection.ts +389 -0
  36. package/src/core/schema-verify/verify-mongo-schema.ts +60 -0
  37. package/src/exports/runtime.ts +38 -0
  38. package/src/exports/schema-verify.ts +2 -0
  39. package/dist/migration-factories-gwi81C8u.mjs.map +0 -1
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Canonicalizes a live (introspected) `MongoSchemaIR` against the expected
3
+ * (contract-built) IR before diffing. MongoDB applies server-side defaults
4
+ * to several option/index families that are absent from authored contracts,
5
+ * which would otherwise cause `verifyMongoSchema` to report false-positive
6
+ * drift on a fresh `migration apply`.
7
+ *
8
+ * The normalization is contract-aware where it has to be: server defaults
9
+ * are stripped from the live IR for fields the contract did not specify, so
10
+ * a contract that *does* specify a value still gets compared faithfully.
11
+ *
12
+ * Symmetric defaults — like `changeStreamPreAndPostImages: { enabled: false }`,
13
+ * which is equivalent to "absent" on both sides — are stripped from both IRs
14
+ * so either authoring style verifies.
15
+ */
16
+
17
+ import type {
18
+ MongoSchemaCollection,
19
+ MongoSchemaCollectionOptions,
20
+ MongoSchemaIndex,
21
+ MongoSchemaIR,
22
+ } from '@prisma-next/mongo-schema-ir';
23
+ import {
24
+ MongoSchemaCollection as MongoSchemaCollectionCtor,
25
+ MongoSchemaCollectionOptions as MongoSchemaCollectionOptionsCtor,
26
+ MongoSchemaIndex as MongoSchemaIndexCtor,
27
+ MongoSchemaIR as MongoSchemaIRCtor,
28
+ } from '@prisma-next/mongo-schema-ir';
29
+ import { ifDefined } from '@prisma-next/utils/defined';
30
+
31
+ export interface CanonicalizedSchemas {
32
+ readonly live: MongoSchemaIR;
33
+ readonly expected: MongoSchemaIR;
34
+ }
35
+
36
+ export function canonicalizeSchemasForVerification(
37
+ live: MongoSchemaIR,
38
+ expected: MongoSchemaIR,
39
+ ): CanonicalizedSchemas {
40
+ const expectedByName = new Map<string, MongoSchemaCollection>();
41
+ for (const c of expected.collections) expectedByName.set(c.name, c);
42
+
43
+ const liveByName = new Map<string, MongoSchemaCollection>();
44
+ for (const c of live.collections) liveByName.set(c.name, c);
45
+
46
+ const canonicalLive = live.collections.map((c) =>
47
+ canonicalizeLiveCollection(c, expectedByName.get(c.name)),
48
+ );
49
+ const canonicalExpected = expected.collections.map((c) =>
50
+ canonicalizeExpectedCollection(c, liveByName.get(c.name)),
51
+ );
52
+
53
+ return {
54
+ live: new MongoSchemaIRCtor(canonicalLive),
55
+ expected: new MongoSchemaIRCtor(canonicalExpected),
56
+ };
57
+ }
58
+
59
+ function canonicalizeLiveCollection(
60
+ liveColl: MongoSchemaCollection,
61
+ expectedColl: MongoSchemaCollection | undefined,
62
+ ): MongoSchemaCollection {
63
+ const expectedIndexes = expectedColl?.indexes ?? [];
64
+ const indexes = liveColl.indexes.map((idx) =>
65
+ canonicalizeLiveIndex(idx, findExpectedIndexCounterpart(idx, expectedIndexes)),
66
+ );
67
+
68
+ const options = liveColl.options
69
+ ? canonicalizeLiveOptions(liveColl.options, expectedColl?.options)
70
+ : undefined;
71
+
72
+ return new MongoSchemaCollectionCtor({
73
+ name: liveColl.name,
74
+ indexes,
75
+ ...ifDefined('validator', liveColl.validator),
76
+ ...ifDefined('options', options),
77
+ });
78
+ }
79
+
80
+ function canonicalizeExpectedCollection(
81
+ expectedColl: MongoSchemaCollection,
82
+ liveColl: MongoSchemaCollection | undefined,
83
+ ): MongoSchemaCollection {
84
+ // Symmetric text-index key ordering: a contract-shaped text index preserves
85
+ // the user-authored field order, but the introspected counterpart comes
86
+ // back from MongoDB with `weights` keys in alphabetical order, so we
87
+ // canonicalize both sides to alphabetical text-key order. The order of
88
+ // text fields within the text block is semantically irrelevant — relevance
89
+ // is governed by `weights`, not key order.
90
+ const indexes = expectedColl.indexes.map(canonicalizeTextIndexKeyOrder);
91
+
92
+ const options = expectedColl.options
93
+ ? canonicalizeExpectedOptions(expectedColl.options, liveColl?.options)
94
+ : undefined;
95
+
96
+ return new MongoSchemaCollectionCtor({
97
+ name: expectedColl.name,
98
+ indexes,
99
+ ...ifDefined('validator', expectedColl.validator),
100
+ ...ifDefined('options', options),
101
+ });
102
+ }
103
+
104
+ function canonicalizeTextIndexKeyOrder(index: MongoSchemaIndex): MongoSchemaIndex {
105
+ const hasTextKey = index.keys.some((k) => k.direction === 'text');
106
+ if (!hasTextKey) return index;
107
+ return new MongoSchemaIndexCtor({
108
+ keys: sortTextKeys(index.keys),
109
+ unique: index.unique,
110
+ ...ifDefined('sparse', index.sparse),
111
+ ...ifDefined('expireAfterSeconds', index.expireAfterSeconds),
112
+ ...ifDefined('partialFilterExpression', index.partialFilterExpression),
113
+ ...ifDefined('wildcardProjection', index.wildcardProjection),
114
+ ...ifDefined('collation', index.collation),
115
+ ...ifDefined('weights', index.weights),
116
+ ...ifDefined('default_language', index.default_language),
117
+ ...ifDefined('language_override', index.language_override),
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Returns a copy of `keys` with text-direction entries sorted alphabetically
123
+ * while preserving the relative position of non-text entries. Compound text
124
+ * indexes (`{a: 1, _fts: 'text', _ftsx: 1, b: 1}`) keep their scalar
125
+ * prefix/suffix layout; only the contiguous text block is reordered.
126
+ */
127
+ function sortTextKeys(
128
+ keys: ReadonlyArray<{
129
+ readonly field: string;
130
+ readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';
131
+ }>,
132
+ ): ReadonlyArray<{
133
+ readonly field: string;
134
+ readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';
135
+ }> {
136
+ const textEntries = keys.filter((k) => k.direction === 'text');
137
+ if (textEntries.length <= 1) return keys;
138
+ const sortedText = [...textEntries].sort((a, b) => a.field.localeCompare(b.field));
139
+ let textIdx = 0;
140
+ return keys.map((k) => {
141
+ if (k.direction !== 'text') return k;
142
+ const next = sortedText[textIdx++];
143
+ /* v8 ignore next 3 -- @preserve invariant guard: textIdx is always < sortedText.length here because we only consume sortedText for text-direction entries and sortedText is built from the same filter. */
144
+ if (next === undefined) {
145
+ throw new Error('sortTextKeys: text-key counts mismatched');
146
+ }
147
+ return next;
148
+ });
149
+ }
150
+
151
+ function canonicalizeLiveIndex(
152
+ liveIndex: MongoSchemaIndex,
153
+ expectedIndex: MongoSchemaIndex | undefined,
154
+ ): MongoSchemaIndex {
155
+ const projectedKeys = sortTextKeys(projectTextIndexKeys(liveIndex));
156
+ const collation = liveIndex.collation
157
+ ? stripUnspecifiedFields(liveIndex.collation, expectedIndex?.collation)
158
+ : liveIndex.collation;
159
+
160
+ // Text-index server defaults: when the contract did not set
161
+ // `weights`/`default_language`/`language_override`, MongoDB applies
162
+ // `weights = {<field>: 1, ...}` (uniform), `'english'`, and `'language'`
163
+ // respectively. Strip them from live *only* when the live value matches
164
+ // those defaults — preserving non-default live values lets the verifier
165
+ // surface drift when the live index is tampered (e.g. weights tuned
166
+ // out-of-band, custom `default_language`/`language_override`) even though
167
+ // the contract authored neither.
168
+ const weights =
169
+ expectedIndex?.weights === undefined && hasDefaultTextWeights(projectedKeys, liveIndex.weights)
170
+ ? undefined
171
+ : liveIndex.weights;
172
+ const default_language =
173
+ expectedIndex?.default_language === undefined && liveIndex.default_language === 'english'
174
+ ? undefined
175
+ : liveIndex.default_language;
176
+ const language_override =
177
+ expectedIndex?.language_override === undefined && liveIndex.language_override === 'language'
178
+ ? undefined
179
+ : liveIndex.language_override;
180
+
181
+ return new MongoSchemaIndexCtor({
182
+ keys: projectedKeys,
183
+ unique: liveIndex.unique,
184
+ ...ifDefined('sparse', liveIndex.sparse),
185
+ ...ifDefined('expireAfterSeconds', liveIndex.expireAfterSeconds),
186
+ ...ifDefined('partialFilterExpression', liveIndex.partialFilterExpression),
187
+ ...ifDefined('wildcardProjection', liveIndex.wildcardProjection),
188
+ ...ifDefined('collation', collation),
189
+ ...ifDefined('weights', weights),
190
+ ...ifDefined('default_language', default_language),
191
+ ...ifDefined('language_override', language_override),
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Locate the contract-side index that corresponds to a live index for the
197
+ * purpose of contract-aware normalization. We deliberately match by the
198
+ * *projected* (contract-shaped) key list — so a live `_fts/_ftsx` index
199
+ * resolves to a contract `{title: 'text', body: 'text'}` index — and pick
200
+ * the first match. Contracts very rarely contain duplicate-key indexes; if
201
+ * we have no counterpart we fall back to no normalization for that index.
202
+ */
203
+ function findExpectedIndexCounterpart(
204
+ liveIndex: MongoSchemaIndex,
205
+ expectedIndexes: ReadonlyArray<MongoSchemaIndex>,
206
+ ): MongoSchemaIndex | undefined {
207
+ const projectedLiveKeys = sortTextKeys(projectTextIndexKeys(liveIndex));
208
+ const liveKeySig = projectedLiveKeys.map((k) => `${k.field}:${k.direction}`).join(',');
209
+ for (const expected of expectedIndexes) {
210
+ const expectedKeySig = sortTextKeys(expected.keys)
211
+ .map((k) => `${k.field}:${k.direction}`)
212
+ .join(',');
213
+ if (expectedKeySig === liveKeySig) return expected;
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ /**
219
+ * MongoDB expands a contract-shaped text index like
220
+ * `[{title: 'text'}, {body: 'text'}]` into its internal weighted vector
221
+ * representation `[{_fts: 'text'}, {_ftsx: 1}]`. We project back to
222
+ * contract-shaped keys via `weights`, iterating in whatever order MongoDB
223
+ * returns them (alphabetical, in practice). `sortTextKeys` is applied
224
+ * downstream to canonicalize the order on both sides, so this projection
225
+ * does not depend on a specific iteration order.
226
+ */
227
+ function projectTextIndexKeys(liveIndex: MongoSchemaIndex): ReadonlyArray<{
228
+ readonly field: string;
229
+ readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';
230
+ }> {
231
+ const isTextIndex =
232
+ liveIndex.keys.length >= 1 &&
233
+ liveIndex.keys.some((k) => k.field === '_fts' && k.direction === 'text');
234
+
235
+ if (!isTextIndex || !liveIndex.weights) return liveIndex.keys;
236
+
237
+ const textKeys = Object.keys(liveIndex.weights).map((field) => ({
238
+ field,
239
+ direction: 'text' as const,
240
+ }));
241
+
242
+ // Splice the projected text fields into the original `_fts/_ftsx` slot so
243
+ // compound text indexes that mix scalar prefixes *and* suffixes — e.g.
244
+ // `[prefix, _fts, _ftsx, suffix]` — keep their original layout. Flattening
245
+ // scalars first would yield `[prefix, suffix, ...text]`, which `sortTextKeys`
246
+ // (downstream) cannot recover because it only reorders text-direction
247
+ // entries within their existing positions. MongoDB always emits exactly one
248
+ // `_fts`/`_ftsx` pair per index, so we don't need to guard against
249
+ // duplicates.
250
+ type IndexKey = (typeof liveIndex.keys)[number];
251
+ const projectedKeys: IndexKey[] = [];
252
+ for (const key of liveIndex.keys) {
253
+ if (key.field === '_ftsx') continue;
254
+ if (key.field === '_fts') {
255
+ projectedKeys.push(...textKeys);
256
+ continue;
257
+ }
258
+ projectedKeys.push(key);
259
+ }
260
+ return projectedKeys;
261
+ }
262
+
263
+ /**
264
+ * MongoDB's server-default `weights` for an authored-without-weights text
265
+ * index assigns `1` to every text-direction field. Returns `true` only when
266
+ * `liveWeights` is exactly that uniform shape (every projected text-direction
267
+ * key weighted at `1`) so the canonicalizer leaves non-default weights —
268
+ * including out-of-band relevance tweaks — visible to the verifier.
269
+ *
270
+ * `projectTextIndexKeys` derives text-direction keys from the live weights
271
+ * map, so the count is guaranteed to match; we only have to check the value
272
+ * shape.
273
+ */
274
+ function hasDefaultTextWeights(
275
+ projectedKeys: ReadonlyArray<{
276
+ readonly field: string;
277
+ readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';
278
+ }>,
279
+ liveWeights: MongoSchemaIndex['weights'],
280
+ ): boolean {
281
+ if (liveWeights === undefined) return false;
282
+ const textFields = projectedKeys.filter((k) => k.direction === 'text').map((k) => k.field);
283
+ return textFields.every((field) => liveWeights[field] === 1);
284
+ }
285
+
286
+ function canonicalizeLiveOptions(
287
+ liveOptions: MongoSchemaCollectionOptions,
288
+ expectedOptions: MongoSchemaCollectionOptions | undefined,
289
+ ): MongoSchemaCollectionOptions | undefined {
290
+ const collation = liveOptions.collation
291
+ ? stripUnspecifiedFields(liveOptions.collation, expectedOptions?.collation)
292
+ : undefined;
293
+
294
+ // Timeseries: drop `bucketMaxSpanSeconds` (and any other server-applied
295
+ // extras) when the contract did not specify them.
296
+ const timeseries = liveOptions.timeseries
297
+ ? (stripUnspecifiedFields(
298
+ liveOptions.timeseries as Record<string, unknown>,
299
+ expectedOptions?.timeseries as Record<string, unknown> | undefined,
300
+ ) as MongoSchemaCollectionOptions['timeseries'])
301
+ : undefined;
302
+
303
+ // ClusteredIndex: drop `key`, `unique`, `v` and any other server-applied
304
+ // extras when the contract did not specify them.
305
+ const clusteredIndex = liveOptions.clusteredIndex
306
+ ? (stripUnspecifiedFields(
307
+ liveOptions.clusteredIndex as Record<string, unknown>,
308
+ expectedOptions?.clusteredIndex as Record<string, unknown> | undefined,
309
+ ) as MongoSchemaCollectionOptions['clusteredIndex'])
310
+ : undefined;
311
+
312
+ // changeStreamPreAndPostImages: `{enabled: false}` is equivalent to
313
+ // "absent". Strip it from live so it round-trips with a contract that
314
+ // omits the field, and is symmetric with the expected-side stripping.
315
+ const changeStreamPreAndPostImages = isDisabledChangeStream(
316
+ liveOptions.changeStreamPreAndPostImages,
317
+ )
318
+ ? undefined
319
+ : liveOptions.changeStreamPreAndPostImages;
320
+
321
+ const hasMeaningful =
322
+ liveOptions.capped || timeseries || collation || changeStreamPreAndPostImages || clusteredIndex;
323
+ if (!hasMeaningful) return undefined;
324
+
325
+ return new MongoSchemaCollectionOptionsCtor({
326
+ ...ifDefined('capped', liveOptions.capped),
327
+ ...ifDefined('timeseries', timeseries),
328
+ ...ifDefined('collation', collation),
329
+ ...ifDefined('changeStreamPreAndPostImages', changeStreamPreAndPostImages),
330
+ ...ifDefined('clusteredIndex', clusteredIndex),
331
+ });
332
+ }
333
+
334
+ function canonicalizeExpectedOptions(
335
+ expectedOptions: MongoSchemaCollectionOptions,
336
+ _liveOptions: MongoSchemaCollectionOptions | undefined,
337
+ ): MongoSchemaCollectionOptions | undefined {
338
+ // Symmetric: a contract `{enabled: false}` is equivalent to absent.
339
+ const changeStreamPreAndPostImages = isDisabledChangeStream(
340
+ expectedOptions.changeStreamPreAndPostImages,
341
+ )
342
+ ? undefined
343
+ : expectedOptions.changeStreamPreAndPostImages;
344
+
345
+ const hasMeaningful =
346
+ expectedOptions.capped ||
347
+ expectedOptions.timeseries ||
348
+ expectedOptions.collation ||
349
+ changeStreamPreAndPostImages ||
350
+ expectedOptions.clusteredIndex;
351
+ if (!hasMeaningful) return undefined;
352
+
353
+ return new MongoSchemaCollectionOptionsCtor({
354
+ ...ifDefined('capped', expectedOptions.capped),
355
+ ...ifDefined('timeseries', expectedOptions.timeseries),
356
+ ...ifDefined('collation', expectedOptions.collation),
357
+ ...ifDefined('changeStreamPreAndPostImages', changeStreamPreAndPostImages),
358
+ ...ifDefined('clusteredIndex', expectedOptions.clusteredIndex),
359
+ });
360
+ }
361
+
362
+ function isDisabledChangeStream(value: { enabled: boolean } | undefined): boolean {
363
+ return value !== undefined && value.enabled === false;
364
+ }
365
+
366
+ /**
367
+ * Returns a copy of `live` containing only the keys that `expected` defines.
368
+ * Used for option families whose individual sub-fields are server-extended
369
+ * with platform defaults (collation, timeseries, clusteredIndex), so the
370
+ * verifier should compare only what the contract actually authored.
371
+ *
372
+ * When `expected` is `undefined` — i.e. the contract authored nothing for
373
+ * this whole option family but the live IR has it — we return `live`
374
+ * unchanged so the verifier still sees the entire live block and can
375
+ * surface it as drift. (Returning `undefined` here would silently strip a
376
+ * server-attached collation/timeseries/clusteredIndex that the contract
377
+ * never asked for, hiding real drift.)
378
+ */
379
+ function stripUnspecifiedFields(
380
+ live: Record<string, unknown>,
381
+ expected: Record<string, unknown> | undefined,
382
+ ): Record<string, unknown> {
383
+ if (expected === undefined) return live;
384
+ const out: Record<string, unknown> = {};
385
+ for (const key of Object.keys(expected)) {
386
+ if (Object.hasOwn(live, key)) out[key] = live[key];
387
+ }
388
+ return out;
389
+ }
@@ -0,0 +1,60 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
2
+ import type {
3
+ OperationContext,
4
+ VerifyDatabaseSchemaResult,
5
+ } from '@prisma-next/framework-components/control';
6
+ import { VERIFY_CODE_SCHEMA_FAILURE } from '@prisma-next/framework-components/control';
7
+ import type { MongoContract } from '@prisma-next/mongo-contract';
8
+ import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
9
+ import { ifDefined } from '@prisma-next/utils/defined';
10
+ import { contractToMongoSchemaIR } from '../contract-to-schema';
11
+ import { diffMongoSchemas } from '../schema-diff';
12
+ import { canonicalizeSchemasForVerification } from './canonicalize-introspection';
13
+
14
+ export interface VerifyMongoSchemaOptions {
15
+ readonly contract: MongoContract;
16
+ readonly schema: MongoSchemaIR;
17
+ readonly strict: boolean;
18
+ readonly context?: OperationContext;
19
+ /**
20
+ * Active framework components participating in this composition. Mongo
21
+ * verification does not currently consult them, but the parameter exists
22
+ * for parity with `verifySqlSchema` so callers can pass the same envelope.
23
+ */
24
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;
25
+ }
26
+
27
+ export function verifyMongoSchema(options: VerifyMongoSchemaOptions): VerifyDatabaseSchemaResult {
28
+ const { contract, schema, strict, context } = options;
29
+ const startTime = Date.now();
30
+
31
+ const expectedIR = contractToMongoSchemaIR(contract);
32
+ // Strip server-applied defaults (and authored equivalents) before diffing so
33
+ // the verifier compares like-with-like — see `canonicalize-introspection.ts`.
34
+ const { live: canonicalLive, expected: canonicalExpected } = canonicalizeSchemasForVerification(
35
+ schema,
36
+ expectedIR,
37
+ );
38
+ const { root, issues, counts } = diffMongoSchemas(canonicalLive, canonicalExpected, strict);
39
+
40
+ const ok = counts.fail === 0;
41
+ const profileHash = typeof contract.profileHash === 'string' ? contract.profileHash : '';
42
+
43
+ return {
44
+ ok,
45
+ ...ifDefined('code', ok ? undefined : VERIFY_CODE_SCHEMA_FAILURE),
46
+ summary: ok ? 'Schema matches contract' : `Schema verification found ${counts.fail} issue(s)`,
47
+ contract: {
48
+ storageHash: contract.storage.storageHash,
49
+ ...(profileHash ? { profileHash } : {}),
50
+ },
51
+ target: { expected: contract.target },
52
+ schema: { issues, root, counts },
53
+ meta: {
54
+ strict,
55
+ ...ifDefined('contractPath', context?.contractPath),
56
+ ...ifDefined('configPath', context?.configPath),
57
+ },
58
+ timings: { total: Date.now() - startTime },
59
+ };
60
+ }
@@ -0,0 +1,38 @@
1
+ import type {
2
+ RuntimeTargetDescriptor,
3
+ RuntimeTargetInstance,
4
+ } from '@prisma-next/framework-components/execution';
5
+ import { createMongoCodecRegistry, type MongoCodecRegistry } from '@prisma-next/mongo-codec';
6
+ import { mongoTargetDescriptorMeta } from '../core/descriptor-meta';
7
+
8
+ export interface MongoRuntimeTargetInstance extends RuntimeTargetInstance<'mongo', 'mongo'> {}
9
+
10
+ /**
11
+ * Target-mongo deliberately does NOT import `MongoRuntimeTargetDescriptor`
12
+ * from `@prisma-next/mongo-runtime`. The target package is a control-plane
13
+ * residence and must not pull the Mongo execution-plane package into its
14
+ * dependency closure. The runtime descriptor here is shaped to satisfy the
15
+ * framework's `RuntimeTargetDescriptor` plus the structural
16
+ * `MongoStaticContributions` (`codecs`) that `@prisma-next/mongo-runtime`
17
+ * consumers narrow to at composition time.
18
+ */
19
+ const mongoRuntimeTargetDescriptor: RuntimeTargetDescriptor<
20
+ 'mongo',
21
+ 'mongo',
22
+ MongoRuntimeTargetInstance
23
+ > & {
24
+ readonly codecs: () => MongoCodecRegistry;
25
+ } = {
26
+ ...mongoTargetDescriptorMeta,
27
+ // The target descriptor itself contributes no codecs — the standard set
28
+ // lives on the adapter descriptor (see `@prisma-next/adapter-mongo/runtime`).
29
+ codecs: () => createMongoCodecRegistry(),
30
+ create(): MongoRuntimeTargetInstance {
31
+ return {
32
+ familyId: 'mongo',
33
+ targetId: 'mongo',
34
+ };
35
+ },
36
+ };
37
+
38
+ export default mongoRuntimeTargetDescriptor;
@@ -0,0 +1,2 @@
1
+ export type { VerifyMongoSchemaOptions } from '../core/schema-verify/verify-mongo-schema';
2
+ export { verifyMongoSchema } from '../core/schema-verify/verify-mongo-schema';
@@ -1 +0,0 @@
1
- {"version":3,"file":"migration-factories-gwi81C8u.mjs","names":["MATCH_ALL_FILTER: MongoFilterExpr","precheck: readonly MongoDataTransformCheck[]","postcheck: readonly MongoDataTransformCheck[]","postcheckExpect: 'exists' | 'notExists'","run: MongoQueryPlan[]"],"sources":["../src/core/migration-factories.ts"],"sourcesContent":["import type {\n MongoDataTransformCheck,\n MongoDataTransformOperation,\n MongoFilterExpr,\n MongoIndexKey,\n} from '@prisma-next/mongo-query-ast/control';\nimport {\n buildIndexOpId,\n CollModCommand,\n type CollModOptions,\n CreateCollectionCommand,\n type CreateCollectionOptions,\n CreateIndexCommand,\n type CreateIndexOptions,\n DropCollectionCommand,\n DropIndexCommand,\n defaultMongoIndexName,\n keysToKeySpec,\n ListCollectionsCommand,\n ListIndexesCommand,\n MongoAndExpr,\n MongoExistsExpr,\n MongoFieldFilter,\n type MongoMigrationPlanOperation,\n} from '@prisma-next/mongo-query-ast/control';\nimport type { MongoQueryPlan } from '@prisma-next/mongo-query-ast/execution';\nimport type { CollModMeta } from './op-factory-call';\n\ninterface Buildable {\n build(): MongoQueryPlan;\n}\n\nfunction isBuildable(value: unknown): value is Buildable {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'build' in value &&\n typeof (value as { build: unknown }).build === 'function'\n );\n}\n\nfunction resolveQuery(value: MongoQueryPlan | Buildable): MongoQueryPlan {\n return isBuildable(value) ? value.build() : value;\n}\n\n// Every MongoDB document carries `_id`, so `exists('_id')` is equivalent to\n// \"match all\". The filter AST has no identity/always-true expression.\nconst MATCH_ALL_FILTER: MongoFilterExpr = MongoExistsExpr.exists('_id');\n\nexport function dataTransform(\n name: string,\n options: {\n check?: {\n source: () => MongoQueryPlan | Buildable;\n filter?: MongoFilterExpr;\n expect?: 'exists' | 'notExists';\n description?: string;\n };\n run: () => MongoQueryPlan | Buildable;\n },\n): MongoDataTransformOperation {\n let precheck: readonly MongoDataTransformCheck[] = [];\n let postcheck: readonly MongoDataTransformCheck[] = [];\n\n if (options.check) {\n const source = resolveQuery(options.check.source());\n const filter = options.check.filter ?? MATCH_ALL_FILTER;\n const description = options.check.description ?? `Check for data transform: ${name}`;\n const precheckExpect = options.check.expect ?? 'exists';\n const postcheckExpect: 'exists' | 'notExists' =\n precheckExpect === 'exists' ? 'notExists' : 'exists';\n\n precheck = [{ description, source, filter, expect: precheckExpect }];\n postcheck = [{ description, source, filter, expect: postcheckExpect }];\n }\n\n const run: MongoQueryPlan[] = [resolveQuery(options.run())];\n\n return {\n id: `data_transform.${name}`,\n label: `Data transform: ${name}`,\n operationClass: 'data',\n name,\n precheck,\n run,\n postcheck,\n };\n}\n\nfunction formatKeys(keys: ReadonlyArray<MongoIndexKey>): string {\n return keys.map((k) => `${k.field}:${k.direction}`).join(', ');\n}\n\nfunction isTextIndex(keys: ReadonlyArray<MongoIndexKey>): boolean {\n return keys.some((k) => k.direction === 'text');\n}\n\nfunction keyFilter(keys: ReadonlyArray<MongoIndexKey>) {\n return isTextIndex(keys)\n ? MongoFieldFilter.eq('key._fts', 'text')\n : MongoFieldFilter.eq('key', keysToKeySpec(keys));\n}\n\nexport function createIndex(\n collection: string,\n keys: ReadonlyArray<MongoIndexKey>,\n options?: CreateIndexOptions,\n): MongoMigrationPlanOperation {\n const name = defaultMongoIndexName(keys);\n const filter = keyFilter(keys);\n const fullFilter = options?.unique\n ? MongoAndExpr.of([filter, MongoFieldFilter.eq('unique', true)])\n : filter;\n\n return {\n id: buildIndexOpId('create', collection, keys),\n label: `Create index on ${collection} (${formatKeys(keys)})`,\n operationClass: 'additive',\n precheck: [\n {\n description: `index does not already exist on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'notExists',\n },\n ],\n execute: [\n {\n description: `create index on ${collection}`,\n command: new CreateIndexCommand(collection, keys, {\n ...options,\n unique: options?.unique ?? undefined,\n name,\n }),\n },\n ],\n postcheck: [\n {\n description: `index exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter: fullFilter,\n expect: 'exists',\n },\n ],\n };\n}\n\nexport function dropIndex(\n collection: string,\n keys: ReadonlyArray<MongoIndexKey>,\n): MongoMigrationPlanOperation {\n const indexName = defaultMongoIndexName(keys);\n const filter = keyFilter(keys);\n\n return {\n id: buildIndexOpId('drop', collection, keys),\n label: `Drop index on ${collection} (${formatKeys(keys)})`,\n operationClass: 'destructive',\n precheck: [\n {\n description: `index exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'exists',\n },\n ],\n execute: [\n {\n description: `drop index on ${collection}`,\n command: new DropIndexCommand(collection, indexName),\n },\n ],\n postcheck: [\n {\n description: `index no longer exists on ${collection}`,\n source: new ListIndexesCommand(collection),\n filter,\n expect: 'notExists',\n },\n ],\n };\n}\n\nexport function createCollection(\n collection: string,\n options?: CreateCollectionOptions,\n): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.create`,\n label: `Create collection ${collection}`,\n operationClass: 'additive',\n precheck: [\n {\n description: `collection ${collection} does not exist`,\n source: new ListCollectionsCommand(),\n filter: MongoFieldFilter.eq('name', collection),\n expect: 'notExists',\n },\n ],\n execute: [\n {\n description: `create collection ${collection}`,\n command: new CreateCollectionCommand(collection, options),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function dropCollection(collection: string): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.drop`,\n label: `Drop collection ${collection}`,\n operationClass: 'destructive',\n precheck: [],\n execute: [\n {\n description: `drop collection ${collection}`,\n command: new DropCollectionCommand(collection),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function setValidation(\n collection: string,\n schema: Record<string, unknown>,\n options?: { validationLevel?: 'strict' | 'moderate'; validationAction?: 'error' | 'warn' },\n): MongoMigrationPlanOperation {\n return {\n id: `collection.${collection}.setValidation`,\n label: `Set validation on ${collection}`,\n operationClass: 'destructive',\n precheck: [],\n execute: [\n {\n description: `set validation on ${collection}`,\n command: new CollModCommand(collection, {\n validator: { $jsonSchema: schema },\n validationLevel: options?.validationLevel,\n validationAction: options?.validationAction,\n }),\n },\n ],\n postcheck: [],\n };\n}\n\nexport function collMod(\n collection: string,\n options: CollModOptions,\n meta?: CollModMeta,\n): MongoMigrationPlanOperation {\n const hasValidator = options.validator != null && Object.keys(options.validator).length > 0;\n\n return {\n id: meta?.id ?? `collection.${collection}.collMod`,\n label: meta?.label ?? `Modify collection ${collection}`,\n operationClass: meta?.operationClass ?? 'destructive',\n precheck:\n options.validator != null\n ? [\n {\n description: `collection ${collection} exists`,\n source: new ListCollectionsCommand(),\n filter: MongoFieldFilter.eq('name', collection),\n expect: 'exists' as const,\n },\n ]\n : [],\n execute: [\n {\n description: `modify ${collection}`,\n command: new CollModCommand(collection, options),\n },\n ],\n postcheck: hasValidator\n ? [\n {\n description: `validator applied on ${collection}`,\n source: new ListCollectionsCommand(),\n filter: MongoAndExpr.of([\n MongoFieldFilter.eq('name', collection),\n ...(options.validationLevel\n ? [MongoFieldFilter.eq('options.validationLevel', options.validationLevel)]\n : []),\n ...(options.validationAction\n ? [MongoFieldFilter.eq('options.validationAction', options.validationAction)]\n : []),\n ]),\n expect: 'exists' as const,\n },\n ]\n : [],\n };\n}\n\nexport function validatedCollection(\n name: string,\n schema: Record<string, unknown>,\n indexes: ReadonlyArray<{ keys: MongoIndexKey[]; unique?: boolean }>,\n): MongoMigrationPlanOperation[] {\n return [\n createCollection(name, {\n validator: { $jsonSchema: schema },\n validationLevel: 'strict',\n validationAction: 'error',\n }),\n ...indexes.map((idx) => createIndex(name, idx.keys, { unique: idx.unique })),\n ];\n}\n"],"mappings":"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"}