@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
@@ -0,0 +1,28 @@
1
+ import { blindCast } from '@prisma-next/utils/casts';
2
+ import { type Type, type } from 'arktype';
3
+ import { asNamespaceId, type NamespaceId } from './namespace-id';
4
+
5
+ export interface CrossReference {
6
+ readonly namespace: NamespaceId;
7
+ readonly model: string;
8
+ }
9
+
10
+ export const CrossReferenceSchema = blindCast<
11
+ Type<CrossReference>,
12
+ 'namespace is validated as string at runtime and branded to NamespaceId by asNamespaceId in crossRef(); the schema accepts plain strings but the public type reflects the branded shape'
13
+ >(
14
+ type({
15
+ '+': 'reject',
16
+ namespace: 'string',
17
+ model: 'string',
18
+ }),
19
+ );
20
+
21
+ const DEFAULT_CROSS_REF_NAMESPACE = '__unbound__';
22
+
23
+ export function crossRef(
24
+ model: string,
25
+ namespace: string = DEFAULT_CROSS_REF_NAMESPACE,
26
+ ): CrossReference {
27
+ return { namespace: asNamespaceId(namespace), model };
28
+ }
@@ -0,0 +1,87 @@
1
+ import { DomainNamespaceResolutionError } from './contract-validation-error';
2
+ import type { ContractModelBase, ContractValueObject } from './domain-types';
3
+
4
+ export const UNBOUND_DOMAIN_NAMESPACE_ID = '__unbound__' as const;
5
+
6
+ /**
7
+ * One namespace's application-domain entities — models and optional value
8
+ * objects keyed by entity name within that namespace coordinate.
9
+ */
10
+ export interface ApplicationDomainNamespace<
11
+ TModels extends Record<string, ContractModelBase> = Record<string, ContractModelBase>,
12
+ > {
13
+ readonly models: TModels;
14
+ readonly valueObjects?: Record<string, ContractValueObject>;
15
+ }
16
+
17
+ /**
18
+ * Application-domain envelope: entity content keyed by namespace id.
19
+ * Mirrors the storage plane's `namespaces` segment (ADR 221).
20
+ */
21
+ export interface ApplicationDomain<
22
+ TModels extends Record<string, ContractModelBase> = Record<string, ContractModelBase>,
23
+ > {
24
+ readonly namespaces: Readonly<Record<string, ApplicationDomainNamespace<TModels>>>;
25
+ }
26
+
27
+ export type ContractWithDomain = {
28
+ readonly domain: ApplicationDomain;
29
+ };
30
+
31
+ export function resolveSingleDomainNamespaceId(
32
+ domain: ApplicationDomain,
33
+ namespaceId?: string,
34
+ ): string {
35
+ if (namespaceId !== undefined) {
36
+ if (!Object.hasOwn(domain.namespaces, namespaceId)) {
37
+ throw new DomainNamespaceResolutionError(
38
+ `domain namespace "${namespaceId}" is not present on the contract`,
39
+ );
40
+ }
41
+ return namespaceId;
42
+ }
43
+
44
+ const namespaceIds = Object.keys(domain.namespaces);
45
+ if (namespaceIds.length === 0) {
46
+ throw new DomainNamespaceResolutionError('domain has no namespaces');
47
+ }
48
+ if (namespaceIds.length > 1) {
49
+ throw new DomainNamespaceResolutionError(
50
+ `expected exactly one domain namespace, found ${namespaceIds.length} (${namespaceIds.join(', ')})`,
51
+ );
52
+ }
53
+ const [soleNamespaceId] = namespaceIds;
54
+ if (soleNamespaceId === undefined) {
55
+ throw new DomainNamespaceResolutionError('domain has no namespaces');
56
+ }
57
+ return soleNamespaceId;
58
+ }
59
+
60
+ // Transitional single-namespace projection; pending runtime-qualification slice.
61
+ export function contractModels<TModels extends Record<string, ContractModelBase>>(
62
+ contract: { readonly domain: ApplicationDomain<TModels> },
63
+ namespaceId?: string,
64
+ ): TModels {
65
+ const resolved = resolveSingleDomainNamespaceId(contract.domain, namespaceId);
66
+ const domainNamespace = contract.domain.namespaces[resolved];
67
+ if (domainNamespace === undefined) {
68
+ throw new DomainNamespaceResolutionError(
69
+ `domain namespace "${resolved}" is not present on the contract`,
70
+ );
71
+ }
72
+ return domainNamespace.models;
73
+ }
74
+
75
+ export function contractValueObjects<TModels extends Record<string, ContractModelBase>>(
76
+ contract: { readonly domain: ApplicationDomain<TModels> },
77
+ namespaceId?: string,
78
+ ): Record<string, ContractValueObject> | undefined {
79
+ const resolved = resolveSingleDomainNamespaceId(contract.domain, namespaceId);
80
+ const domainNamespace = contract.domain.namespaces[resolved];
81
+ if (domainNamespace === undefined) {
82
+ throw new DomainNamespaceResolutionError(
83
+ `domain namespace "${resolved}" is not present on the contract`,
84
+ );
85
+ }
86
+ return domainNamespace.valueObjects;
87
+ }
@@ -1,3 +1,5 @@
1
+ import type { CrossReference } from './cross-reference';
2
+
1
3
  export type ScalarFieldType = {
2
4
  readonly kind: 'scalar';
3
5
  readonly codecId: string;
@@ -29,13 +31,13 @@ export type ContractRelationOn = {
29
31
  };
30
32
 
31
33
  export type ContractReferenceRelation = {
32
- readonly to: string;
34
+ readonly to: CrossReference;
33
35
  readonly cardinality: '1:1' | '1:N' | 'N:1';
34
36
  readonly on: ContractRelationOn;
35
37
  };
36
38
 
37
39
  export type ContractEmbedRelation = {
38
- readonly to: string;
40
+ readonly to: CrossReference;
39
41
  readonly cardinality: '1:1' | '1:N';
40
42
  };
41
43
 
@@ -61,7 +63,7 @@ export interface ContractModelBase<TModelStorage extends ModelStorageBase = Mode
61
63
  readonly storage: TModelStorage;
62
64
  readonly discriminator?: ContractDiscriminator;
63
65
  readonly variants?: Record<string, ContractVariantEntry>;
64
- readonly base?: string;
66
+ readonly base?: CrossReference;
65
67
  readonly owner?: string;
66
68
  }
