@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.
Files changed (62) hide show
  1. package/dist/canonicalization-DFE0HJkI.d.mts +69 -0
  2. package/dist/canonicalization-DFE0HJkI.d.mts.map +1 -0
  3. package/dist/canonicalization-path-match-b2jFuEso.mjs +25 -0
  4. package/dist/canonicalization-path-match-b2jFuEso.mjs.map +1 -0
  5. package/dist/{contract-types-Bt2uyqs3.d.mts → contract-types-xgwKtd7y.d.mts} +34 -74
  6. package/dist/contract-types-xgwKtd7y.d.mts.map +1 -0
  7. package/dist/contract-validation-error-ClZaKqMW.mjs +20 -0
  8. package/dist/contract-validation-error-ClZaKqMW.mjs.map +1 -0
  9. package/dist/contract-validation-error-T5LH4DW-.d.mts +13 -0
  10. package/dist/contract-validation-error-T5LH4DW-.d.mts.map +1 -0
  11. package/dist/contract-validation-error.d.mts +2 -10
  12. package/dist/contract-validation-error.mjs +2 -2
  13. package/dist/domain-envelope-4hyFtJ4_.d.mts +110 -0
  14. package/dist/domain-envelope-4hyFtJ4_.d.mts.map +1 -0
  15. package/dist/hashing-utils.d.mts +19 -0
  16. package/dist/hashing-utils.d.mts.map +1 -0
  17. package/dist/hashing-utils.mjs +50 -0
  18. package/dist/hashing-utils.mjs.map +1 -0
  19. package/dist/hashing.d.mts +8 -37
  20. package/dist/hashing.d.mts.map +1 -1
  21. package/dist/hashing.mjs +175 -1
  22. package/dist/hashing.mjs.map +1 -0
  23. package/dist/namespace-id-CVpkSFUK.mjs +9 -0
  24. package/dist/namespace-id-CVpkSFUK.mjs.map +1 -0
  25. package/dist/types.d.mts +4 -2
  26. package/dist/types.mjs +91 -2
  27. package/dist/types.mjs.map +1 -0
  28. package/dist/validate-domain.d.mts +6 -8
  29. package/dist/validate-domain.d.mts.map +1 -1
  30. package/dist/validate-domain.mjs +99 -56
  31. package/dist/validate-domain.mjs.map +1 -1
  32. package/package.json +16 -9
  33. package/src/canonicalization-path-match.ts +44 -0
  34. package/src/canonicalization-storage-sort.ts +88 -0
  35. package/src/canonicalization.ts +92 -161
  36. package/src/contract-types.ts +33 -7
  37. package/src/contract-validation-error.ts +7 -0
  38. package/src/cross-reference.ts +28 -0
  39. package/src/domain-envelope.ts +87 -0
  40. package/src/domain-types.ts +13 -15
  41. package/src/exports/contract-validation-error.ts +1 -0
  42. package/src/exports/hashing-utils.ts +12 -0
  43. package/src/exports/hashing.ts +2 -0
  44. package/src/exports/types.ts +24 -1
  45. package/src/hashing.ts +28 -11
  46. package/src/namespace-id.ts +10 -0
  47. package/src/types.ts +21 -0
  48. package/src/validate-domain.ts +162 -94
  49. package/dist/contract-types-Bt2uyqs3.d.mts.map +0 -1
  50. package/dist/contract-validation-error-Dp2vHZt5.mjs +0 -14
  51. package/dist/contract-validation-error-Dp2vHZt5.mjs.map +0 -1
  52. package/dist/contract-validation-error.d.mts.map +0 -1
  53. package/dist/hashing-rZiqFOlc.mjs +0 -204
  54. package/dist/hashing-rZiqFOlc.mjs.map +0 -1
  55. package/dist/testing.d.mts +0 -32
  56. package/dist/testing.d.mts.map +0 -1
  57. package/dist/testing.mjs +0 -63
  58. package/dist/testing.mjs.map +0 -1
  59. package/dist/types-CVGwkRLa.mjs +0 -46
  60. package/dist/types-CVGwkRLa.mjs.map +0 -1
  61. package/src/exports/testing.ts +0 -1
  62. 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":"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"}
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.11.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.11.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/test-utils": "0.11.0",
15
- "@prisma-next/tsconfig": "0.11.0",
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
- "./testing": "./dist/testing.mjs",
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
+ }
@@ -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(obj: unknown, path: readonly string[]): unknown {
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 isRequiredModels = isArrayEqual(currentPath, ['models']);
73
- const isRequiredNamespaces = isArrayEqual(currentPath, ['storage', 'namespaces']);
74
- const isNamespaceSlot =
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
- currentPath.length === 3 &&
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
- !isRequiredModels &&
147
- !isRequiredNamespaces &&
148
- !isNamespaceSlot &&
149
- !isRequiredNamespaceTables &&
150
- !isNamespaceTableEntry &&
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
- !isStorageTypeTypeParams
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
- models: serialized['models'],
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<string, unknown>;
334
- const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);
335
- const withSortedStorage = { ...withDefaultsOmitted, storage: withSortedIndexes };
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
  }
@@ -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, string>;
47
- readonly models: TModels;
48
- readonly valueObjects?: Record<string, ContractValueObject>;
48
+ readonly roots: Record<string, CrossReference>;
49
49
  /**
50
- * Domain plane keyed as `domain[plane][namespaceId][entityKind][entityName]`.
51
- * Optional until downstream slices populate it; when absent the field is
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?: Record<string, Record<string, Record<string, unknown>>>;
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
+ }