Skip to content

Commit fecfc1d

Browse files
committed
refactor(contract): coordinate-based domain identity in foundation package
Remove legacy flat domain read paths and cross-namespace merges; validate and project models by (namespace, name); use path-pattern helpers in the canonicalizer. Consumer migration remains for downstream call sites. Signed-off-by: Will Madden <madden@prisma.io>
1 parent a7015fe commit fecfc1d

6 files changed

Lines changed: 261 additions & 221 deletions

File tree

packages/1-framework/0-foundation/contract/src/canonicalization.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isArrayEqual } from '@prisma-next/utils/array-equal';
22
import { ifDefined } from '@prisma-next/utils/defined';
33
import type { JsonObject } from '@prisma-next/utils/json';
4-
4+
import { matchesPathPattern, type PathPattern } from './canonicalization-path-match';
55
import type { Contract } from './contract-types';
66

77
/**
@@ -33,6 +33,30 @@ export type PreserveEmptyPredicate = (path: readonly string[]) => boolean;
3333
*/
3434
export type StorageSort = (storage: unknown) => unknown;
3535

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+
3660
const TOP_LEVEL_ORDER = [
3761
'schemaVersion',
3862
'canonicalVersion',
@@ -91,15 +115,11 @@ function omitDefaults(
91115

92116
if (isDefaultValue(value)) {
93117
const isRequiredDomainNamespaces = isArrayEqual(currentPath, ['domain', 'namespaces']);
94-
const isDomainNamespaceSlot =
95-
currentPath.length === 3 &&
96-
isArrayEqual([currentPath[0], currentPath[1]], ['domain', 'namespaces']);
97-
const isRequiredDomainModels =
98-
currentPath.length === 4 &&
99-
isArrayEqual(
100-
[currentPath[0], currentPath[1], currentPath[3]],
101-
['domain', 'namespaces', 'models'],
102-
);
118+
const isDomainNamespaceSlot = matchesPathPattern(currentPath, DOMAIN_NAMESPACE_SLOT_PATTERN);
119+
const isRequiredDomainModels = matchesPathPattern(
120+
currentPath,
121+
DOMAIN_MODELS_CONTAINER_PATTERN,
122+
);
103123
const isRequiredStorageNamespaces = isArrayEqual(currentPath, ['storage', 'namespaces']);
104124
const isStorageNamespaceSlot =
105125
currentPath.length === 3 &&
@@ -114,18 +134,8 @@ function omitDefaults(
114134
'defaults',
115135
]);
116136
const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';
117-
const isModelRelations =
118-
currentPath.length === 6 &&
119-
currentPath[0] === 'domain' &&
120-
currentPath[1] === 'namespaces' &&
121-
currentPath[3] === 'models' &&
122-
currentPath[5] === 'relations';
123-
const isModelStorage =
124-
currentPath.length === 6 &&
125-
currentPath[0] === 'domain' &&
126-
currentPath[1] === 'namespaces' &&
127-
currentPath[3] === 'models' &&
128-
currentPath[5] === 'storage';
137+
const isModelRelations = matchesPathPattern(currentPath, DOMAIN_MODEL_RELATIONS_PATTERN);
138+
const isModelStorage = matchesPathPattern(currentPath, DOMAIN_MODEL_STORAGE_PATTERN);
129139

130140
const isNullableField = key === 'nullable';
131141

packages/1-framework/0-foundation/contract/src/contract-types.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ export interface Contract<
6262
export type ContractModelsMap<TContract extends Contract> =
6363
TContract extends Contract<StorageBase, infer TModels> ? TModels : never;
6464

65-
type UnionToIntersection<U> = (U extends unknown ? (value: U) => void : never) extends (
66-
value: infer I,
67-
) => void
68-
? I
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
6969
: never;
7070

7171
type NamespaceValueObjectsOf<TNamespace> = TNamespace extends {
@@ -76,16 +76,12 @@ type NamespaceValueObjectsOf<TNamespace> = TNamespace extends {
7676
: Record<never, never>
7777
: Record<never, never>;
7878

79-
/** Merged value-object map across all domain namespaces (for type-level inference). */
79+
/** Value-object map for the contract's sole domain namespace (type-level single-namespace projection). */
8080
export type ContractValueObjectsMap<TContract extends Contract> =
81-
UnionToIntersection<
82-
{
83-
[K in keyof TContract['domain']['namespaces']]: NamespaceValueObjectsOf<
84-
TContract['domain']['namespaces'][K]
85-
>;
86-
}[keyof TContract['domain']['namespaces']]
87-
> extends infer Merged
88-
? Merged extends Record<string, ContractValueObject>
89-
? Merged
81+
NamespaceValueObjectsOf<
82+
TContract['domain']['namespaces'][ExactlyOneKey<TContract['domain']['namespaces']>]
83+
> extends infer Projected
84+
? Projected extends Record<string, ContractValueObject>
85+
? Projected
9086
: Record<never, never>
9187
: Record<never, never>;
Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ContractModelBase, ContractValueObject } from './domain-types';
2+
import type { NamespaceId } from './namespace-id';
23

34
export const UNBOUND_DOMAIN_NAMESPACE_ID = '__unbound__' as const;
45

@@ -23,62 +24,53 @@ export interface DomainPlane<
2324
readonly namespaces: Readonly<Record<string, DomainNamespace<TModels>>>;
2425
}
2526

26-
export type DomainContractSlice = {
27+
export type ContractWithDomain = {
2728
readonly domain: DomainPlane;
2829
};
2930

30-
/** Pre-domain-envelope contract root still carrying flat `models` / `valueObjects`. */
31-
export type LegacyFlatDomainRoot = {
32-
readonly models?: Record<string, ContractModelBase>;
33-
readonly valueObjects?: Record<string, ContractValueObject>;
34-
};
35-
36-
export type DomainContractInput = DomainContractSlice | LegacyFlatDomainRoot;
37-
38-
function domainNamespacesOf(contract: DomainContractInput): DomainPlane['namespaces'] | undefined {
39-
if (!('domain' in contract) || contract.domain?.namespaces === undefined) {
40-
return undefined;
31+
export class DomainNamespaceResolutionError extends Error {
32+
constructor(message: string) {
33+
super(message);
34+
this.name = 'DomainNamespaceResolutionError';
4135
}
42-
return contract.domain.namespaces;
43-
}
44-
45-
function isLegacyFlatDomainRoot(contract: DomainContractInput): contract is LegacyFlatDomainRoot {
46-
return domainNamespacesOf(contract) === undefined;
4736
}
4837

49-
export function contractModels(contract: DomainContractInput): Record<string, ContractModelBase> {
50-
const namespaces = domainNamespacesOf(contract);
51-
if (namespaces !== undefined) {
52-
const merged: Record<string, ContractModelBase> = {};
53-
for (const ns of Object.values(namespaces)) {
54-
Object.assign(merged, ns.models);
38+
export function resolveSingleDomainNamespaceId(domain: DomainPlane, namespaceId?: string): string {
39+
if (namespaceId !== undefined) {
40+
if (!Object.hasOwn(domain.namespaces, namespaceId)) {
41+
throw new DomainNamespaceResolutionError(
42+
`domain namespace "${namespaceId}" is not present on the contract`,
43+
);
5544
}
56-
return merged;
45+
return namespaceId;
5746
}
58-
if (isLegacyFlatDomainRoot(contract)) {
59-
return contract.models ?? {};
47+
48+
const namespaceIds = Object.keys(domain.namespaces);
49+
if (namespaceIds.length === 0) {
50+
throw new DomainNamespaceResolutionError('domain has no namespaces');
51+
}
52+
if (namespaceIds.length > 1) {
53+
throw new DomainNamespaceResolutionError(
54+
`expected exactly one domain namespace, found ${namespaceIds.length} (${namespaceIds.join(', ')})`,
55+
);
6056
}
61-
return {};
57+
return namespaceIds[0]!;
58+
}
59+
60+
export function contractModels(
61+
contract: ContractWithDomain,
62+
namespaceId?: string,
63+
): Record<string, ContractModelBase> {
64+
const resolved = resolveSingleDomainNamespaceId(contract.domain, namespaceId);
65+
return contract.domain.namespaces[resolved]!.models;
6266
}
6367

6468
export function contractValueObjects(
65-
contract: DomainContractInput,
69+
contract: ContractWithDomain,
70+
namespaceId?: string,
6671
): Record<string, ContractValueObject> | undefined {
67-
const namespaces = domainNamespacesOf(contract);
68-
if (namespaces !== undefined) {
69-
const merged: Record<string, ContractValueObject> = {};
70-
let any = false;
71-
for (const ns of Object.values(namespaces)) {
72-
if (ns.valueObjects === undefined) continue;
73-
any = true;
74-
Object.assign(merged, ns.valueObjects);
75-
}
76-
return any ? merged : undefined;
77-
}
78-
if (isLegacyFlatDomainRoot(contract)) {
79-
return contract.valueObjects;
80-
}
81-
return undefined;
72+
const resolved = resolveSingleDomainNamespaceId(contract.domain, namespaceId);
73+
return contract.domain.namespaces[resolved]!.valueObjects;
8274
}
8375

8476
export function buildDomainPlaneFromFlat(params: {
@@ -97,35 +89,6 @@ export function buildDomainPlaneFromFlat(params: {
9789
};
9890
}
9991

100-
/**
101-
* Lifts a legacy flat `models` / `valueObjects` contract root into
102-
* `domain.namespaces.__unbound__` when the domain envelope is absent.
103-
*/
104-
export function normalizeLegacyDomainRoot(value: Record<string, unknown>): Record<string, unknown> {
105-
if (value['domain'] !== undefined && value['domain'] !== null) {
106-
return value;
107-
}
108-
const models = value['models'];
109-
if (models === undefined || typeof models !== 'object' || models === null) {
110-
return value;
111-
}
112-
const valueObjects = value['valueObjects'];
113-
const namespace: Record<string, unknown> = { models };
114-
if (
115-
valueObjects !== undefined &&
116-
typeof valueObjects === 'object' &&
117-
valueObjects !== null &&
118-
!Array.isArray(valueObjects)
119-
) {
120-
namespace['valueObjects'] = valueObjects;
121-
}
122-
const { models: _m, valueObjects: _vo, ...rest } = value;
123-
return {
124-
...rest,
125-
domain: {
126-
namespaces: {
127-
[UNBOUND_DOMAIN_NAMESPACE_ID]: namespace,
128-
},
129-
},
130-
};
92+
export function modelCoordinateKey(namespace: NamespaceId, model: string): string {
93+
return `${namespace}:${model}`;
13194
}

packages/1-framework/0-foundation/contract/src/exports/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@ export type {
77
export type { CrossReference } from '../cross-reference';
88
export { CrossReferenceSchema, crossRef } from '../cross-reference';
99
export type {
10-
DomainContractInput,
11-
DomainContractSlice,
10+
ContractWithDomain,
1211
DomainNamespace,
1312
DomainPlane,
14-
LegacyFlatDomainRoot,
1513
} from '../domain-envelope';
1614
export {
1715
buildDomainPlaneFromFlat,
1816
contractModels,
1917
contractValueObjects,
20-
normalizeLegacyDomainRoot,
18+
modelCoordinateKey,
19+
resolveSingleDomainNamespaceId,
2120
UNBOUND_DOMAIN_NAMESPACE_ID,
2221
} from '../domain-envelope';
2322
export type {

0 commit comments

Comments
 (0)