67
69
 
@@ -72,24 +74,20 @@ export interface ContractModel<TModelStorage extends ModelStorageBase = ModelSto
72
74
 
73
75
  // ── Relation key helpers ─────────────────────────────────────────────────────
74
76
 
75
- type HasModelsWithRelations = {
76
- readonly models: Record<string, { readonly relations: Record<string, ContractRelation> }>;
77
- };
78
-
79
77
  export type ReferenceRelationKeys<
80
- TContract extends HasModelsWithRelations,
81
- ModelName extends string & keyof TContract['models'],
78
+ TModels extends Record<string, { readonly relations: Record<string, ContractRelation> }>,
79
+ ModelName extends string & keyof TModels,
82
80
  > = {
83
- [K in keyof TContract['models'][ModelName]['relations']]: TContract['models'][ModelName]['relations'][K] extends ContractReferenceRelation
81
+ [K in keyof TModels[ModelName]['relations']]: TModels[ModelName]['relations'][K] extends ContractReferenceRelation
84
82
  ? K
85
83
  : never;
86
- }[keyof TContract['models'][ModelName]['relations']];
84
+ }[keyof TModels[ModelName]['relations']];
87
85
 
88
86
  export type EmbedRelationKeys<
89
- TContract extends HasModelsWithRelations,
90
- ModelName extends string & keyof TContract['models'],
87
+ TModels extends Record<string, { readonly relations: Record<string, ContractRelation> }>,
88
+ ModelName extends string & keyof TModels,
91
89
  > = {
92
- [K in keyof TContract['models'][ModelName]['relations']]: TContract['models'][ModelName]['relations'][K] extends ContractReferenceRelation
90
+ [K in keyof TModels[ModelName]['relations']]: TModels[ModelName]['relations'][K] extends ContractReferenceRelation
93
91
  ? never
94
92
  : K;
95
- }[keyof TContract['models'][ModelName]['relations']];
93
+ }[keyof TModels[ModelName]['relations']];
@@ -1,4 +1,5 @@
1
1
  export {
2
2
  ContractValidationError,
3
3
  type ContractValidationPhase,
4
+ DomainNamespaceResolutionError,
4
5
  } from '../contract-validation-error';
