@prisma-next/target-mongo 0.5.0-dev.3 → 0.5.0-dev.30

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-mongo-schema-P0TRBJNs.mjs","names":["issues: SchemaIssue[]","collectionChildren: SchemaVerificationNode[]","nodes: SchemaVerificationNode[]","MongoSchemaIRCtor","MongoSchemaCollectionCtor","MongoSchemaIndexCtor","projectedKeys: IndexKey[]","MongoSchemaCollectionOptionsCtor","out: Record<string, unknown>"],"sources":["../src/core/contract-to-schema.ts","../src/core/schema-diff.ts","../src/core/schema-verify/canonicalize-introspection.ts","../src/core/schema-verify/verify-mongo-schema.ts"],"sourcesContent":["import type {\n MongoContract,\n MongoStorageCollection,\n MongoStorageCollectionOptions,\n MongoStorageIndex,\n MongoStorageValidator,\n} from '@prisma-next/mongo-contract';\nimport {\n MongoSchemaCollection,\n MongoSchemaCollectionOptions,\n MongoSchemaIndex,\n MongoSchemaIR,\n MongoSchemaValidator,\n} from '@prisma-next/mongo-schema-ir';\n\nfunction convertIndex(index: MongoStorageIndex): MongoSchemaIndex {\n return new MongoSchemaIndex({\n keys: index.keys,\n unique: index.unique,\n sparse: index.sparse,\n expireAfterSeconds: index.expireAfterSeconds,\n partialFilterExpression: index.partialFilterExpression,\n wildcardProjection: index.wildcardProjection,\n collation: index.collation,\n weights: index.weights,\n default_language: index.default_language,\n language_override: index.language_override,\n });\n}\n\nfunction convertValidator(v: MongoStorageValidator): MongoSchemaValidator {\n return new MongoSchemaValidator({\n jsonSchema: v.jsonSchema,\n validationLevel: v.validationLevel,\n validationAction: v.validationAction,\n });\n}\n\nfunction convertOptions(o: MongoStorageCollectionOptions): MongoSchemaCollectionOptions {\n return new MongoSchemaCollectionOptions(o);\n}\n\nfunction convertCollection(name: string, def: MongoStorageCollection): MongoSchemaCollection {\n const indexes = (def.indexes ?? []).map(convertIndex);\n return new MongoSchemaCollection({\n name,\n indexes,\n ...(def.validator != null && { validator: convertValidator(def.validator) }),\n ...(def.options != null && { options: convertOptions(def.options) }),\n });\n}\n\nexport function contractToMongoSchemaIR(contract: MongoContract | null): MongoSchemaIR {\n if (!contract) {\n return new MongoSchemaIR([]);\n }\n\n const collections = Object.entries(contract.storage.collections).map(([name, def]) =>\n convertCollection(name, def),\n );\n\n return new MongoSchemaIR(collections);\n}\n","import type {\n SchemaIssue,\n SchemaVerificationNode,\n} from '@prisma-next/framework-components/control';\nimport type {\n MongoSchemaCollection,\n MongoSchemaIndex,\n MongoSchemaIR,\n} from '@prisma-next/mongo-schema-ir';\nimport { canonicalize, deepEqual } from '@prisma-next/mongo-schema-ir';\n\nexport function diffMongoSchemas(\n live: MongoSchemaIR,\n expected: MongoSchemaIR,\n strict: boolean,\n): {\n root: SchemaVerificationNode;\n issues: SchemaIssue[];\n counts: { pass: number; warn: number; fail: number; totalNodes: number };\n} {\n const issues: SchemaIssue[] = [];\n const collectionChildren: SchemaVerificationNode[] = [];\n let pass = 0;\n let warn = 0;\n let fail = 0;\n\n const allNames = new Set([...live.collectionNames, ...expected.collectionNames]);\n\n for (const name of [...allNames].sort()) {\n const liveColl = live.collection(name);\n const expectedColl = expected.collection(name);\n\n if (!liveColl && expectedColl) {\n issues.push({\n kind: 'missing_table',\n table: name,\n message: `Collection \"${name}\" is missing from the database`,\n });\n collectionChildren.push({\n status: 'fail',\n kind: 'collection',\n name,\n contractPath: `storage.collections.${name}`,\n code: 'MISSING_COLLECTION',\n message: `Collection \"${name}\" is missing`,\n expected: name,\n actual: null,\n children: [],\n });\n fail++;\n continue;\n }\n\n if (liveColl && !expectedColl) {\n const status = strict ? 'fail' : 'warn';\n issues.push({\n kind: 'extra_table',\n table: name,\n message: `Extra collection \"${name}\" exists in the database but not in the contract`,\n });\n collectionChildren.push({\n status,\n kind: 'collection',\n name,\n contractPath: `storage.collections.${name}`,\n code: 'EXTRA_COLLECTION',\n message: `Extra collection \"${name}\" found`,\n expected: null,\n actual: name,\n children: [],\n });\n if (status === 'fail') fail++;\n else warn++;\n continue;\n }\n\n const lc = liveColl as MongoSchemaCollection;\n const ec = expectedColl as MongoSchemaCollection;\n const indexChildren = diffIndexes(name, lc, ec, strict, issues);\n const validatorChildren = diffValidator(name, lc, ec, strict, issues);\n const optionsChildren = diffOptions(name, lc, ec, strict, issues);\n const children = [...indexChildren, ...validatorChildren, ...optionsChildren];\n\n const worstStatus = children.reduce<'pass' | 'warn' | 'fail'>(\n (s, c) => (c.status === 'fail' ? 'fail' : c.status === 'warn' && s !== 'fail' ? 'warn' : s),\n 'pass',\n );\n\n for (const c of children) {\n if (c.status === 'pass') pass++;\n else if (c.status === 'warn') warn++;\n else fail++;\n }\n\n if (children.length === 0) {\n pass++;\n }\n\n collectionChildren.push({\n status: worstStatus,\n kind: 'collection',\n name,\n contractPath: `storage.collections.${name}`,\n code: worstStatus === 'pass' ? 'MATCH' : 'DRIFT',\n message:\n worstStatus === 'pass' ? `Collection \"${name}\" matches` : `Collection \"${name}\" has drift`,\n expected: name,\n actual: name,\n children,\n });\n }\n\n const rootStatus = fail > 0 ? 'fail' : warn > 0 ? 'warn' : 'pass';\n const totalNodes = pass + warn + fail + collectionChildren.length;\n\n const root: SchemaVerificationNode = {\n status: rootStatus,\n kind: 'root',\n name: 'mongo-schema',\n contractPath: 'storage',\n code: rootStatus === 'pass' ? 'MATCH' : 'DRIFT',\n message: rootStatus === 'pass' ? 'Schema matches' : 'Schema has drift',\n expected: null,\n actual: null,\n children: collectionChildren,\n };\n\n return { root, issues, counts: { pass, warn, fail, totalNodes } };\n}\n\nfunction buildIndexLookupKey(index: MongoSchemaIndex): string {\n const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(',');\n const opts = [\n index.unique ? 'unique' : '',\n index.sparse ? 'sparse' : '',\n index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : '',\n index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : '',\n index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : '',\n index.collation ? `col:${canonicalize(index.collation)}` : '',\n index.weights ? `wt:${canonicalize(index.weights)}` : '',\n index.default_language ? `dl:${index.default_language}` : '',\n index.language_override ? `lo:${index.language_override}` : '',\n ]\n .filter(Boolean)\n .join(';');\n return opts ? `${keys}|${opts}` : keys;\n}\n\nfunction formatIndexName(index: MongoSchemaIndex): string {\n return index.keys.map((k) => `${k.field}:${k.direction}`).join(', ');\n}\n\nfunction diffIndexes(\n collName: string,\n live: MongoSchemaCollection,\n expected: MongoSchemaCollection,\n strict: boolean,\n issues: SchemaIssue[],\n): SchemaVerificationNode[] {\n const nodes: SchemaVerificationNode[] = [];\n const liveLookup = new Map<string, MongoSchemaIndex>();\n for (const idx of live.indexes) liveLookup.set(buildIndexLookupKey(idx), idx);\n\n const expectedLookup = new Map<string, MongoSchemaIndex>();\n for (const idx of expected.indexes) expectedLookup.set(buildIndexLookupKey(idx), idx);\n\n for (const [key, idx] of expectedLookup) {\n if (liveLookup.has(key)) {\n nodes.push({\n status: 'pass',\n kind: 'index',\n name: formatIndexName(idx),\n contractPath: `storage.collections.${collName}.indexes`,\n code: 'MATCH',\n message: `Index ${formatIndexName(idx)} matches`,\n expected: key,\n actual: key,\n children: [],\n });\n } else {\n issues.push({\n kind: 'index_mismatch',\n table: collName,\n indexOrConstraint: formatIndexName(idx),\n message: `Index ${formatIndexName(idx)} missing on collection \"${collName}\"`,\n });\n nodes.push({\n status: 'fail',\n kind: 'index',\n name: formatIndexName(idx),\n contractPath: `storage.collections.${collName}.indexes`,\n code: 'MISSING_INDEX',\n message: `Index ${formatIndexName(idx)} missing`,\n expected: key,\n actual: null,\n children: [],\n });\n }\n }\n\n for (const [key, idx] of liveLookup) {\n if (!expectedLookup.has(key)) {\n const status = strict ? 'fail' : 'warn';\n issues.push({\n kind: 'extra_index',\n table: collName,\n indexOrConstraint: formatIndexName(idx),\n message: `Extra index ${formatIndexName(idx)} on collection \"${collName}\"`,\n });\n nodes.push({\n status,\n kind: 'index',\n name: formatIndexName(idx),\n contractPath: `storage.collections.${collName}.indexes`,\n code: 'EXTRA_INDEX',\n message: `Extra index ${formatIndexName(idx)}`,\n expected: null,\n actual: key,\n children: [],\n });\n }\n }\n\n return nodes;\n}\n\nfunction diffValidator(\n collName: string,\n live: MongoSchemaCollection,\n expected: MongoSchemaCollection,\n strict: boolean,\n issues: SchemaIssue[],\n): SchemaVerificationNode[] {\n if (!live.validator && !expected.validator) return [];\n\n if (expected.validator && !live.validator) {\n issues.push({\n kind: 'type_missing',\n table: collName,\n message: `Validator missing on collection \"${collName}\"`,\n });\n return [\n {\n status: 'fail',\n kind: 'validator',\n name: 'validator',\n contractPath: `storage.collections.${collName}.validator`,\n code: 'MISSING_VALIDATOR',\n message: 'Validator missing',\n expected: canonicalize(expected.validator.jsonSchema),\n actual: null,\n children: [],\n },\n ];\n }\n\n if (!expected.validator && live.validator) {\n const status = strict ? 'fail' : 'warn';\n issues.push({\n kind: 'extra_validator',\n table: collName,\n message: `Extra validator on collection \"${collName}\"`,\n });\n return [\n {\n status,\n kind: 'validator',\n name: 'validator',\n contractPath: `storage.collections.${collName}.validator`,\n code: 'EXTRA_VALIDATOR',\n message: 'Extra validator found',\n expected: null,\n actual: canonicalize(live.validator.jsonSchema),\n children: [],\n },\n ];\n }\n\n const liveVal = live.validator as NonNullable<typeof live.validator>;\n const expectedVal = expected.validator as NonNullable<typeof expected.validator>;\n const liveSchema = canonicalize(liveVal.jsonSchema);\n const expectedSchema = canonicalize(expectedVal.jsonSchema);\n\n if (\n liveSchema !== expectedSchema ||\n liveVal.validationLevel !== expectedVal.validationLevel ||\n liveVal.validationAction !== expectedVal.validationAction\n ) {\n issues.push({\n kind: 'type_mismatch',\n table: collName,\n expected: expectedSchema,\n actual: liveSchema,\n message: `Validator mismatch on collection \"${collName}\"`,\n });\n return [\n {\n status: 'fail',\n kind: 'validator',\n name: 'validator',\n contractPath: `storage.collections.${collName}.validator`,\n code: 'VALIDATOR_MISMATCH',\n message: 'Validator mismatch',\n expected: {\n jsonSchema: expectedVal.jsonSchema,\n validationLevel: expectedVal.validationLevel,\n validationAction: expectedVal.validationAction,\n },\n actual: {\n jsonSchema: liveVal.jsonSchema,\n validationLevel: liveVal.validationLevel,\n validationAction: liveVal.validationAction,\n },\n children: [],\n },\n ];\n }\n\n return [\n {\n status: 'pass',\n kind: 'validator',\n name: 'validator',\n contractPath: `storage.collections.${collName}.validator`,\n code: 'MATCH',\n message: 'Validator matches',\n expected: expectedSchema,\n actual: liveSchema,\n children: [],\n },\n ];\n}\n\nfunction diffOptions(\n collName: string,\n live: MongoSchemaCollection,\n expected: MongoSchemaCollection,\n strict: boolean,\n issues: SchemaIssue[],\n): SchemaVerificationNode[] {\n if (!live.options && !expected.options) return [];\n\n if (!expected.options && live.options) {\n const status = strict ? 'fail' : 'warn';\n issues.push({\n kind: 'type_mismatch',\n table: collName,\n actual: canonicalize(live.options),\n message: `Extra collection options on \"${collName}\"`,\n });\n return [\n {\n status,\n kind: 'options',\n name: 'options',\n contractPath: `storage.collections.${collName}.options`,\n code: 'EXTRA_OPTIONS',\n message: 'Extra collection options found',\n expected: null,\n actual: live.options,\n children: [],\n },\n ];\n }\n\n if (deepEqual(live.options, expected.options)) {\n return [\n {\n status: 'pass',\n kind: 'options',\n name: 'options',\n contractPath: `storage.collections.${collName}.options`,\n code: 'MATCH',\n message: 'Collection options match',\n expected: canonicalize(expected.options),\n actual: canonicalize(live.options),\n children: [],\n },\n ];\n }\n\n issues.push({\n kind: 'type_mismatch',\n table: collName,\n expected: canonicalize(expected.options),\n actual: canonicalize(live.options),\n message: `Collection options mismatch on \"${collName}\"`,\n });\n return [\n {\n status: 'fail',\n kind: 'options',\n name: 'options',\n contractPath: `storage.collections.${collName}.options`,\n code: 'OPTIONS_MISMATCH',\n message: 'Collection options mismatch',\n expected: expected.options,\n actual: live.options,\n children: [],\n },\n ];\n}\n","/**\n * Canonicalizes a live (introspected) `MongoSchemaIR` against the expected\n * (contract-built) IR before diffing. MongoDB applies server-side defaults\n * to several option/index families that are absent from authored contracts,\n * which would otherwise cause `verifyMongoSchema` to report false-positive\n * drift on a fresh `migration apply`.\n *\n * The normalization is contract-aware where it has to be: server defaults\n * are stripped from the live IR for fields the contract did not specify, so\n * a contract that *does* specify a value still gets compared faithfully.\n *\n * Symmetric defaults — like `changeStreamPreAndPostImages: { enabled: false }`,\n * which is equivalent to \"absent\" on both sides — are stripped from both IRs\n * so either authoring style verifies.\n */\n\nimport type {\n MongoSchemaCollection,\n MongoSchemaCollectionOptions,\n MongoSchemaIndex,\n MongoSchemaIR,\n} from '@prisma-next/mongo-schema-ir';\nimport {\n MongoSchemaCollection as MongoSchemaCollectionCtor,\n MongoSchemaCollectionOptions as MongoSchemaCollectionOptionsCtor,\n MongoSchemaIndex as MongoSchemaIndexCtor,\n MongoSchemaIR as MongoSchemaIRCtor,\n} from '@prisma-next/mongo-schema-ir';\nimport { ifDefined } from '@prisma-next/utils/defined';\n\nexport interface CanonicalizedSchemas {\n readonly live: MongoSchemaIR;\n readonly expected: MongoSchemaIR;\n}\n\nexport function canonicalizeSchemasForVerification(\n live: MongoSchemaIR,\n expected: MongoSchemaIR,\n): CanonicalizedSchemas {\n const expectedByName = new Map<string, MongoSchemaCollection>();\n for (const c of expected.collections) expectedByName.set(c.name, c);\n\n const liveByName = new Map<string, MongoSchemaCollection>();\n for (const c of live.collections) liveByName.set(c.name, c);\n\n const canonicalLive = live.collections.map((c) =>\n canonicalizeLiveCollection(c, expectedByName.get(c.name)),\n );\n const canonicalExpected = expected.collections.map((c) =>\n canonicalizeExpectedCollection(c, liveByName.get(c.name)),\n );\n\n return {\n live: new MongoSchemaIRCtor(canonicalLive),\n expected: new MongoSchemaIRCtor(canonicalExpected),\n };\n}\n\nfunction canonicalizeLiveCollection(\n liveColl: MongoSchemaCollection,\n expectedColl: MongoSchemaCollection | undefined,\n): MongoSchemaCollection {\n const expectedIndexes = expectedColl?.indexes ?? [];\n const indexes = liveColl.indexes.map((idx) =>\n canonicalizeLiveIndex(idx, findExpectedIndexCounterpart(idx, expectedIndexes)),\n );\n\n const options = liveColl.options\n ? canonicalizeLiveOptions(liveColl.options, expectedColl?.options)\n : undefined;\n\n return new MongoSchemaCollectionCtor({\n name: liveColl.name,\n indexes,\n ...ifDefined('validator', liveColl.validator),\n ...ifDefined('options', options),\n });\n}\n\nfunction canonicalizeExpectedCollection(\n expectedColl: MongoSchemaCollection,\n liveColl: MongoSchemaCollection | undefined,\n): MongoSchemaCollection {\n // Symmetric text-index key ordering: a contract-shaped text index preserves\n // the user-authored field order, but the introspected counterpart comes\n // back from MongoDB with `weights` keys in alphabetical order, so we\n // canonicalize both sides to alphabetical text-key order. The order of\n // text fields within the text block is semantically irrelevant — relevance\n // is governed by `weights`, not key order.\n const indexes = expectedColl.indexes.map(canonicalizeTextIndexKeyOrder);\n\n const options = expectedColl.options\n ? canonicalizeExpectedOptions(expectedColl.options, liveColl?.options)\n : undefined;\n\n return new MongoSchemaCollectionCtor({\n name: expectedColl.name,\n indexes,\n ...ifDefined('validator', expectedColl.validator),\n ...ifDefined('options', options),\n });\n}\n\nfunction canonicalizeTextIndexKeyOrder(index: MongoSchemaIndex): MongoSchemaIndex {\n const hasTextKey = index.keys.some((k) => k.direction === 'text');\n if (!hasTextKey) return index;\n return new MongoSchemaIndexCtor({\n keys: sortTextKeys(index.keys),\n unique: index.unique,\n ...ifDefined('sparse', index.sparse),\n ...ifDefined('expireAfterSeconds', index.expireAfterSeconds),\n ...ifDefined('partialFilterExpression', index.partialFilterExpression),\n ...ifDefined('wildcardProjection', index.wildcardProjection),\n ...ifDefined('collation', index.collation),\n ...ifDefined('weights', index.weights),\n ...ifDefined('default_language', index.default_language),\n ...ifDefined('language_override', index.language_override),\n });\n}\n\n/**\n * Returns a copy of `keys` with text-direction entries sorted alphabetically\n * while preserving the relative position of non-text entries. Compound text\n * indexes (`{a: 1, _fts: 'text', _ftsx: 1, b: 1}`) keep their scalar\n * prefix/suffix layout; only the contiguous text block is reordered.\n */\nfunction sortTextKeys(\n keys: ReadonlyArray<{\n readonly field: string;\n readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';\n }>,\n): ReadonlyArray<{\n readonly field: string;\n readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';\n}> {\n const textEntries = keys.filter((k) => k.direction === 'text');\n if (textEntries.length <= 1) return keys;\n const sortedText = [...textEntries].sort((a, b) => a.field.localeCompare(b.field));\n let textIdx = 0;\n return keys.map((k) => {\n if (k.direction !== 'text') return k;\n const next = sortedText[textIdx++];\n /* 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. */\n if (next === undefined) {\n throw new Error('sortTextKeys: text-key counts mismatched');\n }\n return next;\n });\n}\n\nfunction canonicalizeLiveIndex(\n liveIndex: MongoSchemaIndex,\n expectedIndex: MongoSchemaIndex | undefined,\n): MongoSchemaIndex {\n const projectedKeys = sortTextKeys(projectTextIndexKeys(liveIndex));\n const collation = liveIndex.collation\n ? stripUnspecifiedFields(liveIndex.collation, expectedIndex?.collation)\n : liveIndex.collation;\n\n // Text-index server defaults: when the contract did not set\n // `weights`/`default_language`/`language_override`, MongoDB applies\n // `weights = {<field>: 1, ...}` (uniform), `'english'`, and `'language'`\n // respectively. Strip them from live *only* when the live value matches\n // those defaults — preserving non-default live values lets the verifier\n // surface drift when the live index is tampered (e.g. weights tuned\n // out-of-band, custom `default_language`/`language_override`) even though\n // the contract authored neither.\n const weights =\n expectedIndex?.weights === undefined && hasDefaultTextWeights(projectedKeys, liveIndex.weights)\n ? undefined\n : liveIndex.weights;\n const default_language =\n expectedIndex?.default_language === undefined && liveIndex.default_language === 'english'\n ? undefined\n : liveIndex.default_language;\n const language_override =\n expectedIndex?.language_override === undefined && liveIndex.language_override === 'language'\n ? undefined\n : liveIndex.language_override;\n\n return new MongoSchemaIndexCtor({\n keys: projectedKeys,\n unique: liveIndex.unique,\n ...ifDefined('sparse', liveIndex.sparse),\n ...ifDefined('expireAfterSeconds', liveIndex.expireAfterSeconds),\n ...ifDefined('partialFilterExpression', liveIndex.partialFilterExpression),\n ...ifDefined('wildcardProjection', liveIndex.wildcardProjection),\n ...ifDefined('collation', collation),\n ...ifDefined('weights', weights),\n ...ifDefined('default_language', default_language),\n ...ifDefined('language_override', language_override),\n });\n}\n\n/**\n * Locate the contract-side index that corresponds to a live index for the\n * purpose of contract-aware normalization. We deliberately match by the\n * *projected* (contract-shaped) key list — so a live `_fts/_ftsx` index\n * resolves to a contract `{title: 'text', body: 'text'}` index — and pick\n * the first match. Contracts very rarely contain duplicate-key indexes; if\n * we have no counterpart we fall back to no normalization for that index.\n */\nfunction findExpectedIndexCounterpart(\n liveIndex: MongoSchemaIndex,\n expectedIndexes: ReadonlyArray<MongoSchemaIndex>,\n): MongoSchemaIndex | undefined {\n const projectedLiveKeys = sortTextKeys(projectTextIndexKeys(liveIndex));\n const liveKeySig = projectedLiveKeys.map((k) => `${k.field}:${k.direction}`).join(',');\n for (const expected of expectedIndexes) {\n const expectedKeySig = sortTextKeys(expected.keys)\n .map((k) => `${k.field}:${k.direction}`)\n .join(',');\n if (expectedKeySig === liveKeySig) return expected;\n }\n return undefined;\n}\n\n/**\n * MongoDB expands a contract-shaped text index like\n * `[{title: 'text'}, {body: 'text'}]` into its internal weighted vector\n * representation `[{_fts: 'text'}, {_ftsx: 1}]`. We project back to\n * contract-shaped keys via `weights`, iterating in whatever order MongoDB\n * returns them (alphabetical, in practice). `sortTextKeys` is applied\n * downstream to canonicalize the order on both sides, so this projection\n * does not depend on a specific iteration order.\n */\nfunction projectTextIndexKeys(liveIndex: MongoSchemaIndex): ReadonlyArray<{\n readonly field: string;\n readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';\n}> {\n const isTextIndex =\n liveIndex.keys.length >= 1 &&\n liveIndex.keys.some((k) => k.field === '_fts' && k.direction === 'text');\n\n if (!isTextIndex || !liveIndex.weights) return liveIndex.keys;\n\n const textKeys = Object.keys(liveIndex.weights).map((field) => ({\n field,\n direction: 'text' as const,\n }));\n\n // Splice the projected text fields into the original `_fts/_ftsx` slot so\n // compound text indexes that mix scalar prefixes *and* suffixes — e.g.\n // `[prefix, _fts, _ftsx, suffix]` — keep their original layout. Flattening\n // scalars first would yield `[prefix, suffix, ...text]`, which `sortTextKeys`\n // (downstream) cannot recover because it only reorders text-direction\n // entries within their existing positions. MongoDB always emits exactly one\n // `_fts`/`_ftsx` pair per index, so we don't need to guard against\n // duplicates.\n type IndexKey = (typeof liveIndex.keys)[number];\n const projectedKeys: IndexKey[] = [];\n for (const key of liveIndex.keys) {\n if (key.field === '_ftsx') continue;\n if (key.field === '_fts') {\n projectedKeys.push(...textKeys);\n continue;\n }\n projectedKeys.push(key);\n }\n return projectedKeys;\n}\n\n/**\n * MongoDB's server-default `weights` for an authored-without-weights text\n * index assigns `1` to every text-direction field. Returns `true` only when\n * `liveWeights` is exactly that uniform shape (every projected text-direction\n * key weighted at `1`) so the canonicalizer leaves non-default weights —\n * including out-of-band relevance tweaks — visible to the verifier.\n *\n * `projectTextIndexKeys` derives text-direction keys from the live weights\n * map, so the count is guaranteed to match; we only have to check the value\n * shape.\n */\nfunction hasDefaultTextWeights(\n projectedKeys: ReadonlyArray<{\n readonly field: string;\n readonly direction: 'text' | 1 | -1 | '2dsphere' | '2d' | 'hashed';\n }>,\n liveWeights: MongoSchemaIndex['weights'],\n): boolean {\n if (liveWeights === undefined) return false;\n const textFields = projectedKeys.filter((k) => k.direction === 'text').map((k) => k.field);\n return textFields.every((field) => liveWeights[field] === 1);\n}\n\nfunction canonicalizeLiveOptions(\n liveOptions: MongoSchemaCollectionOptions,\n expectedOptions: MongoSchemaCollectionOptions | undefined,\n): MongoSchemaCollectionOptions | undefined {\n const collation = liveOptions.collation\n ? stripUnspecifiedFields(liveOptions.collation, expectedOptions?.collation)\n : undefined;\n\n // Timeseries: drop `bucketMaxSpanSeconds` (and any other server-applied\n // extras) when the contract did not specify them.\n const timeseries = liveOptions.timeseries\n ? (stripUnspecifiedFields(\n liveOptions.timeseries as Record<string, unknown>,\n expectedOptions?.timeseries as Record<string, unknown> | undefined,\n ) as MongoSchemaCollectionOptions['timeseries'])\n : undefined;\n\n // ClusteredIndex: drop `key`, `unique`, `v` and any other server-applied\n // extras when the contract did not specify them.\n const clusteredIndex = liveOptions.clusteredIndex\n ? (stripUnspecifiedFields(\n liveOptions.clusteredIndex as Record<string, unknown>,\n expectedOptions?.clusteredIndex as Record<string, unknown> | undefined,\n ) as MongoSchemaCollectionOptions['clusteredIndex'])\n : undefined;\n\n // changeStreamPreAndPostImages: `{enabled: false}` is equivalent to\n // \"absent\". Strip it from live so it round-trips with a contract that\n // omits the field, and is symmetric with the expected-side stripping.\n const changeStreamPreAndPostImages = isDisabledChangeStream(\n liveOptions.changeStreamPreAndPostImages,\n )\n ? undefined\n : liveOptions.changeStreamPreAndPostImages;\n\n const hasMeaningful =\n liveOptions.capped || timeseries || collation || changeStreamPreAndPostImages || clusteredIndex;\n if (!hasMeaningful) return undefined;\n\n return new MongoSchemaCollectionOptionsCtor({\n ...ifDefined('capped', liveOptions.capped),\n ...ifDefined('timeseries', timeseries),\n ...ifDefined('collation', collation),\n ...ifDefined('changeStreamPreAndPostImages', changeStreamPreAndPostImages),\n ...ifDefined('clusteredIndex', clusteredIndex),\n });\n}\n\nfunction canonicalizeExpectedOptions(\n expectedOptions: MongoSchemaCollectionOptions,\n _liveOptions: MongoSchemaCollectionOptions | undefined,\n): MongoSchemaCollectionOptions | undefined {\n // Symmetric: a contract `{enabled: false}` is equivalent to absent.\n const changeStreamPreAndPostImages = isDisabledChangeStream(\n expectedOptions.changeStreamPreAndPostImages,\n )\n ? undefined\n : expectedOptions.changeStreamPreAndPostImages;\n\n const hasMeaningful =\n expectedOptions.capped ||\n expectedOptions.timeseries ||\n expectedOptions.collation ||\n changeStreamPreAndPostImages ||\n expectedOptions.clusteredIndex;\n if (!hasMeaningful) return undefined;\n\n return new MongoSchemaCollectionOptionsCtor({\n ...ifDefined('capped', expectedOptions.capped),\n ...ifDefined('timeseries', expectedOptions.timeseries),\n ...ifDefined('collation', expectedOptions.collation),\n ...ifDefined('changeStreamPreAndPostImages', changeStreamPreAndPostImages),\n ...ifDefined('clusteredIndex', expectedOptions.clusteredIndex),\n });\n}\n\nfunction isDisabledChangeStream(value: { enabled: boolean } | undefined): boolean {\n return value !== undefined && value.enabled === false;\n}\n\n/**\n * Returns a copy of `live` containing only the keys that `expected` defines.\n * Used for option families whose individual sub-fields are server-extended\n * with platform defaults (collation, timeseries, clusteredIndex), so the\n * verifier should compare only what the contract actually authored.\n *\n * When `expected` is `undefined` — i.e. the contract authored nothing for\n * this whole option family but the live IR has it — we return `live`\n * unchanged so the verifier still sees the entire live block and can\n * surface it as drift. (Returning `undefined` here would silently strip a\n * server-attached collation/timeseries/clusteredIndex that the contract\n * never asked for, hiding real drift.)\n */\nfunction stripUnspecifiedFields(\n live: Record<string, unknown>,\n expected: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n if (expected === undefined) return live;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(expected)) {\n if (Object.hasOwn(live, key)) out[key] = live[key];\n }\n return out;\n}\n","import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport type {\n OperationContext,\n VerifyDatabaseSchemaResult,\n} from '@prisma-next/framework-components/control';\nimport { VERIFY_CODE_SCHEMA_FAILURE } from '@prisma-next/framework-components/control';\nimport type { MongoContract } from '@prisma-next/mongo-contract';\nimport type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { contractToMongoSchemaIR } from '../contract-to-schema';\nimport { diffMongoSchemas } from '../schema-diff';\nimport { canonicalizeSchemasForVerification } from './canonicalize-introspection';\n\nexport interface VerifyMongoSchemaOptions {\n readonly contract: MongoContract;\n readonly schema: MongoSchemaIR;\n readonly strict: boolean;\n readonly context?: OperationContext;\n /**\n * Active framework components participating in this composition. Mongo\n * verification does not currently consult them, but the parameter exists\n * for parity with `verifySqlSchema` so callers can pass the same envelope.\n */\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;\n}\n\nexport function verifyMongoSchema(options: VerifyMongoSchemaOptions): VerifyDatabaseSchemaResult {\n const { contract, schema, strict, context } = options;\n const startTime = Date.now();\n\n const expectedIR = contractToMongoSchemaIR(contract);\n // Strip server-applied defaults (and authored equivalents) before diffing so\n // the verifier compares like-with-like — see `canonicalize-introspection.ts`.\n const { live: canonicalLive, expected: canonicalExpected } = canonicalizeSchemasForVerification(\n schema,\n expectedIR,\n );\n const { root, issues, counts } = diffMongoSchemas(canonicalLive, canonicalExpected, strict);\n\n const ok = counts.fail === 0;\n const profileHash = typeof contract.profileHash === 'string' ? contract.profileHash : '';\n\n return {\n ok,\n ...ifDefined('code', ok ? undefined : VERIFY_CODE_SCHEMA_FAILURE),\n summary: ok ? 'Schema matches contract' : `Schema verification found ${counts.fail} issue(s)`,\n contract: {\n storageHash: contract.storage.storageHash,\n ...(profileHash ? { profileHash } : {}),\n },\n target: { expected: contract.target },\n schema: { issues, root, counts },\n meta: {\n strict,\n ...ifDefined('contractPath', context?.contractPath),\n ...ifDefined('configPath', context?.configPath),\n },\n timings: { total: Date.now() - startTime },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@prisma-next/target-mongo",
3
- "version": "0.5.0-dev.3",
3
+ "version": "0.5.0-dev.30",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "MongoDB target pack for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.1.29",
9
9
  "mongodb": "^6.16.0",
10
- "@prisma-next/contract": "0.5.0-dev.3",
11
- "@prisma-next/framework-components": "0.5.0-dev.3",
12
- "@prisma-next/migration-tools": "0.5.0-dev.3",
13
- "@prisma-next/mongo-contract": "0.5.0-dev.3",
14
- "@prisma-next/ts-render": "0.5.0-dev.3",
15
- "@prisma-next/mongo-query-ast": "0.5.0-dev.3",
16
- "@prisma-next/mongo-schema-ir": "0.5.0-dev.3",
17
- "@prisma-next/mongo-value": "0.5.0-dev.3",
18
- "@prisma-next/utils": "0.5.0-dev.3",
19
- "@prisma-next/mongo-lowering": "0.5.0-dev.3",
20
- "@prisma-next/errors": "0.5.0-dev.3"
10
+ "@prisma-next/contract": "0.5.0-dev.30",
11
+ "@prisma-next/errors": "0.5.0-dev.30",
12
+ "@prisma-next/framework-components": "0.5.0-dev.30",
13
+ "@prisma-next/migration-tools": "0.5.0-dev.30",
14
+ "@prisma-next/ts-render": "0.5.0-dev.30",
15
+ "@prisma-next/mongo-contract": "0.5.0-dev.30",
16
+ "@prisma-next/mongo-query-ast": "0.5.0-dev.30",
17
+ "@prisma-next/mongo-schema-ir": "0.5.0-dev.30",
18
+ "@prisma-next/mongo-value": "0.5.0-dev.30",
19
+ "@prisma-next/mongo-lowering": "0.5.0-dev.30",
20
+ "@prisma-next/utils": "0.5.0-dev.30"
21
21
  },
