@prisma-next/contract 0.11.0 → 0.12.0
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/canonicalization-DFE0HJkI.d.mts +69 -0
- package/dist/canonicalization-DFE0HJkI.d.mts.map +1 -0
- package/dist/canonicalization-path-match-b2jFuEso.mjs +25 -0
- package/dist/canonicalization-path-match-b2jFuEso.mjs.map +1 -0
- package/dist/{contract-types-Bt2uyqs3.d.mts → contract-types-xgwKtd7y.d.mts} +34 -74
- package/dist/contract-types-xgwKtd7y.d.mts.map +1 -0
- package/dist/contract-validation-error-ClZaKqMW.mjs +20 -0
- package/dist/contract-validation-error-ClZaKqMW.mjs.map +1 -0
- package/dist/contract-validation-error-T5LH4DW-.d.mts +13 -0
- package/dist/contract-validation-error-T5LH4DW-.d.mts.map +1 -0
- package/dist/contract-validation-error.d.mts +2 -10
- package/dist/contract-validation-error.mjs +2 -2
- package/dist/domain-envelope-4hyFtJ4_.d.mts +110 -0
- package/dist/domain-envelope-4hyFtJ4_.d.mts.map +1 -0
- package/dist/hashing-utils.d.mts +19 -0
- package/dist/hashing-utils.d.mts.map +1 -0
- package/dist/hashing-utils.mjs +50 -0
- package/dist/hashing-utils.mjs.map +1 -0
- package/dist/hashing.d.mts +8 -37
- package/dist/hashing.d.mts.map +1 -1
- package/dist/hashing.mjs +175 -1
- package/dist/hashing.mjs.map +1 -0
- package/dist/namespace-id-CVpkSFUK.mjs +9 -0
- package/dist/namespace-id-CVpkSFUK.mjs.map +1 -0
- package/dist/types.d.mts +4 -2
- package/dist/types.mjs +91 -2
- package/dist/types.mjs.map +1 -0
- package/dist/validate-domain.d.mts +6 -8
- package/dist/validate-domain.d.mts.map +1 -1
- package/dist/validate-domain.mjs +99 -56
- package/dist/validate-domain.mjs.map +1 -1
- package/package.json +16 -9
- package/src/canonicalization-path-match.ts +44 -0
- package/src/canonicalization-storage-sort.ts +88 -0
- package/src/canonicalization.ts +92 -161
- package/src/contract-types.ts +33 -7
- package/src/contract-validation-error.ts +7 -0
- package/src/cross-reference.ts +28 -0
- package/src/domain-envelope.ts +87 -0
- package/src/domain-types.ts +13 -15
- package/src/exports/contract-validation-error.ts +1 -0
- package/src/exports/hashing-utils.ts +12 -0
- package/src/exports/hashing.ts +2 -0
- package/src/exports/types.ts +24 -1
- package/src/hashing.ts +28 -11
- package/src/namespace-id.ts +10 -0
- package/src/types.ts +21 -0
- package/src/validate-domain.ts +162 -94
- package/dist/contract-types-Bt2uyqs3.d.mts.map +0 -1
- package/dist/contract-validation-error-Dp2vHZt5.mjs +0 -14
- package/dist/contract-validation-error-Dp2vHZt5.mjs.map +0 -1
- package/dist/contract-validation-error.d.mts.map +0 -1
- package/dist/hashing-rZiqFOlc.mjs +0 -204
- package/dist/hashing-rZiqFOlc.mjs.map +0 -1
- package/dist/testing.d.mts +0 -32
- package/dist/testing.d.mts.map +0 -1
- package/dist/testing.mjs +0 -63
- package/dist/testing.mjs.map +0 -1
- package/dist/types-CVGwkRLa.mjs +0 -46
- package/dist/types-CVGwkRLa.mjs.map +0 -1
- package/src/exports/testing.ts +0 -1
- package/src/testing-factories.ts +0 -115
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-domain.mjs","names":["f"],"sources":["../src/validate-domain.ts"],"sourcesContent":["import { ContractValidationError } from './contract-validation-error';\n\nexport interface DomainModelShape {\n readonly fields: Record<string, unknown>;\n readonly relations?: Record<string, { readonly to: string }>;\n readonly discriminator?: { readonly field: string };\n readonly variants?: Record<string, unknown>;\n readonly base?: string;\n readonly owner?: string;\n}\n\nexport interface DomainContractShape {\n readonly roots: Record<string, string>;\n readonly models: Record<string, DomainModelShape>;\n readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;\n}\n\nexport function validateContractDomain(contract: DomainContractShape): void {\n const errors: string[] = [];\n const modelNames = new Set(Object.keys(contract.models));\n\n validateRoots(contract, modelNames, errors);\n validateVariantsAndBases(contract, modelNames, errors);\n validateRelationTargets(contract, modelNames, errors);\n validateDiscriminators(contract, errors);\n validateOwnership(contract, modelNames, errors);\n validateValueObjectReferences(contract, errors);\n validateFieldModifiers(contract, errors);\n\n if (errors.length > 0) {\n throw new ContractValidationError(\n `Contract domain validation failed:\\n- ${errors.join('\\n- ')}`,\n 'domain',\n );\n }\n}\n\nfunction validateRoots(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const seenValues = new Set<string>();\n for (const [rootKey, modelName] of Object.entries(contract.roots)) {\n if (seenValues.has(modelName)) {\n errors.push(`Duplicate root value: \"${modelName}\" is mapped by multiple root keys`);\n }\n seenValues.add(modelName);\n\n if (!modelNames.has(modelName)) {\n errors.push(\n `Root \"${rootKey}\" references model \"${modelName}\" which does not exist in models`,\n );\n }\n }\n}\n\nfunction validateVariantsAndBases(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const models = new Map(Object.entries(contract.models));\n\n for (const [modelName, model] of models) {\n if (model.variants) {\n for (const variantName of Object.keys(model.variants)) {\n if (!modelNames.has(variantName)) {\n errors.push(\n `Model \"${modelName}\" lists variant \"${variantName}\" which does not exist in models`,\n );\n continue;\n }\n const variantModel = models.get(variantName);\n if (!variantModel) continue;\n if (variantModel.base !== modelName) {\n errors.push(\n `Variant \"${variantName}\" has base \"${variantModel.base ?? '(none)'}\" but expected \"${modelName}\"`,\n );\n }\n }\n }\n\n if (model.base) {\n if (!modelNames.has(model.base)) {\n errors.push(`Model \"${modelName}\" has base \"${model.base}\" which does not exist in models`);\n continue;\n }\n const baseModel = models.get(model.base);\n if (!baseModel) continue;\n if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) {\n errors.push(\n `Model \"${modelName}\" has base \"${model.base}\" which does not list it as a variant`,\n );\n }\n }\n }\n}\n\nfunction validateRelationTargets(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [relName, relation] of Object.entries(model.relations ?? {})) {\n if (!modelNames.has(relation.to)) {\n errors.push(\n `Relation \"${relName}\" on model \"${modelName}\" targets \"${relation.to}\" which does not exist in models`,\n );\n }\n }\n }\n}\n\nfunction validateDiscriminators(contract: DomainContractShape, errors: string[]): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (model.discriminator) {\n if (!model.variants || Object.keys(model.variants).length === 0) {\n errors.push(`Model \"${modelName}\" has discriminator but no variants`);\n }\n if (!Object.hasOwn(model.fields, model.discriminator.field)) {\n errors.push(\n `Discriminator field \"${model.discriminator.field}\" is not a field on model \"${modelName}\"`,\n );\n }\n }\n\n if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) {\n errors.push(`Model \"${modelName}\" has variants but no discriminator`);\n }\n\n if (model.base) {\n if (model.discriminator) {\n errors.push(`Model \"${modelName}\" has base and must not have discriminator`);\n }\n if (model.variants && Object.keys(model.variants).length > 0) {\n errors.push(`Model \"${modelName}\" has base and must not have variants`);\n }\n }\n }\n}\n\nfunction validateOwnership(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (!model.owner) continue;\n\n if (model.owner === modelName) {\n errors.push(`Model \"${modelName}\" cannot own itself`);\n }\n\n if (!modelNames.has(model.owner)) {\n errors.push(`Model \"${modelName}\" has owner \"${model.owner}\" which does not exist in models`);\n }\n\n for (const [rootKey, rootModel] of Object.entries(contract.roots)) {\n if (rootModel === modelName) {\n errors.push(\n `Owned model \"${modelName}\" must not appear in roots (found as root \"${rootKey}\")`,\n );\n }\n }\n }\n}\n\ninterface FieldTypeLike {\n readonly kind?: string;\n readonly name?: string;\n readonly members?: readonly FieldTypeLike[];\n}\n\ninterface FieldLike {\n readonly type?: FieldTypeLike;\n readonly many?: boolean;\n readonly dict?: boolean;\n}\n\nfunction forEachContractField(\n contract: DomainContractShape,\n callback: (field: unknown, location: string) => void,\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [fieldName, field] of Object.entries(model.fields)) {\n callback(field, `Model \"${modelName}\" field \"${fieldName}\"`);\n }\n }\n for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {\n for (const [fieldName, field] of Object.entries(vo.fields)) {\n callback(field, `Value object \"${voName}\" field \"${fieldName}\"`);\n }\n }\n}\n\nfunction validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {\n const voNames = new Set(Object.keys(contract.valueObjects ?? {}));\n\n function checkType(type: FieldTypeLike | undefined, location: string): void {\n if (!type) return;\n if (type.kind === 'valueObject' && type.name && !voNames.has(type.name)) {\n errors.push(\n `${location} references value object \"${type.name}\" which does not exist in valueObjects`,\n );\n return;\n }\n if (type.kind === 'union') {\n for (const member of type.members ?? []) checkType(member, location);\n }\n }\n\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n checkType(f?.type, location);\n });\n}\n\nfunction validateFieldModifiers(contract: DomainContractShape, errors: string[]): void {\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n if (f?.many && f?.dict) {\n errors.push(`${location} cannot have both \"many\" and \"dict\" modifiers`);\n }\n });\n}\n"],"mappings":";;AAiBA,SAAgB,uBAAuB,UAAqC;CAC1E,MAAM,SAAmB,EAAE;CAC3B,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,SAAS,OAAO,CAAC;CAExD,cAAc,UAAU,YAAY,OAAO;CAC3C,yBAAyB,UAAU,YAAY,OAAO;CACtD,wBAAwB,UAAU,YAAY,OAAO;CACrD,uBAAuB,UAAU,OAAO;CACxC,kBAAkB,UAAU,YAAY,OAAO;CAC/C,8BAA8B,UAAU,OAAO;CAC/C,uBAAuB,UAAU,OAAO;CAExC,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,wBACR,yCAAyC,OAAO,KAAK,OAAO,IAC5D,SACD;;AAIL,SAAS,cACP,UACA,YACA,QACM;CACN,MAAM,6BAAa,IAAI,KAAa;CACpC,KAAK,MAAM,CAAC,SAAS,cAAc,OAAO,QAAQ,SAAS,MAAM,EAAE;EACjE,IAAI,WAAW,IAAI,UAAU,EAC3B,OAAO,KAAK,0BAA0B,UAAU,mCAAmC;EAErF,WAAW,IAAI,UAAU;EAEzB,IAAI,CAAC,WAAW,IAAI,UAAU,EAC5B,OAAO,KACL,SAAS,QAAQ,sBAAsB,UAAU,kCAClD;;;AAKP,SAAS,yBACP,UACA,YACA,QACM;CACN,MAAM,SAAS,IAAI,IAAI,OAAO,QAAQ,SAAS,OAAO,CAAC;CAEvD,KAAK,MAAM,CAAC,WAAW,UAAU,QAAQ;EACvC,IAAI,MAAM,UACR,KAAK,MAAM,eAAe,OAAO,KAAK,MAAM,SAAS,EAAE;GACrD,IAAI,CAAC,WAAW,IAAI,YAAY,EAAE;IAChC,OAAO,KACL,UAAU,UAAU,mBAAmB,YAAY,kCACpD;IACD;;GAEF,MAAM,eAAe,OAAO,IAAI,YAAY;GAC5C,IAAI,CAAC,cAAc;GACnB,IAAI,aAAa,SAAS,WACxB,OAAO,KACL,YAAY,YAAY,cAAc,aAAa,QAAQ,SAAS,kBAAkB,UAAU,GACjG;;EAKP,IAAI,MAAM,MAAM;GACd,IAAI,CAAC,WAAW,IAAI,MAAM,KAAK,EAAE;IAC/B,OAAO,KAAK,UAAU,UAAU,cAAc,MAAM,KAAK,kCAAkC;IAC3F;;GAEF,MAAM,YAAY,OAAO,IAAI,MAAM,KAAK;GACxC,IAAI,CAAC,WAAW;GAChB,IAAI,CAAC,UAAU,YAAY,CAAC,OAAO,OAAO,UAAU,UAAU,UAAU,EACtE,OAAO,KACL,UAAU,UAAU,cAAc,MAAM,KAAK,uCAC9C;;;;AAMT,SAAS,wBACP,UACA,YACA,QACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAC9D,KAAK,MAAM,CAAC,SAAS,aAAa,OAAO,QAAQ,MAAM,aAAa,EAAE,CAAC,EACrE,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,EAC9B,OAAO,KACL,aAAa,QAAQ,cAAc,UAAU,aAAa,SAAS,GAAG,kCACvE;;AAMT,SAAS,uBAAuB,UAA+B,QAAwB;CACrF,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAChE,IAAI,MAAM,eAAe;GACvB,IAAI,CAAC,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,WAAW,GAC5D,OAAO,KAAK,UAAU,UAAU,qCAAqC;GAEvE,IAAI,CAAC,OAAO,OAAO,MAAM,QAAQ,MAAM,cAAc,MAAM,EACzD,OAAO,KACL,wBAAwB,MAAM,cAAc,MAAM,6BAA6B,UAAU,GAC1F;;EAIL,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,SAAS,KAAK,CAAC,MAAM,eACrE,OAAO,KAAK,UAAU,UAAU,qCAAqC;EAGvE,IAAI,MAAM,MAAM;GACd,IAAI,MAAM,eACR,OAAO,KAAK,UAAU,UAAU,4CAA4C;GAE9E,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,SAAS,GACzD,OAAO,KAAK,UAAU,UAAU,uCAAuC;;;;AAM/E,SAAS,kBACP,UACA,YACA,QACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAChE,IAAI,CAAC,MAAM,OAAO;EAElB,IAAI,MAAM,UAAU,WAClB,OAAO,KAAK,UAAU,UAAU,qBAAqB;EAGvD,IAAI,CAAC,WAAW,IAAI,MAAM,MAAM,EAC9B,OAAO,KAAK,UAAU,UAAU,eAAe,MAAM,MAAM,kCAAkC;EAG/F,KAAK,MAAM,CAAC,SAAS,cAAc,OAAO,QAAQ,SAAS,MAAM,EAC/D,IAAI,cAAc,WAChB,OAAO,KACL,gBAAgB,UAAU,6CAA6C,QAAQ,IAChF;;;AAkBT,SAAS,qBACP,UACA,UACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAC9D,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,MAAM,OAAO,EAC3D,SAAS,OAAO,UAAU,UAAU,WAAW,UAAU,GAAG;CAGhE,KAAK,MAAM,CAAC,QAAQ,OAAO,OAAO,QAAQ,SAAS,gBAAgB,EAAE,CAAC,EACpE,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,GAAG,OAAO,EACxD,SAAS,OAAO,iBAAiB,OAAO,WAAW,UAAU,GAAG;;AAKtE,SAAS,8BAA8B,UAA+B,QAAwB;CAC5F,MAAM,UAAU,IAAI,IAAI,OAAO,KAAK,SAAS,gBAAgB,EAAE,CAAC,CAAC;CAEjE,SAAS,UAAU,MAAiC,UAAwB;EAC1E,IAAI,CAAC,MAAM;EACX,IAAI,KAAK,SAAS,iBAAiB,KAAK,QAAQ,CAAC,QAAQ,IAAI,KAAK,KAAK,EAAE;GACvE,OAAO,KACL,GAAG,SAAS,4BAA4B,KAAK,KAAK,wCACnD;GACD;;EAEF,IAAI,KAAK,SAAS,SAChB,KAAK,MAAM,UAAU,KAAK,WAAW,EAAE,EAAE,UAAU,QAAQ,SAAS;;CAIxE,qBAAqB,WAAW,OAAO,aAAa;EAElD,UAAUA,OAAG,MAAM,SAAS;GAC5B;;AAGJ,SAAS,uBAAuB,UAA+B,QAAwB;CACrF,qBAAqB,WAAW,OAAO,aAAa;EAClD,MAAM,IAAI;EACV,IAAI,GAAG,QAAQ,GAAG,MAChB,OAAO,KAAK,GAAG,SAAS,+CAA+C;GAEzE"}
|
|
1
|
+
{"version":3,"file":"validate-domain.mjs","names":["f"],"sources":["../src/validate-domain.ts"],"sourcesContent":["import { ContractValidationError } from './contract-validation-error';\nimport type { CrossReference } from './cross-reference';\nimport type { ContractWithDomain } from './domain-envelope';\nimport { asNamespaceId, type NamespaceId } from './namespace-id';\n\nexport interface DomainModelShape {\n readonly fields: Record<string, unknown>;\n readonly relations?: Record<string, { readonly to: CrossReference }>;\n readonly discriminator?: { readonly field: string };\n readonly variants?: Record<string, unknown>;\n readonly base?: CrossReference;\n readonly owner?: string;\n}\n\nexport interface DomainContractShape extends ContractWithDomain {\n readonly roots: Record<string, CrossReference>;\n}\n\ninterface IndexedModel {\n readonly namespaceId: NamespaceId;\n readonly name: string;\n readonly model: DomainModelShape;\n}\n\ntype ModelIndex = Map<NamespaceId, Map<string, IndexedModel>>;\n\nfunction indexDomainModels(contract: DomainContractShape): ModelIndex {\n const index: ModelIndex = new Map();\n for (const [namespaceKey, namespace] of Object.entries(contract.domain.namespaces)) {\n const namespaceId = asNamespaceId(namespaceKey);\n let modelsInNamespace = index.get(namespaceId);\n if (modelsInNamespace === undefined) {\n modelsInNamespace = new Map();\n index.set(namespaceId, modelsInNamespace);\n }\n for (const [name, model] of Object.entries(namespace.models)) {\n modelsInNamespace.set(name, { namespaceId, name, model });\n }\n }\n return index;\n}\n\nfunction lookupModel(index: ModelIndex, ref: CrossReference): IndexedModel | undefined {\n return index.get(ref.namespace)?.get(ref.model);\n}\n\nfunction* iterateIndexedModels(index: ModelIndex): IterableIterator<IndexedModel> {\n for (const modelsInNamespace of index.values()) {\n for (const entry of modelsInNamespace.values()) {\n yield entry;\n }\n }\n}\n\nexport function validateContractDomain(contract: DomainContractShape): void {\n const errors: string[] = [];\n const modelIndex = indexDomainModels(contract);\n\n validateRoots(contract, modelIndex, errors);\n validateVariantsAndBases(modelIndex, errors);\n validateRelationTargets(modelIndex, errors);\n validateDiscriminators(modelIndex, errors);\n validateOwnership(contract, modelIndex, errors);\n validateValueObjectReferences(contract, errors);\n validateFieldModifiers(modelIndex, contract, errors);\n\n if (errors.length > 0) {\n throw new ContractValidationError(\n `Contract domain validation failed:\\n- ${errors.join('\\n- ')}`,\n 'domain',\n );\n }\n}\n\nfunction validateRoots(\n contract: DomainContractShape,\n modelIndex: ModelIndex,\n errors: string[],\n): void {\n const seenRootTargets = new Map<NamespaceId, Set<string>>();\n for (const [rootKey, crossRef] of Object.entries(contract.roots)) {\n let modelsInNamespace = seenRootTargets.get(crossRef.namespace);\n if (modelsInNamespace === undefined) {\n modelsInNamespace = new Set();\n seenRootTargets.set(crossRef.namespace, modelsInNamespace);\n }\n if (modelsInNamespace.has(crossRef.model)) {\n errors.push(\n `Duplicate root value: \"${crossRef.namespace}:${crossRef.model}\" is mapped by multiple root keys`,\n );\n }\n modelsInNamespace.add(crossRef.model);\n\n if (!lookupModel(modelIndex, crossRef)) {\n errors.push(\n `Root \"${rootKey}\" references model \"${crossRef.namespace}:${crossRef.model}\" which does not exist in domain.namespaces`,\n );\n }\n }\n}\n\nfunction validateVariantsAndBases(modelIndex: ModelIndex, errors: string[]): void {\n for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {\n if (model.variants) {\n for (const variantName of Object.keys(model.variants)) {\n const variantRef: CrossReference = { namespace: namespaceId, model: variantName };\n const variantEntry = lookupModel(modelIndex, variantRef);\n if (!variantEntry) {\n errors.push(\n `Model \"${namespaceId}:${modelName}\" lists variant \"${variantName}\" which does not exist at that namespace coordinate`,\n );\n continue;\n }\n const variantBase = variantEntry.model.base;\n if (variantBase?.namespace !== namespaceId || variantBase?.model !== modelName) {\n errors.push(\n `Variant \"${namespaceId}:${variantName}\" has base \"${variantBase?.namespace ?? '?'}:${variantBase?.model ?? '(none)'}\" but expected \"${namespaceId}:${modelName}\"`,\n );\n }\n }\n }\n\n if (model.base) {\n const baseEntry = lookupModel(modelIndex, model.base);\n if (!baseEntry) {\n errors.push(\n `Model \"${namespaceId}:${modelName}\" has base \"${model.base.namespace}:${model.base.model}\" which does not exist in domain.namespaces`,\n );\n continue;\n }\n if (!baseEntry.model.variants || !Object.hasOwn(baseEntry.model.variants, modelName)) {\n errors.push(\n `Model \"${namespaceId}:${modelName}\" has base \"${model.base.namespace}:${model.base.model}\" which does not list it as a variant`,\n );\n }\n }\n }\n}\n\nfunction validateRelationTargets(modelIndex: ModelIndex, errors: string[]): void {\n for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {\n for (const [relName, relation] of Object.entries(model.relations ?? {})) {\n if (!lookupModel(modelIndex, relation.to)) {\n errors.push(\n `Relation \"${relName}\" on model \"${namespaceId}:${modelName}\" targets \"${relation.to.namespace}:${relation.to.model}\" which does not exist in domain.namespaces`,\n );\n }\n }\n }\n}\n\nfunction validateDiscriminators(modelIndex: ModelIndex, errors: string[]): void {\n for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {\n if (model.discriminator) {\n if (!model.variants || Object.keys(model.variants).length === 0) {\n errors.push(`Model \"${namespaceId}:${modelName}\" has discriminator but no variants`);\n }\n if (!Object.hasOwn(model.fields, model.discriminator.field)) {\n errors.push(\n `Discriminator field \"${model.discriminator.field}\" is not a field on model \"${namespaceId}:${modelName}\"`,\n );\n }\n }\n\n if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) {\n errors.push(`Model \"${namespaceId}:${modelName}\" has variants but no discriminator`);\n }\n\n if (model.base) {\n if (model.discriminator) {\n errors.push(`Model \"${namespaceId}:${modelName}\" has base and must not have discriminator`);\n }\n if (model.variants && Object.keys(model.variants).length > 0) {\n errors.push(`Model \"${namespaceId}:${modelName}\" has base and must not have variants`);\n }\n }\n }\n}\n\nfunction validateOwnership(\n contract: DomainContractShape,\n modelIndex: ModelIndex,\n errors: string[],\n): void {\n for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {\n if (!model.owner) continue;\n\n if (model.owner === modelName) {\n errors.push(`Model \"${namespaceId}:${modelName}\" cannot own itself`);\n }\n\n const ownerRef: CrossReference = { namespace: namespaceId, model: model.owner };\n if (!lookupModel(modelIndex, ownerRef)) {\n errors.push(\n `Model \"${namespaceId}:${modelName}\" has owner \"${namespaceId}:${model.owner}\" which does not exist in domain.namespaces`,\n );\n }\n\n for (const [rootKey, rootRef] of Object.entries(contract.roots)) {\n if (rootRef.namespace === namespaceId && rootRef.model === modelName) {\n errors.push(\n `Owned model \"${namespaceId}:${modelName}\" must not appear in roots (found as root \"${rootKey}\")`,\n );\n }\n }\n }\n}\n\ninterface FieldTypeLike {\n readonly kind?: string;\n readonly name?: string;\n readonly members?: readonly FieldTypeLike[];\n}\n\ninterface FieldLike {\n readonly type?: FieldTypeLike;\n readonly many?: boolean;\n readonly dict?: boolean;\n}\n\nfunction validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {\n const voNamesByNamespace = new Map<NamespaceId, Set<string>>();\n for (const [namespaceKey, namespace] of Object.entries(contract.domain.namespaces)) {\n const namespaceId = asNamespaceId(namespaceKey);\n voNamesByNamespace.set(namespaceId, new Set(Object.keys(namespace.valueObjects ?? {})));\n }\n\n function checkType(\n type: FieldTypeLike | undefined,\n location: string,\n namespaceId: NamespaceId,\n ): void {\n if (!type) return;\n const voNames = voNamesByNamespace.get(namespaceId) ?? new Set<string>();\n if (type.kind === 'valueObject' && type.name && !voNames.has(type.name)) {\n errors.push(\n `${location} references value object \"${namespaceId}:${type.name}\" which does not exist in that namespace's valueObjects`,\n );\n return;\n }\n if (type.kind === 'union') {\n for (const member of type.members ?? []) checkType(member, location, namespaceId);\n }\n }\n\n for (const [namespaceKey, namespace] of Object.entries(contract.domain.namespaces)) {\n const namespaceId = asNamespaceId(namespaceKey);\n for (const [modelName, model] of Object.entries(namespace.models)) {\n for (const [fieldName, field] of Object.entries(model.fields)) {\n const f = field as FieldLike | undefined;\n checkType(f?.type, `Model \"${namespaceId}:${modelName}\" field \"${fieldName}\"`, namespaceId);\n }\n }\n for (const [voName, vo] of Object.entries(namespace.valueObjects ?? {})) {\n for (const [fieldName, field] of Object.entries(vo.fields)) {\n const f = field as FieldLike | undefined;\n checkType(\n f?.type,\n `Value object \"${namespaceId}:${voName}\" field \"${fieldName}\"`,\n namespaceId,\n );\n }\n }\n }\n}\n\nfunction validateFieldModifiers(\n modelIndex: ModelIndex,\n contract: DomainContractShape,\n errors: string[],\n): void {\n for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {\n for (const [fieldName, field] of Object.entries(model.fields)) {\n const f = field as FieldLike | undefined;\n if (f?.many && f?.dict) {\n errors.push(\n `Model \"${namespaceId}:${modelName}\" field \"${fieldName}\" cannot have both \"many\" and \"dict\" modifiers`,\n );\n }\n }\n }\n for (const [namespaceKey, namespace] of Object.entries(contract.domain.namespaces)) {\n const namespaceId = asNamespaceId(namespaceKey);\n for (const [voName, vo] of Object.entries(namespace.valueObjects ?? {})) {\n for (const [fieldName, field] of Object.entries(vo.fields)) {\n const f = field as FieldLike | undefined;\n if (f?.many && f?.dict) {\n errors.push(\n `Value object \"${namespaceId}:${voName}\" field \"${fieldName}\" cannot have both \"many\" and \"dict\" modifiers`,\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Data contract type definitions and JSON schema for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/utils": "0.12.0",
|
|
10
10
|
"@standard-schema/spec": "^1.1.0",
|
|
11
11
|
"arktype": "^2.2.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@prisma-next/
|
|
15
|
-
"@prisma-next/
|
|
16
|
-
"@prisma-next/tsdown": "0.11.0",
|
|
14
|
+
"@prisma-next/tsconfig": "0.12.0",
|
|
15
|
+
"@prisma-next/tsdown": "0.12.0",
|
|
17
16
|
"tsdown": "0.22.0",
|
|
18
17
|
"typescript": "5.9.3",
|
|
19
18
|
"vitest": "4.1.6"
|
|
20
19
|
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"typescript": ">=5.9"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"typescript": {
|
|
25
|
+
"optional": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
21
28
|
"files": [
|
|
22
29
|
"dist",
|
|
23
30
|
"src",
|
|
24
31
|
"schemas"
|
|
25
32
|
],
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=20"
|
|
28
|
-
},
|
|
29
33
|
"exports": {
|
|
30
34
|
"./contract-validation-error": "./dist/contract-validation-error.mjs",
|
|
31
35
|
"./hashing": "./dist/hashing.mjs",
|
|
32
|
-
"./
|
|
36
|
+
"./hashing-utils": "./dist/hashing-utils.mjs",
|
|
33
37
|
"./types": "./dist/types.mjs",
|
|
34
38
|
"./validate-domain": "./dist/validate-domain.mjs",
|
|
35
39
|
"./package.json": "./package.json"
|
|
36
40
|
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24"
|
|
43
|
+
},
|
|
37
44
|
"repository": {
|
|
38
45
|
"type": "git",
|
|
39
46
|
"url": "https://github.com/prisma/prisma-next.git",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PreserveEmptyPredicate } from './canonicalization';
|
|
2
|
+
|
|
3
|
+
export type PathSegment = string | '*' | readonly string[];
|
|
4
|
+
|
|
5
|
+
export type PathPattern = readonly PathSegment[];
|
|
6
|
+
|
|
7
|
+
export function matchesPathPattern(path: readonly string[], pattern: PathPattern): boolean {
|
|
8
|
+
if (path.length !== pattern.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
13
|
+
const segment = pattern[i];
|
|
14
|
+
const value = path[i];
|
|
15
|
+
if (segment === undefined || value === undefined) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (segment === '*') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof segment === 'string') {
|
|
24
|
+
if (value !== segment) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(segment)) {
|
|
31
|
+
if (!segment.includes(value)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createPreserveEmptyPredicate(
|
|
41
|
+
patterns: readonly PathPattern[],
|
|
42
|
+
): PreserveEmptyPredicate {
|
|
43
|
+
return (path) => patterns.some((pattern) => matchesPathPattern(path, pattern));
|
|
44
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { StorageSort } from './canonicalization';
|
|
2
|
+
|
|
3
|
+
export type PathSegment = string | '*';
|
|
4
|
+
|
|
5
|
+
export interface NamedArraySortTarget {
|
|
6
|
+
readonly path: readonly PathSegment[];
|
|
7
|
+
readonly arrayKeys: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function compareByNameProperty(a: unknown, b: unknown): number {
|
|
15
|
+
const nameA = isPlainRecord(a) && typeof a['name'] === 'string' ? a['name'] : '';
|
|
16
|
+
const nameB = isPlainRecord(b) && typeof b['name'] === 'string' ? b['name'] : '';
|
|
17
|
+
return nameA.localeCompare(nameB);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sortArrayKeysOnRecord(
|
|
21
|
+
record: Record<string, unknown>,
|
|
22
|
+
arrayKeys: readonly string[],
|
|
23
|
+
compare: (a: unknown, b: unknown) => number,
|
|
24
|
+
): Record<string, unknown> {
|
|
25
|
+
const sorted: Record<string, unknown> = { ...record };
|
|
26
|
+
for (const key of arrayKeys) {
|
|
27
|
+
const value = record[key];
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
sorted[key] = [...value].sort(compare);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return sorted;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walkAndSort(
|
|
36
|
+
node: unknown,
|
|
37
|
+
pathSegments: readonly PathSegment[],
|
|
38
|
+
arrayKeys: readonly string[],
|
|
39
|
+
compare: (a: unknown, b: unknown) => number,
|
|
40
|
+
): unknown {
|
|
41
|
+
if (pathSegments.length === 0) {
|
|
42
|
+
if (!isPlainRecord(node)) {
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
return sortArrayKeysOnRecord(node, arrayKeys, compare);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!isPlainRecord(node)) {
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [head, ...rest] = pathSegments;
|
|
53
|
+
if (head === undefined) {
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (head === '*') {
|
|
58
|
+
const sorted: Record<string, unknown> = { ...node };
|
|
59
|
+
for (const key of Object.keys(node)) {
|
|
60
|
+
sorted[key] = walkAndSort(node[key], rest, arrayKeys, compare);
|
|
61
|
+
}
|
|
62
|
+
return sorted;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const child = node[head];
|
|
66
|
+
if (child === undefined) {
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { ...node, [head]: walkAndSort(child, rest, arrayKeys, compare) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createStorageSort(
|
|
74
|
+
targets: readonly NamedArraySortTarget[],
|
|
75
|
+
compare: (a: unknown, b: unknown) => number = compareByNameProperty,
|
|
76
|
+
): StorageSort {
|
|
77
|
+
return (storage) => {
|
|
78
|
+
if (!isPlainRecord(storage)) {
|
|
79
|
+
return storage;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let result: unknown = storage;
|
|
83
|
+
for (const target of targets) {
|
|
84
|
+
result = walkAndSort(result, target.path, target.arrayKeys, compare);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/canonicalization.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isArrayEqual } from '@prisma-next/utils/array-equal';
|
|
2
2
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
3
|
import type { JsonObject } from '@prisma-next/utils/json';
|
|
4
|
-
|
|
4
|
+
import { matchesPathPattern, type PathPattern } from './canonicalization-path-match';
|
|
5
5
|
import type { Contract } from './contract-types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -14,6 +14,49 @@ import type { Contract } from './contract-types';
|
|
|
14
14
|
*/
|
|
15
15
|
export type SerializeContract = (contract: Contract) => JsonObject;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Family-contributed predicate for the default-omission walk. Called when
|
|
19
|
+
* a value at `path` is a default (empty object/array or `false`); if this
|
|
20
|
+
* returns `true` the value is kept rather than stripped.
|
|
21
|
+
*
|
|
22
|
+
* The framework only calls the predicate inside the `isDefaultValue` branch,
|
|
23
|
+
* so there is no need to guard against non-default values.
|
|
24
|
+
*/
|
|
25
|
+
export type PreserveEmptyPredicate = (path: readonly string[]) => boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Family-contributed storage sort. Applied to the serialized `storage`
|
|
29
|
+
* subtree after the default-omission walk; the result replaces the
|
|
30
|
+
* `storage` field before the final key sort. Use to establish a
|
|
31
|
+
* deterministic order for storage arrays (indexes, uniques) that the
|
|
32
|
+
* family-agnostic `sortObjectKeys` pass cannot handle.
|
|
33
|
+
*/
|
|
34
|
+
export type StorageSort = (storage: unknown) => unknown;
|
|
35
|
+
|
|
36
|
+
const DOMAIN_NAMESPACE_SLOT_PATTERN = ['domain', 'namespaces', '*'] as const satisfies PathPattern;
|
|
37
|
+
const DOMAIN_MODELS_CONTAINER_PATTERN = [
|
|
38
|
+
'domain',
|
|
39
|
+
'namespaces',
|
|
40
|
+
'*',
|
|
41
|
+
'models',
|
|
42
|
+
] as const satisfies PathPattern;
|
|
43
|
+
const DOMAIN_MODEL_RELATIONS_PATTERN = [
|
|
44
|
+
'domain',
|
|
45
|
+
'namespaces',
|
|
46
|
+
'*',
|
|
47
|
+
'models',
|
|
48
|
+
'*',
|
|
49
|
+
'relations',
|
|
50
|
+
] as const satisfies PathPattern;
|
|
51
|
+
const DOMAIN_MODEL_STORAGE_PATTERN = [
|
|
52
|
+
'domain',
|
|
53
|
+
'namespaces',
|
|
54
|
+
'*',
|
|
55
|
+
'models',
|
|
56
|
+
'*',
|
|
57
|
+
'storage',
|
|
58
|
+
] as const satisfies PathPattern;
|
|
59
|
+
|
|
17
60
|
const TOP_LEVEL_ORDER = [
|
|
18
61
|
'schemaVersion',
|
|
19
62
|
'canonicalVersion',
|
|
@@ -21,8 +64,6 @@ const TOP_LEVEL_ORDER = [
|
|
|
21
64
|
'target',
|
|
22
65
|
'profileHash',
|
|
23
66
|
'roots',
|
|
24
|
-
'models',
|
|
25
|
-
'valueObjects',
|
|
26
67
|
'domain',
|
|
27
68
|
'storage',
|
|
28
69
|
'execution',
|
|
@@ -42,13 +83,17 @@ function isDefaultValue(value: unknown): boolean {
|
|
|
42
83
|
return false;
|
|
43
84
|
}
|
|
44
85
|
|
|
45
|
-
function omitDefaults(
|
|
86
|
+
function omitDefaults(
|
|
87
|
+
obj: unknown,
|
|
88
|
+
path: readonly string[],
|
|
89
|
+
shouldPreserveEmpty: PreserveEmptyPredicate | undefined,
|
|
90
|
+
): unknown {
|
|
46
91
|
if (obj === null || typeof obj !== 'object') {
|
|
47
92
|
return obj;
|
|
48
93
|
}
|
|
49
94
|
|
|
50
95
|
if (Array.isArray(obj)) {
|
|
51
|
-
return obj.map((item) => omitDefaults(item, path));
|
|
96
|
+
return obj.map((item) => omitDefaults(item, path, shouldPreserveEmpty));
|
|
52
97
|
}
|
|
53
98
|
|
|
54
99
|
const result: Record<string, unknown> = {};
|
|
@@ -69,25 +114,16 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
69
114
|
}
|
|
70
115
|
|
|
71
116
|
if (isDefaultValue(value)) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
117
|
+
const isRequiredDomainNamespaces = isArrayEqual(currentPath, ['domain', 'namespaces']);
|
|
118
|
+
const isDomainNamespaceSlot = matchesPathPattern(currentPath, DOMAIN_NAMESPACE_SLOT_PATTERN);
|
|
119
|
+
const isRequiredDomainModels = matchesPathPattern(
|
|
120
|
+
currentPath,
|
|
121
|
+
DOMAIN_MODELS_CONTAINER_PATTERN,
|
|
122
|
+
);
|
|
123
|
+
const isRequiredStorageNamespaces = isArrayEqual(currentPath, ['storage', 'namespaces']);
|
|
124
|
+
const isStorageNamespaceSlot =
|
|
75
125
|
currentPath.length === 3 &&
|
|
76
126
|
isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'namespaces']);
|
|
77
|
-
const isRequiredNamespaceTables =
|
|
78
|
-
currentPath.length === 4 &&
|
|
79
|
-
currentPath[0] === 'storage' &&
|
|
80
|
-
currentPath[1] === 'namespaces' &&
|
|
81
|
-
currentPath[3] === 'tables';
|
|
82
|
-
// Preserve per-table payloads even when empty. SQL tables are never
|
|
83
|
-
// emitted empty; Mongo collections legitimately are (a declared
|
|
84
|
-
// collection with no schema is a valid representation), and the
|
|
85
|
-
// family-agnostic canonicalizer must not strip them.
|
|
86
|
-
const isNamespaceTableEntry =
|
|
87
|
-
currentPath.length === 5 &&
|
|
88
|
-
currentPath[0] === 'storage' &&
|
|
89
|
-
currentPath[1] === 'namespaces' &&
|
|
90
|
-
currentPath[3] === 'tables';
|
|
91
127
|
const isRequiredRoots = isArrayEqual(currentPath, ['roots']);
|
|
92
128
|
const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);
|
|
93
129
|
const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);
|
|
@@ -98,56 +134,19 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
98
134
|
'defaults',
|
|
99
135
|
]);
|
|
100
136
|
const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';
|
|
101
|
-
const isModelRelations =
|
|
102
|
-
|
|
103
|
-
isArrayEqual([currentPath[0], currentPath[2]], ['models', 'relations']);
|
|
104
|
-
const isModelStorage =
|
|
105
|
-
currentPath.length === 3 &&
|
|
106
|
-
isArrayEqual([currentPath[0], currentPath[2]], ['models', 'storage']);
|
|
107
|
-
const isNamespaceTableUniques =
|
|
108
|
-
currentPath.length === 6 &&
|
|
109
|
-
currentPath[0] === 'storage' &&
|
|
110
|
-
currentPath[1] === 'namespaces' &&
|
|
111
|
-
currentPath[3] === 'tables' &&
|
|
112
|
-
currentPath[5] === 'uniques';
|
|
113
|
-
const isNamespaceTableIndexes =
|
|
114
|
-
currentPath.length === 6 &&
|
|
115
|
-
currentPath[0] === 'storage' &&
|
|
116
|
-
currentPath[1] === 'namespaces' &&
|
|
117
|
-
currentPath[3] === 'tables' &&
|
|
118
|
-
currentPath[5] === 'indexes';
|
|
119
|
-
const isNamespaceTableForeignKeys =
|
|
120
|
-
currentPath.length === 6 &&
|
|
121
|
-
currentPath[0] === 'storage' &&
|
|
122
|
-
currentPath[1] === 'namespaces' &&
|
|
123
|
-
currentPath[3] === 'tables' &&
|
|
124
|
-
currentPath[5] === 'foreignKeys';
|
|
125
|
-
|
|
126
|
-
// `storage.types.<name>.typeParams` is part of the StorageTypeInstance
|
|
127
|
-
// shape (validators require it). Preserve it even when empty so the
|
|
128
|
-
// emitted contract.json remains structurally valid after a round-trip.
|
|
129
|
-
const isStorageTypeTypeParams =
|
|
130
|
-
currentPath.length === 4 &&
|
|
131
|
-
currentPath[0] === 'storage' &&
|
|
132
|
-
currentPath[1] === 'types' &&
|
|
133
|
-
key === 'typeParams';
|
|
134
|
-
|
|
135
|
-
const isFkBooleanField =
|
|
136
|
-
currentPath.length === 7 &&
|
|
137
|
-
currentPath[0] === 'storage' &&
|
|
138
|
-
currentPath[1] === 'namespaces' &&
|
|
139
|
-
currentPath[3] === 'tables' &&
|
|
140
|
-
currentPath[5] === 'foreignKeys' &&
|
|
141
|
-
(key === 'constraint' || key === 'index');
|
|
137
|
+
const isModelRelations = matchesPathPattern(currentPath, DOMAIN_MODEL_RELATIONS_PATTERN);
|
|
138
|
+
const isModelStorage = matchesPathPattern(currentPath, DOMAIN_MODEL_STORAGE_PATTERN);
|
|
142
139
|
|
|
143
140
|
const isNullableField = key === 'nullable';
|
|
144
141
|
|
|
142
|
+
const isFamilyPreserved = shouldPreserveEmpty?.(currentPath) ?? false;
|
|
143
|
+
|
|
145
144
|
if (
|
|
146
|
-
!
|
|
147
|
-
!
|
|
148
|
-
!
|
|
149
|
-
!
|
|
150
|
-
!
|
|
145
|
+
!isRequiredDomainNamespaces &&
|
|
146
|
+
!isDomainNamespaceSlot &&
|
|
147
|
+
!isRequiredDomainModels &&
|
|
148
|
+
!isRequiredStorageNamespaces &&
|
|
149
|
+
!isStorageNamespaceSlot &&
|
|
151
150
|
!isRequiredRoots &&
|
|
152
151
|
!isRequiredExtensionPacks &&
|
|
153
152
|
!isRequiredCapabilities &&
|
|
@@ -156,18 +155,14 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
156
155
|
!isExtensionNamespace &&
|
|
157
156
|
!isModelRelations &&
|
|
158
157
|
!isModelStorage &&
|
|
159
|
-
!isNamespaceTableUniques &&
|
|
160
|
-
!isNamespaceTableIndexes &&
|
|
161
|
-
!isNamespaceTableForeignKeys &&
|
|
162
|
-
!isFkBooleanField &&
|
|
163
158
|
!isNullableField &&
|
|
164
|
-
!
|
|
159
|
+
!isFamilyPreserved
|
|
165
160
|
) {
|
|
166
161
|
continue;
|
|
167
162
|
}
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
result[key] = omitDefaults(value, currentPath);
|
|
165
|
+
result[key] = omitDefaults(value, currentPath, shouldPreserveEmpty);
|
|
171
166
|
}
|
|
172
167
|
|
|
173
168
|
return result;
|
|
@@ -191,88 +186,6 @@ function sortObjectKeys(obj: unknown): unknown {
|
|
|
191
186
|
return sorted;
|
|
192
187
|
}
|
|
193
188
|
|
|
194
|
-
type NamespaceObject = {
|
|
195
|
-
tables?: Record<string, unknown>;
|
|
196
|
-
[key: string]: unknown;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
type StorageObject = {
|
|
200
|
-
namespaces?: Record<string, unknown>;
|
|
201
|
-
[key: string]: unknown;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
type TableObject = {
|
|
205
|
-
indexes?: unknown[];
|
|
206
|
-
uniques?: unknown[];
|
|
207
|
-
[key: string]: unknown;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
function sortTableArrays(tableObj: TableObject): TableObject {
|
|
211
|
-
const sortedTable: TableObject = { ...tableObj };
|
|
212
|
-
|
|
213
|
-
if (Array.isArray(tableObj.indexes)) {
|
|
214
|
-
sortedTable.indexes = [...tableObj.indexes].sort((a, b) => {
|
|
215
|
-
const nameA = (a as { name?: string })?.name || '';
|
|
216
|
-
const nameB = (b as { name?: string })?.name || '';
|
|
217
|
-
return nameA.localeCompare(nameB);
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (Array.isArray(tableObj.uniques)) {
|
|
222
|
-
sortedTable.uniques = [...tableObj.uniques].sort((a, b) => {
|
|
223
|
-
const nameA = (a as { name?: string })?.name || '';
|
|
224
|
-
const nameB = (b as { name?: string })?.name || '';
|
|
225
|
-
return nameA.localeCompare(nameB);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return sortedTable;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sortIndexesAndUniques(storage: unknown): unknown {
|
|
233
|
-
if (!storage || typeof storage !== 'object') {
|
|
234
|
-
return storage;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const storageObj = storage as StorageObject;
|
|
238
|
-
if (!storageObj.namespaces || typeof storageObj.namespaces !== 'object') {
|
|
239
|
-
return storage;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const namespaces = storageObj.namespaces;
|
|
243
|
-
const result: StorageObject = { ...storageObj, namespaces: {} };
|
|
244
|
-
const resultNamespaces = result.namespaces as Record<string, unknown>;
|
|
245
|
-
|
|
246
|
-
for (const nsId of Object.keys(namespaces)) {
|
|
247
|
-
const ns = namespaces[nsId];
|
|
248
|
-
if (!ns || typeof ns !== 'object') {
|
|
249
|
-
resultNamespaces[nsId] = ns;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const nsObj = ns as NamespaceObject;
|
|
254
|
-
if (!nsObj.tables || typeof nsObj.tables !== 'object') {
|
|
255
|
-
resultNamespaces[nsId] = ns;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const sortedTables: Record<string, unknown> = {};
|
|
260
|
-
const sortedTableNames = Object.keys(nsObj.tables).sort();
|
|
261
|
-
for (const tableName of sortedTableNames) {
|
|
262
|
-
const table = nsObj.tables[tableName];
|
|
263
|
-
if (!table || typeof table !== 'object') {
|
|
264
|
-
sortedTables[tableName] = table;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
sortedTables[tableName] = sortTableArrays(table as TableObject);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
resultNamespaces[nsId] = { ...nsObj, tables: sortedTables };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
189
|
export function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
277
190
|
const ordered: Record<string, unknown> = {};
|
|
278
191
|
const remaining = new Set(Object.keys(obj));
|
|
@@ -304,6 +217,21 @@ export interface CanonicalizeContractOptions {
|
|
|
304
217
|
* the per-target serializer not putting them in the JSON shape.
|
|
305
218
|
*/
|
|
306
219
|
readonly serializeContract: SerializeContract;
|
|
220
|
+
/**
|
|
221
|
+
* Family-contributed preserve-empty predicate. When the walk encounters a
|
|
222
|
+
* default value (empty object/array or `false`) at `path`, calling this
|
|
223
|
+
* with the full path allows the family to veto the omission. If absent,
|
|
224
|
+
* only the framework's family-agnostic required-slot rules apply.
|
|
225
|
+
*/
|
|
226
|
+
readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
227
|
+
/**
|
|
228
|
+
* Family-contributed storage sort. Applied to the serialized `storage`
|
|
229
|
+
* subtree after the default-omission walk, before the final key sort.
|
|
230
|
+
* SQL family uses this to impose a deterministic order on `indexes` and
|
|
231
|
+
* `uniques` arrays within each namespace table. Families that require no
|
|
232
|
+
* special storage ordering omit this hook.
|
|
233
|
+
*/
|
|
234
|
+
readonly sortStorage?: StorageSort;
|
|
307
235
|
}
|
|
308
236
|
|
|
309
237
|
/**
|
|
@@ -322,17 +250,20 @@ export function canonicalizeContractToObject(
|
|
|
322
250
|
target: serialized['target'],
|
|
323
251
|
profileHash: serialized['profileHash'],
|
|
324
252
|
roots: serialized['roots'],
|
|
325
|
-
|
|
326
|
-
...ifDefined('valueObjects', serialized['valueObjects']),
|
|
253
|
+
domain: serialized['domain'],
|
|
327
254
|
storage: serialized['storage'],
|
|
328
255
|
...ifDefined('execution', serialized['execution']),
|
|
329
256
|
extensionPacks: serialized['extensionPacks'],
|
|
330
257
|
capabilities: serialized['capabilities'],
|
|
331
258
|
meta: serialized['meta'],
|
|
332
259
|
};
|
|
333
|
-
const withDefaultsOmitted = omitDefaults(normalized, []) as Record<
|
|
334
|
-
|
|
335
|
-
|
|
260
|
+
const withDefaultsOmitted = omitDefaults(normalized, [], options.shouldPreserveEmpty) as Record<
|
|
261
|
+
string,
|
|
262
|
+
unknown
|
|
263
|
+
>;
|
|
264
|
+
const withSortedStorage = options.sortStorage
|
|
265
|
+
? { ...withDefaultsOmitted, storage: options.sortStorage(withDefaultsOmitted['storage']) }
|
|
266
|
+
: withDefaultsOmitted;
|
|
336
267
|
const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;
|
|
337
268
|
return orderTopLevel(withSortedKeys);
|
|
338
269
|
}
|
package/src/contract-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CrossReference } from './cross-reference';
|
|
2
|
+
import type { ApplicationDomain } from './domain-envelope';
|
|
1
3
|
import type { ContractModelBase, ContractValueObject } from './domain-types';
|
|
2
4
|
import type {
|
|
3
5
|
ExecutionHashBase,
|
|
@@ -43,15 +45,12 @@ export interface Contract<
|
|
|
43
45
|
> {
|
|
44
46
|
readonly target: string;
|
|
45
47
|
readonly targetFamily: string;
|
|
46
|
-
readonly roots: Record<string,
|
|
47
|
-
readonly models: TModels;
|
|
48
|
-
readonly valueObjects?: Record<string, ContractValueObject>;
|
|
48
|
+
readonly roots: Record<string, CrossReference>;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* omitted from the on-disk envelope so emitted contracts stay byte-stable.
|
|
50
|
+
* Application plane (ADR 221): `domain.namespaces.<nsId>.{ models, valueObjects }`.
|
|
51
|
+
* `TModels` types the union of model entries across namespaces for family DSL inference.
|
|
53
52
|
*/
|
|
54
|
-
readonly domain
|
|
53
|
+
readonly domain: ApplicationDomain<TModels>;
|
|
55
54
|
readonly storage: TStorage;
|
|
56
55
|
readonly capabilities: Record<string, Record<string, boolean>>;
|
|
57
56
|
readonly extensionPacks: Record<string, unknown>;
|
|
@@ -59,3 +58,30 @@ export interface Contract<
|
|
|
59
58
|
readonly profileHash: ProfileHashBase<string>;
|
|
60
59
|
readonly meta: Record<string, unknown>;
|
|
61
60
|
}
|
|
61
|
+
|
|
62
|
+
export type ContractModelsMap<TContract extends Contract> =
|
|
63
|
+
TContract extends Contract<StorageBase, infer TModels> ? TModels : never;
|
|
64
|
+
|
|
65
|
+
type ExactlyOneKey<T extends Record<string, unknown>> = keyof T extends infer Only extends keyof T
|
|
66
|
+
? [keyof T] extends [Only]
|
|
67
|
+
? Only
|
|
68
|
+
: never
|
|
69
|
+
: never;
|
|
70
|
+
|
|
71
|
+
type NamespaceValueObjectsOf<TNamespace> = TNamespace extends {
|
|
72
|
+
readonly valueObjects?: infer VO;
|
|
73
|
+
}
|
|
74
|
+
? VO extends Record<string, ContractValueObject>
|
|
75
|
+
? VO
|
|
76
|
+
: Record<never, never>
|
|
77
|
+
: Record<never, never>;
|
|
78
|
+
|
|
79
|
+
/** Value-object map for the contract's sole domain namespace (type-level single-namespace projection). */
|
|
80
|
+
export type ContractValueObjectsMap<TContract extends Contract> =
|
|
81
|
+
NamespaceValueObjectsOf<
|
|
82
|
+
TContract['domain']['namespaces'][ExactlyOneKey<TContract['domain']['namespaces']>]
|
|
83
|
+
> extends infer Projected
|
|
84
|
+
? Projected extends Record<string, ContractValueObject>
|
|
85
|
+
? Projected
|
|
86
|
+
: Record<never, never>
|
|
87
|
+
: Record<never, never>;
|
|
@@ -10,3 +10,10 @@ export class ContractValidationError extends Error {
|
|
|
10
10
|
this.phase = phase;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
export class DomainNamespaceResolutionError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'DomainNamespaceResolutionError';
|
|
18
|
+
}
|
|
19
|
+
}
|