@@ -0,0 +1,12 @@
1
+ export {
2
+ createPreserveEmptyPredicate,
3
+ matchesPathPattern,
4
+ type PathPattern,
5
+ type PathSegment as PreserveEmptyPathSegment,
6
+ } from '../canonicalization-path-match';
7
+ export {
8
+ compareByNameProperty,
9
+ createStorageSort,
10
+ type NamedArraySortTarget,
11
+ type PathSegment as StorageSortPathSegment,
12
+ } from '../canonicalization-storage-sort';
@@ -2,6 +2,8 @@ export {
2
2
  type CanonicalizeContractOptions,
3
3
  canonicalizeContract,
4
4
  canonicalizeContractToObject,
5
+ type PreserveEmptyPredicate,
5
6
  type SerializeContract,
7
+ type StorageSort,
6
8
  } from '../canonicalization';
7
9
  export { computeExecutionHash, computeProfileHash, computeStorageHash } from '../hashing';
@@ -1,4 +1,23 @@
1
- export type { Contract, ContractExecutionSection } from '../contract-types';
1
+ export type {
2
+ Contract,
3
+ ContractExecutionSection,
4
+ ContractModelsMap,
5
+ ContractValueObjectsMap,
6
+ } from '../contract-types';
7
+ export { DomainNamespaceResolutionError } from '../contract-validation-error';
8
+ export type { CrossReference } from '../cross-reference';
9
+ export { CrossReferenceSchema, crossRef } from '../cross-reference';
10
+ export type {
11
+ ApplicationDomain,
12
+ ApplicationDomainNamespace,
13
+ ContractWithDomain,
14
+ } from '../domain-envelope';
15
+ export {
16
+ contractModels,
17
+ contractValueObjects,
18
+ resolveSingleDomainNamespaceId,
19
+ UNBOUND_DOMAIN_NAMESPACE_ID,
20
+ } from '../domain-envelope';
2
21
  export type {
3
22
  ContractDiscriminator,
4
23
  ContractEmbedRelation,
@@ -18,6 +37,8 @@ export type {
18
37
  UnionFieldType,
19
38
  ValueObjectFieldType,
20
39
  } from '../domain-types';
40
+ export type { NamespaceId } from '../namespace-id';
41
+ export { asNamespaceId } from '../namespace-id';
21
42
  export type {
22
43
  $,
23
44
  Brand,
@@ -41,7 +62,9 @@ export type {
41
62
  ProfileHashBase,
42
63
  Source,
43
64
  StorageBase,
65
+ StorageEntitySlot,
44
66
  StorageHashBase,
67
+ StorageNamespace,
45
68
  } from '../types';
46
69
  export {
47
70
  coreHash,
package/src/hashing.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { ifDefined } from '@prisma-next/utils/defined';
2
3
  import type { JsonObject } from '@prisma-next/utils/json';
3
- import { canonicalizeContract } from './canonicalization';
4
+ import {
5
+ canonicalizeContract,
6
+ type PreserveEmptyPredicate,
7
+ type StorageSort,
8
+ } from './canonicalization';
4
9
  import type { Contract } from './contract-types';
5
10
  import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';
6
11
 
@@ -12,7 +17,13 @@ function sha256(content: string): string {
12
17
  return `sha256:${hash.digest('hex')}`;
13
18
  }
14
19
 
15
- function hashContract(section: Record<string, unknown>): string {
20
+ type HashContractSection = Record<string, unknown> & {
21
+ readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
22
+ readonly sortStorage?: StorageSort;
23
+ };
24
+
25
+ function hashContract(section: HashContractSection): string {
26
+ const { shouldPreserveEmpty, sortStorage, ...sectionData } = section;
16
27
  // Blind cast: the synthesised object is a hash-only stand-in
17
28
  // — never returned to callers, never executed as a Contract.
18
29
  // `canonicalizeContract` only walks the storage / execution /
@@ -20,29 +31,35 @@ function hashContract(section: Record<string, unknown>): string {
20
31
  // missing precise Contract typing on the other slots is
21
32
  // immaterial for the hash result.
22
33
  const contract = {
23
- targetFamily: section['targetFamily'],
24
- target: section['target'],
34
+ targetFamily: sectionData['targetFamily'],
35
+ target: sectionData['target'],
25
36
  roots: {},
26
- models: {},
27
- storage: section['storage'] ?? {},
28
- execution: section['execution'],
37
+ domain: { namespaces: {} },
38
+ storage: sectionData['storage'] ?? {},
39
+ execution: sectionData['execution'],
29
40
  extensionPacks: {},
30
- capabilities: section['capabilities'] ?? {},
41
+ capabilities: sectionData['capabilities'] ?? {},
31
42
  meta: {},
32
43
  profileHash: '',
33
- ...section,
44
+ ...sectionData,
34
45
  } as unknown as Contract;
35
46
  return canonicalizeContract(contract, {
36
47
  schemaVersion: SCHEMA_VERSION,
37
48
  serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,
49
+ ...ifDefined('shouldPreserveEmpty', shouldPreserveEmpty),
50
+ ...ifDefined('sortStorage', sortStorage),
38
51
  });
39
52
  }
40
53
 
41
- export function computeStorageHash(args: {
54
+ export type ComputeStorageHashArgs = {
42
55
  target: string;
43
56
  targetFamily: string;
44
57
  storage: Record<string, unknown>;
45
- }): StorageHashBase<string> {
58
+ readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
59
+ readonly sortStorage?: StorageSort;
60
+ };
61
+
62
+ export function computeStorageHash(args: ComputeStorageHashArgs): StorageHashBase<string> {
46
63
  return sha256(hashContract(args)) as StorageHashBase<string>;
47
64
  }
48
65
 
@@ -0,0 +1,10 @@
1
+ import { blindCast } from '@prisma-next/utils/casts';
2
+
3
+ export type NamespaceId = string & { readonly __brand: 'NamespaceId' };
4
+
5
+ export function asNamespaceId(value: string): NamespaceId {
6
+ return blindCast<
7
+ NamespaceId,
8
+ 'NamespaceId is a compile-time-only brand on string; this factory is the sole assertion site'
9
+ >(value);
10
+ }
package/src/types.ts CHANGED
@@ -48,13 +48,34 @@ export function profileHash<const T extends string>(value: T): ProfileHashBase<T
48
48
  return value as ProfileHashBase<T>;
49
49
  }
50
50
 
51
+ /**
52
+ * One entity-kind slot in a namespace — a map of entity name to entry.
53
+ * Values are opaque at the foundation layer; family and target concretions
54
+ * refine them to typed IR classes.
55
+ */
56
+ export type StorageEntitySlot = Readonly<Record<string, unknown>>;
57
+
58
+ /**
59
+ * Plain-data namespace entry in a storage block. Every hydrated contract
60
+ * carries at least `id` plus zero or more entity-kind slot maps (`tables`,
61
+ * `collections`, …). Foundation declares only this shape — no IR machinery.
62
+ */
63
+ export interface StorageNamespace {
64
+ readonly id: string;
65
+ }
66
+
51
67
  /**
52
68
  * Base type for family-specific storage blocks.
53
69
  * Family storage types (SqlStorage, MongoStorage, etc.) extend this to carry the
54
70
  * storage hash alongside family-specific data (tables, collections, etc.).
71
+ *
72
+ * The `namespaces` map is carried by every hydrated storage block. Serialized
73
+ * envelope shape is target-owned; this types the in-memory contract after
74
+ * `deserializeContract`.
55
75
  */
56
76
  export interface StorageBase<THash extends string = string> {
57
77
  readonly storageHash: StorageHashBase<THash>;
78
+ readonly namespaces: Readonly<Record<string, StorageNamespace>>;
58
79
  }
59
80
 
60
81
  export interface FieldType {