22
22
  "devDependencies": {
23
23
  "mongodb-memory-server": "10.4.3",
@@ -25,9 +25,9 @@
25
25
  "tsdown": "0.18.4",
26
26
  "typescript": "5.9.3",
27
27
  "vitest": "4.0.17",
28
+ "@prisma-next/tsdown": "0.0.0",
28
29
  "@prisma-next/test-utils": "0.0.1",
29
- "@prisma-next/tsconfig": "0.0.0",
30
- "@prisma-next/tsdown": "0.0.0"
30
+ "@prisma-next/tsconfig": "0.0.0"
31
31
  },
32
32
  "files": [
33
33
  "dist",
@@ -38,6 +38,7 @@
38
38
  "./control": "./dist/control.mjs",
39
39
  "./migration": "./dist/migration.mjs",
40
40
  "./pack": "./dist/pack.mjs",
41
+ "./schema-verify": "./dist/schema-verify.mjs",
41
42
  "./package.json": "./package.json"
42
43
  },
43
44
  "repository": {
@@ -48,6 +49,9 @@
48
49
  "main": "./dist/pack.mjs",
49
50
  "module": "./dist/pack.mjs",
50
51
  "types": "./dist/pack.d.mts",
52
+ "prismaNext": {
53
+ "minServerVersion": "6.0"
54
+ },
51
55
  "scripts": {
52
56
  "build": "tsdown",
53
57
  "test": "vitest run --passWithNoTests",
@@ -4,11 +4,45 @@ import {
4
4
  RawFindOneAndUpdateCommand,
5
5
  RawInsertOneCommand,
6
6
  } from '@prisma-next/mongo-query-ast/execution';
7
- import type { Db, Document } from 'mongodb';
7
+ import { type } from 'arktype';
8
+ import type { Db, Document, UpdateFilter } from 'mongodb';
8
9
 
9
10
  const COLLECTION = '_prisma_migrations';
10
11
  const MARKER_ID = 'marker';
11
12
 
13
+ // Same shape as the SQL marker row but camelCase + Mongo-native types:
14
+ // `Date` is BSON-hydrated, `meta` is a native object (not JSON-stringified),
15
+ // `_id` and any extension fields are tolerated. `invariants?` is optional —
16
+ // absent reads as `[]` (schemaless default); present-but-malformed throws.
17
+ const MongoMarkerDocSchema = type({
18
+ storageHash: 'string',
19
+ profileHash: 'string',
20
+ 'contractJson?': 'unknown | null',
21
+ 'canonicalVersion?': 'number | null',
22
+ 'updatedAt?': 'Date',
23
+ 'appTag?': 'string | null',
24
+ 'meta?': type({ '[string]': 'unknown' }).or('null'),
25
+ 'invariants?': type('string').array(),
26
+ '+': 'delete',
27
+ });
28
+
29
+ function parseMongoMarkerDoc(doc: unknown): ContractMarkerRecord {
30
+ const result = MongoMarkerDocSchema(doc);
31
+ if (result instanceof type.errors) {
32
+ throw new Error(`Invalid marker doc on ${COLLECTION}: ${result.summary}`);
33
+ }
34
+ return {
35
+ storageHash: result.storageHash,
36
+ profileHash: result.profileHash,
37
+ contractJson: result.contractJson ?? null,
38
+ canonicalVersion: result.canonicalVersion ?? null,
39
+ updatedAt: result.updatedAt ?? new Date(),
40
+ appTag: result.appTag ?? null,
41
+ meta: (result.meta as Record<string, unknown> | null) ?? {},
42
+ invariants: result.invariants ?? [],
43
+ };
44
+ }
45
+
12
46
  async function executeAggregate(db: Db, cmd: RawAggregateCommand): Promise<Document[]> {
13
47
  return db
14
48
  .collection(cmd.collection)
@@ -24,9 +58,16 @@ async function executeFindOneAndUpdate(
24
58
  db: Db,
25
59
  cmd: RawFindOneAndUpdateCommand,
26
60
  ): Promise<Document | null> {
61
+ // `cmd.update` is `Document | ReadonlyArray<Document>` per the AST. The
62
+ // MongoDB driver's `findOneAndUpdate` accepts the same shape under the
63
+ // type `UpdateFilter<T> | Document[]`. The driver's runtime path handles
64
+ // both forms identically — pipelines (array) and update docs (object).
65
+ // One cast to that union keeps the call single-arm.
27
66
  return db
28
67
  .collection(cmd.collection)
29
- .findOneAndUpdate(cmd.filter, cmd.update as Record<string, unknown>, { upsert: cmd.upsert });
68
+ .findOneAndUpdate(cmd.filter, cmd.update as UpdateFilter<Document> | Document[], {
69
+ upsert: cmd.upsert,
70
+ });
30
71
  }
31
72
 
32
73
  export async function readMarker(db: Db): Promise<ContractMarkerRecord | null> {
@@ -34,20 +75,16 @@ export async function readMarker(db: Db): Promise<ContractMarkerRecord | null> {
34
75
  const docs = await executeAggregate(db, cmd);
35
76
  const doc = docs[0];
36
77
  if (!doc) return null;
37
- return {
38
- storageHash: doc['storageHash'] as string,
39
- profileHash: doc['profileHash'] as string,
40
- contractJson: (doc['contractJson'] as unknown) ?? null,
41
- canonicalVersion: (doc['canonicalVersion'] as number) ?? null,
42
- updatedAt: doc['updatedAt'] as Date,
43
- appTag: (doc['appTag'] as string) ?? null,
44
- meta: (doc['meta'] as Record<string, unknown>) ?? {},
45
- };
78
+ return parseMongoMarkerDoc(doc);
46
79
  }
47
80
 
48
81
  export async function initMarker(
49
82
  db: Db,
50
- destination: { readonly storageHash: string; readonly profileHash: string },
83
+ destination: {
84
+ readonly storageHash: string;
85
+ readonly profileHash: string;
86
+ readonly invariants?: readonly string[];
87
+ },
51
88
  ): Promise<void> {
52
89
  const cmd = new RawInsertOneCommand(COLLECTION, {
53
90
  _id: MARKER_ID,
@@ -58,25 +95,58 @@ export async function initMarker(
58
95
  updatedAt: new Date(),
59
96
  appTag: null,
60
97
  meta: {},
98
+ invariants: destination.invariants ?? [],
61
99
  });
62
100
  await executeInsertOne(db, cmd);
63
101
  }
64
102
 
103
+ /**
104
+ * Updates the marker doc atomically (CAS on `expectedFrom`).
105
+ *
106
+ * `destination.invariants`:
107
+ * - `undefined` → existing field left untouched.
108
+ * - explicit value → merged into the existing field server-side via an
109
+ * aggregation pipeline (`$setUnion + $sortArray`), atomic at the
110
+ * document level. `[]` is a no-op merge.
111
+ */
65
112
  export async function updateMarker(
66
113
  db: Db,
67
114
  expectedFrom: string,
68
- destination: { readonly storageHash: string; readonly profileHash: string },
115
+ destination: {
116
+ readonly storageHash: string;
117
+ readonly profileHash: string;
118
+ readonly invariants?: readonly string[];
119
+ },
69
120
  ): Promise<boolean> {
121
+ const setBase: Record<string, unknown> = {
122
+ storageHash: destination.storageHash,
123
+ profileHash: destination.profileHash,
124
+ updatedAt: new Date(),
125
+ };
126
+ // When invariants is supplied, use an aggregation pipeline so the
127
+ // merge runs server-side against the doc's current value (atomic, no
128
+ // read-then-write window). When omitted, a regular update doc keeps
129
+ // the field untouched.
130
+ const update: Document | Document[] =
131
+ destination.invariants === undefined
132
+ ? { $set: setBase }
133
+ : [
134
+ {
135
+ $set: {
136
+ ...setBase,
137
+ invariants: {
138
+ $sortArray: {
139
+ input: { $setUnion: [{ $ifNull: ['$invariants', []] }, destination.invariants] },
140
+ sortBy: 1,
141
+ },
142
+ },
143
+ },
144
+ },
145
+ ];
70
146
  const cmd = new RawFindOneAndUpdateCommand(
71
147
  COLLECTION,
72
148
  { _id: MARKER_ID, storageHash: expectedFrom },
73
- {
74
- $set: {
75
- storageHash: destination.storageHash,
76
- profileHash: destination.profileHash,
77
- updatedAt: new Date(),
78
- },
79
- },
149
+ update,
80
150
  false,
81
151
  );
82
152
  const result = await executeFindOneAndUpdate(db, cmd);
@@ -24,6 +24,7 @@ import {
24
24
  type MongoMigrationPlanOperation,
25
25
  } from '@prisma-next/mongo-query-ast/control';
26
26
  import type { MongoQueryPlan } from '@prisma-next/mongo-query-ast/execution';
27
+ import { ifDefined } from '@prisma-next/utils/defined';
27
28
  import type { CollModMeta } from './op-factory-call';
28
29
 
29
30
  interface Buildable {
@@ -50,6 +51,12 @@ const MATCH_ALL_FILTER: MongoFilterExpr = MongoExistsExpr.exists('_id');
50
51
  export function dataTransform(
51
52
  name: string,
52
53
  options: {
54
+ /**
55
+ * Optional opt-in routing identity. Presence opts the transform into
56
+ * invariant-aware routing; absence means it is path-dependent and
57
+ * not referenceable from refs.
58
+ */
59
+ invariantId?: string;
53
60
  check?: {
54
61
  source: () => MongoQueryPlan | Buildable;
55
62
  filter?: MongoFilterExpr;
@@ -81,6 +88,7 @@ export function dataTransform(
81
88
  label: `Data transform: ${name}`,
82
89
  operationClass: 'data',
83
90
  name,
91
+ ...ifDefined('invariantId', options.invariantId),
84
92
  precheck,
85
93
  run,
86
94
  postcheck,
@@ -197,13 +197,9 @@ const PlanMetaJson = type({
197
197
  target: 'string',
198
198
  storageHash: 'string',
199
199
  lane: 'string',
200
- paramDescriptors: 'unknown[]',
201
200
  'targetFamily?': 'string',
202
201
  'profileHash?': 'string',
203
202
  'annotations?': 'Record<string, unknown>',
204
- 'refs?': 'Record<string, unknown>',
205
- 'projection?': 'Record<string, string> | string[]',
206
- 'projectionTypes?': 'Record<string, string>',
207
203
  });
208
204
 
209
205
  const QueryPlanJson = type({
@@ -427,13 +423,9 @@ export function deserializeMongoQueryPlan(json: unknown): MongoQueryPlan {
427
423
  target: m.target,
428
424
  storageHash: m.storageHash,
429
425
  lane: m.lane,
430
- paramDescriptors: m.paramDescriptors as PlanMeta['paramDescriptors'],
431
426
  ...ifDefined('targetFamily', m.targetFamily),
432
427
  ...ifDefined('profileHash', m.profileHash),
433
428
  ...ifDefined('annotations', m.annotations),
434
- ...ifDefined('refs', m.refs),
435
- ...ifDefined('projection', m.projection),
436
- ...ifDefined('projectionTypes', m.projectionTypes),
437
429
  };
438
430
  return { collection: data.collection, command, meta };
439
431
  }
@@ -225,7 +225,7 @@ export class MongoMigrationPlanner implements MigrationPlanner<'mongo', 'mongo'>
225
225
  readonly contract: unknown;
226
226
  readonly schema: unknown;
227
227
  readonly policy: MigrationOperationPolicy;
228
- readonly fromHash: string;
228
+ readonly fromHash: string | null;
229
229
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
230
230
  }): MigrationPlannerResult {
231
231
  const contract = options.contract as MongoContract;
@@ -8,7 +8,9 @@ import type {
8
8
  MigrationRunnerExecutionChecks,
9
9
  MigrationRunnerFailure,
10
10
  MigrationRunnerResult,
11
+ OperationContext,
11
12
  } from '@prisma-next/framework-components/control';
13
+ import type { MongoContract } from '@prisma-next/mongo-contract';
12
14
  import type { MongoAdapter, MongoDriver } from '@prisma-next/mongo-lowering';
13
15
  import type {
14
16
  AnyMongoMigrationOperation,
@@ -19,31 +21,28 @@ import type {
19
21
  MongoMigrationCheck,
20
22
  MongoMigrationPlanOperation,
21
23
  } from '@prisma-next/mongo-query-ast/control';
24
+ import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
22
25
  import { notOk, ok } from '@prisma-next/utils/result';
23
-
24
- const READ_ONLY_CHECK_COMMAND_KINDS: ReadonlySet<string> = new Set(['aggregate', 'rawAggregate']);
25
-
26
- function hasProfileHash(value: unknown): value is { readonly profileHash: string } {
27
- return (
28
- typeof value === 'object' &&
29
- value !== null &&
30
- Object.hasOwn(value, 'profileHash') &&
31
- typeof (value as { profileHash: unknown }).profileHash === 'string'
32
- );
33
- }
34
-
35
26
  import { FilterEvaluator } from './filter-evaluator';
36
27
  import { deserializeMongoOps } from './mongo-ops-serializer';
28
+ import { verifyMongoSchema } from './schema-verify/verify-mongo-schema';
29
+
30
+ const READ_ONLY_CHECK_COMMAND_KINDS: ReadonlySet<string> = new Set(['aggregate', 'rawAggregate']);
37
31
 
38
32
  export interface MarkerOperations {
39
33
  readMarker(): Promise<ContractMarkerRecord | null>;
40
34
  initMarker(destination: {
41
35
  readonly storageHash: string;
42
36
  readonly profileHash: string;
37
+ readonly invariants?: readonly string[];
43
38
  }): Promise<void>;
44
39
  updateMarker(
45
40
  expectedFrom: string,
46
- destination: { readonly storageHash: string; readonly profileHash: string },
41
+ destination: {
42
+ readonly storageHash: string;
43
+ readonly profileHash: string;
44
+ readonly invariants?: readonly string[];
45
+ },
47
46
  ): Promise<boolean>;
48
47
  writeLedgerEntry(entry: {
49
48
  readonly edgeId: string;
@@ -58,6 +57,27 @@ export interface MongoRunnerDependencies {
58
57
  readonly adapter: MongoAdapter;
59
58
  readonly driver: MongoDriver;
60
59
  readonly markerOps: MarkerOperations;
60
+ readonly introspectSchema: () => Promise<MongoSchemaIR>;
61
+ }
62
+
63
+ export interface MongoMigrationRunnerExecuteOptions {
64
+ readonly plan: MigrationPlan;
65
+ readonly destinationContract: MongoContract;
66
+ readonly policy: MigrationOperationPolicy;
67
+ readonly callbacks?: {
68
+ onOperationStart?(op: MigrationPlanOperation): void;
69
+ onOperationComplete?(op: MigrationPlanOperation): void;
70
+ };
71
+ readonly executionChecks?: MigrationRunnerExecutionChecks;
72
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
73
+ readonly strictVerification?: boolean;
74
+ readonly context?: OperationContext;
75
+ /**
76
+ * Invariant ids contributed by this apply (the migration's `providedInvariants`).
77
+ * The runner unions these into `marker.invariants` atomically with the marker write.
78
+ * Defaults to `[]` for marker-only flows.
79
+ */
80
+ readonly invariants?: readonly string[];
61
81
  }
62
82
 
63
83
  function runnerFailure(
@@ -75,17 +95,7 @@ function runnerFailure(
75
95
  export class MongoMigrationRunner {
76
96
  constructor(private readonly deps: MongoRunnerDependencies) {}
77
97
 
78
- async execute(options: {
79
- readonly plan: MigrationPlan;
80
- readonly destinationContract: unknown;
81
- readonly policy: MigrationOperationPolicy;
82
- readonly callbacks?: {
83
- onOperationStart?(op: MigrationPlanOperation): void;
84
- onOperationComplete?(op: MigrationPlanOperation): void;
85
- };
86
- readonly executionChecks?: MigrationRunnerExecutionChecks;
87
- readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', 'mongo'>>;
88
- }): Promise<MigrationRunnerResult> {
98
+ async execute(options: MongoMigrationRunnerExecuteOptions): Promise<MigrationRunnerResult> {
89
99
  const { commandExecutor, inspectionExecutor, adapter, driver, markerOps } = this.deps;
90
100
  const operations = deserializeMongoOps(options.plan.operations as readonly unknown[]);
91
101
 
@@ -176,22 +186,47 @@ export class MongoMigrationRunner {
176
186
  }
177
187
 
178
188
  const destination = options.plan.destination;
179
- const profileHash = hasProfileHash(options.destinationContract)
180
- ? options.destinationContract.profileHash
181
- : destination.storageHash;
182
-
183
- if (
184
- operationsExecuted === 0 &&
185
- existingMarker?.storageHash === destination.storageHash &&
186
- existingMarker.profileHash === profileHash
187
- ) {
189
+ const profileHash = options.destinationContract.profileHash ?? destination.storageHash;
190
+
191
+ // Sort + dedupe so the initMarker path writes a stable initial value.
192
+ // updateMarker merges server-side, so no client-side union is needed.
193
+ const incomingInvariants = Array.from(new Set(options.invariants ?? [])).sort();
194
+ const existingInvariantSet = new Set(existingMarker?.invariants ?? []);
195
+ const incomingIsSubsetOfExisting = incomingInvariants.every((id) =>
196
+ existingInvariantSet.has(id),
197
+ );
198
+ const markerAlreadyAtDestination =
199
+ existingMarker !== null &&
200
+ existingMarker.storageHash === destination.storageHash &&
201
+ existingMarker.profileHash === profileHash;
202
+
203
+ // Skip marker/ledger writes (and schema verification) only when the apply
204
+ // is a true no-op: no operations executed, marker already at destination,
205
+ // and every incoming invariant is already in the stored set.
206
+ if (operationsExecuted === 0 && markerAlreadyAtDestination && incomingIsSubsetOfExisting) {
188
207
  return ok({ operationsPlanned: operations.length, operationsExecuted });
189
208
  }
190
209
 
210
+ const liveSchema = await this.deps.introspectSchema();
211
+ const verifyResult = verifyMongoSchema({
212
+ contract: options.destinationContract,
213
+ schema: liveSchema,
214
+ strict: options.strictVerification ?? true,
215
+ frameworkComponents: options.frameworkComponents,
216
+ ...(options.context ? { context: options.context } : {}),
217
+ });
218
+ if (!verifyResult.ok) {
219
+ return runnerFailure('SCHEMA_VERIFY_FAILED', verifyResult.summary, {
220
+ why: 'The resulting database schema does not satisfy the destination contract.',
221
+ meta: { issues: verifyResult.schema.issues },
222
+ });
223
+ }
224
+
191
225
  if (existingMarker) {
192
226
  const updated = await markerOps.updateMarker(existingMarker.storageHash, {
193
227
  storageHash: destination.storageHash,
194
228
  profileHash,
229
+ invariants: incomingInvariants,
195
230
  });
196
231
  if (!updated) {
197
232
  return runnerFailure(
@@ -209,6 +244,7 @@ export class MongoMigrationRunner {
209
244
  await markerOps.initMarker({
210
245
  storageHash: destination.storageHash,
211
246
  profileHash,
247
+ invariants: incomingInvariants,
212
248
  });
213
249
  }
214
250
 
@@ -259,7 +295,7 @@ export class MongoMigrationRunner {
259
295
  }
260
296
 
261
297
  for (const plan of op.run) {
262
- const wireCommand = adapter.lower(plan);
298
+ const wireCommand = await adapter.lower(plan, {});
263
299
  for await (const _ of driver.execute(wireCommand)) {
264
300
  /* consume */
265
301
  }
@@ -307,7 +343,7 @@ export class MongoMigrationRunner {
307
343
  },
308
344
  );
309
345
  }
310
- const wireCommand = adapter.lower(check.source);
346
+ const wireCommand = await adapter.lower(check.source, {});
311
347
  let matchFound = false;
312
348
  for await (const row of driver.execute<Record<string, unknown>>(wireCommand)) {
313
349
  if (filterEvaluator.evaluate(check.filter, row)) {
@@ -375,13 +411,9 @@ export class MongoMigrationRunner {
375
411
  ): MigrationRunnerResult | undefined {
376
412
  const origin = plan.origin ?? null;
377
413
  if (!origin) {
378
- if (marker) {
379
- return runnerFailure(
380
- 'MARKER_ORIGIN_MISMATCH',
381
- 'Database already has a contract marker but the plan has no origin. This would silently overwrite the existing marker.',
382
- { meta: { markerStorageHash: marker.storageHash } },
383
- );
384
- }
414
+ // No origin assertion on the plan — the caller has done its own
415
+ // correctness check (typically `db update` via live-schema
416
+ // introspection) and does not rely on marker continuity.
385
417
  return undefined;
386
418
  }
387
419
 
@@ -47,7 +47,6 @@ export class PlannerProducedMongoMigration
47
47
  return renderCallsToTypeScript(this.calls, {
48
48
  from: this.meta.from,
49
49
  to: this.meta.to,
50
- ...ifDefined('kind', this.meta.kind),
51
50
  ...ifDefined('labels', this.meta.labels),
52
51
  });
53
52
  }
@@ -3,9 +3,8 @@ import { type ImportRequirement, jsonToTsSource, renderImports } from '@prisma-n
3
3
  import type { OpFactoryCall } from './op-factory-call';
4
4
 
5
5
  export interface RenderMigrationMeta {
6
- readonly from: string;
6
+ readonly from: string | null;
7
7
  readonly to: string;
8
- readonly kind?: string;
9
8
  readonly labels?: readonly string[];
10
9
  }
11
10
 
@@ -36,8 +35,8 @@ const BASE_IMPORTS: readonly ImportRequirement[] = [
36
35
  * `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and
37
36
  * implements the abstract `operations` and `describe` members. `meta` is
38
37
  * always rendered — `describe()` is part of the `Migration` contract, so
39
- * even an empty stub must satisfy it; callers pass empty strings for a
40
- * migration-new scaffold.
38
+ * even an empty stub must satisfy it; callers pass `from: null` for a
39
+ * baseline `migration-new` scaffold (and a real `to` hash either way).
41
40
  *
42
41
  * The walk is polymorphic: each call node contributes its own
43
42
  * `renderTypeScript()` expression and declares its own
@@ -89,9 +88,6 @@ function buildDescribeMethod(meta: RenderMigrationMeta): string {
89
88
  lines.push(' return {');
90
89
  lines.push(` from: ${JSON.stringify(meta.from)},`);
91
90
  lines.push(` to: ${JSON.stringify(meta.to)},`);
92
- if (meta.kind) {
93
- lines.push(` kind: ${JSON.stringify(meta.kind)},`);
94
- }
95
91
  if (meta.labels && meta.labels.length > 0) {
96
92
  lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
97
93
  }