@prisma-next/contract 0.12.0 → 0.13.0-dev.10
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/apply-specifier-default-control-policy.d.mts +7 -0
- package/dist/apply-specifier-default-control-policy.d.mts.map +1 -0
- package/dist/apply-specifier-default-control-policy.mjs +12 -0
- package/dist/apply-specifier-default-control-policy.mjs.map +1 -0
- package/dist/{canonicalization-DFE0HJkI.d.mts → canonicalization-BqYzAeWE.d.mts} +2 -2
- package/dist/canonicalization-BqYzAeWE.d.mts.map +1 -0
- package/dist/{canonicalization-path-match-b2jFuEso.mjs → canonicalization-path-match-CNgHuwM_.mjs} +1 -1
- package/dist/{canonicalization-path-match-b2jFuEso.mjs.map → canonicalization-path-match-CNgHuwM_.mjs.map} +1 -1
- package/dist/contract-types-CtIh62PH.d.mts +82 -0
- package/dist/contract-types-CtIh62PH.d.mts.map +1 -0
- package/dist/{contract-validation-error-ClZaKqMW.mjs → contract-validation-error-BFA66rwU.mjs} +1 -1
- package/dist/{contract-validation-error-ClZaKqMW.mjs.map → contract-validation-error-BFA66rwU.mjs.map} +1 -1
- package/dist/{contract-validation-error-T5LH4DW-.d.mts → contract-validation-error-D7g0kmcc.d.mts} +1 -1
- package/dist/{contract-validation-error-T5LH4DW-.d.mts.map → contract-validation-error-D7g0kmcc.d.mts.map} +1 -1
- package/dist/contract-validation-error.d.mts +1 -1
- package/dist/contract-validation-error.mjs +1 -1
- package/dist/default-namespace-D5X_k6hJ.d.mts +25 -0
- package/dist/default-namespace-D5X_k6hJ.d.mts.map +1 -0
- package/dist/default-namespace-rpdJeUMq.mjs +33 -0
- package/dist/default-namespace-rpdJeUMq.mjs.map +1 -0
- package/dist/default-namespace.d.mts +2 -0
- package/dist/default-namespace.mjs +2 -0
- package/dist/domain-envelope-DKOnhO5d.d.mts +352 -0
- package/dist/domain-envelope-DKOnhO5d.d.mts.map +1 -0
- package/dist/enum-accessor.d.mts +77 -0
- package/dist/enum-accessor.d.mts.map +1 -0
- package/dist/enum-accessor.mjs +45 -0
- package/dist/enum-accessor.mjs.map +1 -0
- package/dist/hashing-utils.d.mts +1 -1
- package/dist/hashing-utils.d.mts.map +1 -1
- package/dist/hashing-utils.mjs +5 -4
- package/dist/hashing-utils.mjs.map +1 -1
- package/dist/hashing.d.mts +2 -2
- package/dist/hashing.d.mts.map +1 -1
- package/dist/hashing.mjs +38 -11
- package/dist/hashing.mjs.map +1 -1
- package/dist/{namespace-id-CVpkSFUK.mjs → namespace-id-CUxYd4KL.mjs} +1 -1
- package/dist/{namespace-id-CVpkSFUK.mjs.map → namespace-id-CUxYd4KL.mjs.map} +1 -1
- package/dist/resolve-domain-model-BovPAsW2.mjs +20 -0
- package/dist/resolve-domain-model-BovPAsW2.mjs.map +1 -0
- package/dist/resolve-domain-model-CSEqpByI.d.mts +17 -0
- package/dist/resolve-domain-model-CSEqpByI.d.mts.map +1 -0
- package/dist/resolve-domain-model.d.mts +2 -0
- package/dist/resolve-domain-model.mjs +2 -0
- package/dist/types.d.mts +21 -4
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +40 -30
- package/dist/types.mjs.map +1 -1
- package/dist/validate-domain.d.mts +1 -1
- package/dist/validate-domain.mjs +6 -3
- package/dist/validate-domain.mjs.map +1 -1
- package/package.json +10 -6
- package/src/apply-specifier-default-control-policy.ts +12 -0
- package/src/canonicalization-storage-sort.ts +8 -1
- package/src/canonicalization.ts +13 -4
- package/src/contract-types.ts +9 -5
- package/src/control-policy.ts +25 -0
- package/src/cross-reference.ts +15 -3
- package/src/default-namespace.ts +36 -0
- package/src/domain-envelope.ts +3 -61
- package/src/domain-namespace-access.ts +32 -0
- package/src/domain-types.ts +32 -1
- package/src/enum-accessor.ts +173 -0
- package/src/exports/apply-specifier-default-control-policy.ts +1 -0
- package/src/exports/default-namespace.ts +1 -0
- package/src/exports/enum-accessor.ts +11 -0
- package/src/exports/resolve-domain-model.ts +1 -0
- package/src/exports/types.ts +16 -7
- package/src/hashing.ts +42 -13
- package/src/resolve-domain-model.ts +27 -0
- package/src/types.ts +18 -2
- package/src/validate-domain.ts +6 -0
- package/src/value-set-ref.ts +26 -0
- package/dist/canonicalization-DFE0HJkI.d.mts.map +0 -1
- package/dist/contract-types-xgwKtd7y.d.mts +0 -233
- package/dist/contract-types-xgwKtd7y.d.mts.map +0 -1
- package/dist/domain-envelope-4hyFtJ4_.d.mts +0 -110
- package/dist/domain-envelope-4hyFtJ4_.d.mts.map +0 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DomainNamespaceResolutionError } from './contract-validation-error';
|
|
2
|
+
import { soleDomainNamespaceId } from './default-namespace';
|
|
3
|
+
import type { ApplicationDomain } from './domain-envelope';
|
|
4
|
+
import type { ContractModelBase, ContractValueObject } from './domain-types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Models map for the contract's single domain namespace. Throws when the
|
|
8
|
+
* contract does not declare exactly one namespace — bare-name access is
|
|
9
|
+
* ambiguous across namespaces and must be qualified explicitly (TML-2550).
|
|
10
|
+
*/
|
|
11
|
+
export function domainModelsAtDefaultNamespace<TModels extends Record<string, ContractModelBase>>(
|
|
12
|
+
domain: ApplicationDomain<TModels>,
|
|
13
|
+
): TModels {
|
|
14
|
+
const namespaceId = soleDomainNamespaceId(domain);
|
|
15
|
+
const domainNamespace = domain.namespaces[namespaceId];
|
|
16
|
+
if (domainNamespace === undefined) {
|
|
17
|
+
throw new DomainNamespaceResolutionError(
|
|
18
|
+
`domain namespace "${namespaceId}" is not present on the contract`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return domainNamespace.models;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Value objects for the contract's single domain namespace, when present.
|
|
26
|
+
* Throws when the contract does not declare exactly one namespace.
|
|
27
|
+
*/
|
|
28
|
+
export function domainValueObjectsAtDefaultNamespace<
|
|
29
|
+
TModels extends Record<string, ContractModelBase>,
|
|
30
|
+
>(domain: ApplicationDomain<TModels>): Record<string, ContractValueObject> | undefined {
|
|
31
|
+
return domain.namespaces[soleDomainNamespaceId(domain)]?.valueObjects;
|
|
32
|
+
}
|
package/src/domain-types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CrossReference } from './cross-reference';
|
|
2
|
+
import type { JsonValue } from './types';
|
|
3
|
+
import type { ValueSetRef } from './value-set-ref';
|
|
2
4
|
|
|
3
5
|
export type ScalarFieldType = {
|
|
4
6
|
readonly kind: 'scalar';
|
|
@@ -23,6 +25,17 @@ export type ContractField = {
|
|
|
23
25
|
readonly type: ContractFieldType;
|
|
24
26
|
readonly many?: true;
|
|
25
27
|
readonly dict?: true;
|
|
28
|
+
readonly valueSet?: ValueSetRef;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A domain enum: an ordered set of named members, each with a codec-encoded
|
|
33
|
+
* value. The `codecId` identifies the codec used to encode member values in
|
|
34
|
+
* storage. The `members` array is ordered (declaration order is preserved).
|
|
35
|
+
*/
|
|
36
|
+
export type ContractEnum = {
|
|
37
|
+
readonly codecId: string;
|
|
38
|
+
readonly members: readonly { readonly name: string; readonly value: JsonValue }[];
|
|
26
39
|
};
|
|
27
40
|
|
|
28
41
|
export type ContractRelationOn = {
|
|
@@ -30,12 +43,30 @@ export type ContractRelationOn = {
|
|
|
30
43
|
readonly targetFields: readonly string[];
|
|
31
44
|
};
|
|
32
45
|
|
|
33
|
-
export type
|
|
46
|
+
export type ContractRelationThrough = {
|
|
47
|
+
readonly table: string;
|
|
48
|
+
readonly namespaceId: string;
|
|
49
|
+
readonly parentColumns: readonly string[];
|
|
50
|
+
readonly childColumns: readonly string[];
|
|
51
|
+
readonly targetColumns: readonly string[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ContractManyToManyRelation = {
|
|
55
|
+
readonly to: CrossReference;
|
|
56
|
+
readonly cardinality: 'N:M';
|
|
57
|
+
readonly on: ContractRelationOn;
|
|
58
|
+
readonly through: ContractRelationThrough;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ContractNonJunctionRelation = {
|
|
34
62
|
readonly to: CrossReference;
|
|
35
63
|
readonly cardinality: '1:1' | '1:N' | 'N:1';
|
|
36
64
|
readonly on: ContractRelationOn;
|
|
65
|
+
readonly through?: never;
|
|
37
66
|
};
|
|
38
67
|
|
|
68
|
+
export type ContractReferenceRelation = ContractManyToManyRelation | ContractNonJunctionRelation;
|
|
69
|
+
|
|
39
70
|
export type ContractEmbedRelation = {
|
|
40
71
|
readonly to: CrossReference;
|
|
41
72
|
readonly cardinality: '1:1' | '1:N';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Contract } from './contract-types';
|
|
2
|
+
import type { ContractEnum } from './domain-types';
|
|
3
|
+
import type { JsonValue } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime view of a domain enum, built at the client from the emitted
|
|
7
|
+
* `ContractEnum` JSON (codec-encoded `JsonValue` members, literal types erased).
|
|
8
|
+
*
|
|
9
|
+
* This deliberately mirrors the accessor shape of the authoring-time
|
|
10
|
+
* `EnumTypeHandle` (in `contract-ts`) rather than reusing it: that handle carries
|
|
11
|
+
* the literal value generics and lives in the authoring layer, which the
|
|
12
|
+
* foundation layer cannot depend on. The two are the same surface seen from the
|
|
13
|
+
* two planes — authoring (typed) and runtime (validated JSON).
|
|
14
|
+
*/
|
|
15
|
+
export interface EnumAccessor {
|
|
16
|
+
readonly values: readonly JsonValue[];
|
|
17
|
+
readonly names: readonly string[];
|
|
18
|
+
readonly members: Readonly<Record<string, JsonValue>>;
|
|
19
|
+
has(v: JsonValue): boolean;
|
|
20
|
+
nameOf(v: JsonValue): string | undefined;
|
|
21
|
+
ordinalOf(v: JsonValue): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createEnumAccessor(contractEnum: ContractEnum): EnumAccessor {
|
|
25
|
+
const values = Object.freeze(contractEnum.members.map((m) => m.value));
|
|
26
|
+
const names = Object.freeze(contractEnum.members.map((m) => m.name));
|
|
27
|
+
const members: Readonly<Record<string, JsonValue>> = Object.freeze(
|
|
28
|
+
Object.fromEntries(contractEnum.members.map((m) => [m.name, m.value])),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const valueSet = new Set(values);
|
|
32
|
+
const valueToName = new Map(contractEnum.members.map((m) => [m.value, m.name]));
|
|
33
|
+
const valueToOrdinal = new Map(values.map((v, i) => [v, i]));
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
values,
|
|
37
|
+
names,
|
|
38
|
+
members,
|
|
39
|
+
has: (v: JsonValue) => valueSet.has(v),
|
|
40
|
+
nameOf: (v: JsonValue) => valueToName.get(v),
|
|
41
|
+
ordinalOf: (v: JsonValue) => valueToOrdinal.get(v) ?? -1,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the enum-accessor map for a single namespace, keyed by enum name.
|
|
47
|
+
* Each namespace facet exposes only its own enums — the IR keys enums under
|
|
48
|
+
* `domain.namespaces[ns].enum`, so the same name in two namespaces resolves
|
|
49
|
+
* independently rather than colliding in one flat map.
|
|
50
|
+
*/
|
|
51
|
+
export function buildEnumsMapForNamespace(
|
|
52
|
+
domain: {
|
|
53
|
+
readonly namespaces: Readonly<
|
|
54
|
+
Record<string, { readonly enum?: Readonly<Record<string, ContractEnum>> }>
|
|
55
|
+
>;
|
|
56
|
+
},
|
|
57
|
+
namespaceId: string,
|
|
58
|
+
): Record<string, EnumAccessor> {
|
|
59
|
+
const result: Record<string, EnumAccessor> = {};
|
|
60
|
+
const namespace = domain.namespaces[namespaceId];
|
|
61
|
+
if (namespace?.enum) {
|
|
62
|
+
for (const [name, contractEnum] of Object.entries(namespace.enum)) {
|
|
63
|
+
result[name] = createEnumAccessor(contractEnum);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the enum-accessor map for every namespace of a domain, keyed by
|
|
71
|
+
* namespace id then enum name. This is the lane-agnostic enum surface the
|
|
72
|
+
* `db.enums` facade member exposes: enums are contract metadata, the same
|
|
73
|
+
* whether reached through the sql lane or the orm lane, so the facade builds
|
|
74
|
+
* this once and projects it per target.
|
|
75
|
+
*/
|
|
76
|
+
export function buildNamespacedEnums(domain: {
|
|
77
|
+
readonly namespaces: Readonly<
|
|
78
|
+
Record<string, { readonly enum?: Readonly<Record<string, ContractEnum>> }>
|
|
79
|
+
>;
|
|
80
|
+
}): Record<string, Record<string, EnumAccessor>> {
|
|
81
|
+
const result: Record<string, Record<string, EnumAccessor>> = {};
|
|
82
|
+
for (const namespaceId of Object.keys(domain.namespaces)) {
|
|
83
|
+
result[namespaceId] = buildEnumsMapForNamespace(domain, namespaceId);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Type-level projection of the namespaced enum surface.
|
|
90
|
+
//
|
|
91
|
+
// These types derive the literal-preserving accessor shape from the contract,
|
|
92
|
+
// hung off the `db.enums` facade map (`db.enums.<ns>.<Name>`). They are the
|
|
93
|
+
// same accessors the runtime builds above, but typed from the two emission
|
|
94
|
+
// paths:
|
|
95
|
+
// - Emitted contracts carry the literal enum entries under
|
|
96
|
+
// `domain.namespaces[ns].enum`; each maps to a `ContractEnumAccessor`.
|
|
97
|
+
// - The no-emit (built) contract carries them flat on `enumAccessors`
|
|
98
|
+
// (already accessor-shaped, literal-preserving), since its built domain
|
|
99
|
+
// type does not narrow `namespaces[ns].enum`. All authored enums land in
|
|
100
|
+
// the single built namespace, so exposing the flat map per namespace is
|
|
101
|
+
// correct there.
|
|
102
|
+
// Only `SqlContractResult` carries `enumAccessors`; emitted contracts never
|
|
103
|
+
// do, so the two carriers never overlap.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
type Present<T> = Exclude<T, undefined>;
|
|
107
|
+
|
|
108
|
+
// A domain enum entry as carried in `domain.namespaces[ns].enum[name]`: an
|
|
109
|
+
// ordered member tuple. The no-emit (built) path preserves the literal member
|
|
110
|
+
// values so the derived accessor keeps its literal `values`/`names`/`members`.
|
|
111
|
+
type EnumMemberEntry = { readonly name: string; readonly value: unknown };
|
|
112
|
+
type EnumEntry = { readonly members: readonly EnumMemberEntry[] };
|
|
113
|
+
|
|
114
|
+
type EnumEntryValues<Entry extends EnumEntry> = {
|
|
115
|
+
readonly [I in keyof Entry['members']]: Entry['members'][I] extends EnumMemberEntry
|
|
116
|
+
? Entry['members'][I]['value']
|
|
117
|
+
: never;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type EnumEntryNames<Entry extends EnumEntry> = {
|
|
121
|
+
readonly [I in keyof Entry['members']]: Entry['members'][I] extends EnumMemberEntry
|
|
122
|
+
? Entry['members'][I]['name']
|
|
123
|
+
: never;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type EnumEntryMembers<Entry extends EnumEntry> = {
|
|
127
|
+
readonly [M in Entry['members'][number] as M['name']]: M['value'];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// The runtime accessor shape for one enum, with literal `values`/`names`/
|
|
131
|
+
// `members` derived from the entry's member tuple. Mirrors `EnumAccessor`'s
|
|
132
|
+
// runtime surface and the authoring `EnumTypeHandle` accessor.
|
|
133
|
+
export type ContractEnumAccessor<Entry extends EnumEntry> = {
|
|
134
|
+
readonly values: EnumEntryValues<Entry>;
|
|
135
|
+
readonly names: EnumEntryNames<Entry>;
|
|
136
|
+
readonly members: EnumEntryMembers<Entry>;
|
|
137
|
+
has(v: EnumEntryValues<Entry>[number]): boolean;
|
|
138
|
+
nameOf(v: EnumEntryValues<Entry>[number]): string | undefined;
|
|
139
|
+
ordinalOf(v: EnumEntryValues<Entry>[number]): number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type EnumEntriesToAccessors<Enums> = {
|
|
143
|
+
readonly [K in keyof Enums]: Enums[K] extends EnumEntry ? ContractEnumAccessor<Enums[K]> : never;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type BuiltEnumAccessorsOf<TContract> = TContract extends {
|
|
147
|
+
readonly enumAccessors: infer A;
|
|
148
|
+
}
|
|
149
|
+
? A
|
|
150
|
+
: Record<never, never>;
|
|
151
|
+
|
|
152
|
+
type NamespaceEnumEntries<TNamespace> = TNamespace extends {
|
|
153
|
+
readonly enum?: infer E;
|
|
154
|
+
}
|
|
155
|
+
? unknown extends E
|
|
156
|
+
? Record<never, never>
|
|
157
|
+
: Present<E>
|
|
158
|
+
: Record<never, never>;
|
|
159
|
+
|
|
160
|
+
// The per-namespace enum accessors. Each namespace exposes only its own enums
|
|
161
|
+
// (the IR's `domain.namespaces[ns].enum`), so the same enum name in two
|
|
162
|
+
// namespaces resolves to each namespace's own accessor.
|
|
163
|
+
export type NamespaceEnumAccessors<
|
|
164
|
+
TContract extends Contract,
|
|
165
|
+
NsId extends keyof TContract['domain']['namespaces'],
|
|
166
|
+
> = EnumEntriesToAccessors<NamespaceEnumEntries<TContract['domain']['namespaces'][NsId]>> &
|
|
167
|
+
BuiltEnumAccessorsOf<TContract>;
|
|
168
|
+
|
|
169
|
+
// The lane-agnostic enum surface exposed on the `db.enums` facade member: a
|
|
170
|
+
// namespace-keyed map projected per target exactly like `db.sql` / `db.orm`.
|
|
171
|
+
export type NamespacedEnums<TContract extends Contract> = {
|
|
172
|
+
readonly [Ns in keyof TContract['domain']['namespaces']]: NamespaceEnumAccessors<TContract, Ns>;
|
|
173
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { applySpecifierDefaultControlPolicy } from '../apply-specifier-default-control-policy';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { soleDomainNamespaceId, UNBOUND_DOMAIN_NAMESPACE_ID } from '../default-namespace';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type ResolvedDomainModel, resolveDomainModel } from '../resolve-domain-model';
|
package/src/exports/types.ts
CHANGED
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
Contract,
|
|
3
3
|
ContractExecutionSection,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
ContractModelDefinitions,
|
|
5
|
+
ContractValueObjectDefinitions,
|
|
6
6
|
} from '../contract-types';
|
|
7
7
|
export { DomainNamespaceResolutionError } from '../contract-validation-error';
|
|
8
|
+
export type { ControlPolicy } from '../control-policy';
|
|
9
|
+
export { effectiveControlPolicy } from '../control-policy';
|
|
8
10
|
export type { CrossReference } from '../cross-reference';
|
|
9
11
|
export { CrossReferenceSchema, crossRef } from '../cross-reference';
|
|
12
|
+
export { soleDomainNamespaceId } from '../default-namespace';
|
|
10
13
|
export type {
|
|
11
14
|
ApplicationDomain,
|
|
12
15
|
ApplicationDomainNamespace,
|
|
13
16
|
ContractWithDomain,
|
|
14
17
|
} from '../domain-envelope';
|
|
18
|
+
export { UNBOUND_DOMAIN_NAMESPACE_ID } from '../domain-envelope';
|
|
15
19
|
export {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
UNBOUND_DOMAIN_NAMESPACE_ID,
|
|
20
|
-
} from '../domain-envelope';
|
|
20
|
+
domainModelsAtDefaultNamespace,
|
|
21
|
+
domainValueObjectsAtDefaultNamespace,
|
|
22
|
+
} from '../domain-namespace-access';
|
|
21
23
|
export type {
|
|
22
24
|
ContractDiscriminator,
|
|
23
25
|
ContractEmbedRelation,
|
|
26
|
+
ContractEnum,
|
|
24
27
|
ContractField,
|
|
25
28
|
ContractFieldType,
|
|
29
|
+
ContractManyToManyRelation,
|
|
26
30
|
ContractModel,
|
|
27
31
|
ContractModelBase,
|
|
32
|
+
ContractNonJunctionRelation,
|
|
28
33
|
ContractReferenceRelation,
|
|
29
34
|
ContractRelation,
|
|
30
35
|
ContractRelationOn,
|
|
36
|
+
ContractRelationThrough,
|
|
31
37
|
ContractValueObject,
|
|
32
38
|
ContractVariantEntry,
|
|
33
39
|
EmbedRelationKeys,
|
|
@@ -39,6 +45,7 @@ export type {
|
|
|
39
45
|
} from '../domain-types';
|
|
40
46
|
export type { NamespaceId } from '../namespace-id';
|
|
41
47
|
export { asNamespaceId } from '../namespace-id';
|
|
48
|
+
export { type ResolvedDomainModel, resolveDomainModel } from '../resolve-domain-model';
|
|
42
49
|
export type {
|
|
43
50
|
$,
|
|
44
51
|
Brand,
|
|
@@ -58,6 +65,7 @@ export type {
|
|
|
58
65
|
GeneratedValueSpec,
|
|
59
66
|
JsonPrimitive,
|
|
60
67
|
JsonValue,
|
|
68
|
+
LedgerEntryRecord,
|
|
61
69
|
PlanMeta,
|
|
62
70
|
ProfileHashBase,
|
|
63
71
|
Source,
|
|
@@ -74,3 +82,4 @@ export {
|
|
|
74
82
|
isExecutionMutationDefaultValue,
|
|
75
83
|
profileHash,
|
|
76
84
|
} from '../types';
|
|
85
|
+
export type { ValueSetRef } from '../value-set-ref';
|
package/src/hashing.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { blindCast, castAs } from '@prisma-next/utils/casts';
|
|
2
3
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
4
|
import type { JsonObject } from '@prisma-next/utils/json';
|
|
4
5
|
import {
|
|
@@ -11,6 +12,33 @@ import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './type
|
|
|
11
12
|
|
|
12
13
|
const SCHEMA_VERSION = '1';
|
|
13
14
|
|
|
15
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Storage hashes fingerprint table/column layout, not which target pack emitted a
|
|
20
|
+
// namespace. Persisted contract.json carries namespace `kind` discriminators;
|
|
21
|
+
// authoring-time hashes never included them (IR `kind` is non-enumerable).
|
|
22
|
+
function omitNamespaceKindsForHash(storage: unknown): unknown {
|
|
23
|
+
if (!isPlainRecord(storage)) {
|
|
24
|
+
return storage;
|
|
25
|
+
}
|
|
26
|
+
const namespaces = storage['namespaces'];
|
|
27
|
+
if (!isPlainRecord(namespaces)) {
|
|
28
|
+
return storage;
|
|
29
|
+
}
|
|
30
|
+
const stripped: Record<string, unknown> = {};
|
|
31
|
+
for (const [nsId, ns] of Object.entries(namespaces)) {
|
|
32
|
+
if (isPlainRecord(ns)) {
|
|
33
|
+
const { kind: _kind, ...rest } = ns;
|
|
34
|
+
stripped[nsId] = rest;
|
|
35
|
+
} else {
|
|
36
|
+
stripped[nsId] = ns;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { ...storage, namespaces: stripped };
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
function sha256(content: string): string {
|
|
15
43
|
const hash = createHash('sha256');
|
|
16
44
|
hash.update(content);
|
|
@@ -24,28 +52,23 @@ type HashContractSection = Record<string, unknown> & {
|
|
|
24
52
|
|
|
25
53
|
function hashContract(section: HashContractSection): string {
|
|
26
54
|
const { shouldPreserveEmpty, sortStorage, ...sectionData } = section;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// `canonicalizeContract` only walks the storage / execution /
|
|
30
|
-
// capabilities slices, all of which are populated above, so the
|
|
31
|
-
// missing precise Contract typing on the other slots is
|
|
32
|
-
// immaterial for the hash result.
|
|
33
|
-
const contract = {
|
|
55
|
+
const storageForHash = omitNamespaceKindsForHash(sectionData['storage'] ?? {});
|
|
56
|
+
const contract = blindCast<Contract, 'hash-only partial contract for canonicalizeContract'>({
|
|
34
57
|
targetFamily: sectionData['targetFamily'],
|
|
35
58
|
target: sectionData['target'],
|
|
36
59
|
roots: {},
|
|
37
60
|
domain: { namespaces: {} },
|
|
38
|
-
storage: sectionData['storage'] ?? {},
|
|
39
61
|
execution: sectionData['execution'],
|
|
40
62
|
extensionPacks: {},
|
|
41
63
|
capabilities: sectionData['capabilities'] ?? {},
|
|
42
64
|
meta: {},
|
|
43
65
|
profileHash: '',
|
|
44
66
|
...sectionData,
|
|
45
|
-
|
|
67
|
+
storage: storageForHash,
|
|
68
|
+
});
|
|
46
69
|
return canonicalizeContract(contract, {
|
|
47
70
|
schemaVersion: SCHEMA_VERSION,
|
|
48
|
-
serializeContract: (c) => JSON.parse(JSON.stringify(c))
|
|
71
|
+
serializeContract: (c) => castAs<JsonObject>(JSON.parse(JSON.stringify(c))),
|
|
49
72
|
...ifDefined('shouldPreserveEmpty', shouldPreserveEmpty),
|
|
50
73
|
...ifDefined('sortStorage', sortStorage),
|
|
51
74
|
});
|
|
@@ -60,7 +83,9 @@ export type ComputeStorageHashArgs = {
|
|
|
60
83
|
};
|
|
61
84
|
|
|
62
85
|
export function computeStorageHash(args: ComputeStorageHashArgs): StorageHashBase<string> {
|
|
63
|
-
return sha256
|
|
86
|
+
return blindCast<StorageHashBase<string>, 'sha256 digest of canonicalized storage'>(
|
|
87
|
+
sha256(hashContract(args)),
|
|
88
|
+
);
|
|
64
89
|
}
|
|
65
90
|
|
|
66
91
|
export function computeExecutionHash(args: {
|
|
@@ -68,7 +93,9 @@ export function computeExecutionHash(args: {
|
|
|
68
93
|
targetFamily: string;
|
|
69
94
|
execution: Record<string, unknown>;
|
|
70
95
|
}): ExecutionHashBase<string> {
|
|
71
|
-
return sha256
|
|
96
|
+
return blindCast<ExecutionHashBase<string>, 'sha256 digest of canonicalized execution'>(
|
|
97
|
+
sha256(hashContract(args)),
|
|
98
|
+
);
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
export function computeProfileHash(args: {
|
|
@@ -76,5 +103,7 @@ export function computeProfileHash(args: {
|
|
|
76
103
|
targetFamily: string;
|
|
77
104
|
capabilities: Record<string, Record<string, boolean>>;
|
|
78
105
|
}): ProfileHashBase<string> {
|
|
79
|
-
return sha256
|
|
106
|
+
return blindCast<ProfileHashBase<string>, 'sha256 digest of canonicalized profile'>(
|
|
107
|
+
sha256(hashContract(args)),
|
|
108
|
+
);
|
|
80
109
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApplicationDomain } from './domain-envelope';
|
|
2
|
+
import type { ContractModelBase } from './domain-types';
|
|
3
|
+
|
|
4
|
+
export interface ResolvedDomainModel {
|
|
5
|
+
readonly namespaceId: string;
|
|
6
|
+
readonly model: ContractModelBase;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a bare domain model name to its namespace coordinate and model IR by
|
|
11
|
+
* scanning the contract's namespaces. For the single-namespace contracts in
|
|
12
|
+
* scope the scan is exact; cross-namespace bare-name collisions are selected
|
|
13
|
+
* explicitly (TML-2550).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveDomainModel(
|
|
16
|
+
domain: ApplicationDomain,
|
|
17
|
+
modelName: string,
|
|
18
|
+
): ResolvedDomainModel | undefined {
|
|
19
|
+
for (const namespaceId of Object.keys(domain.namespaces)) {
|
|
20
|
+
const model = domain.namespaces[namespaceId]?.models[modelName];
|
|
21
|
+
if (model !== undefined) {
|
|
22
|
+
return { namespaceId, model };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -57,11 +57,13 @@ export type StorageEntitySlot = Readonly<Record<string, unknown>>;
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Plain-data namespace entry in a storage block. Every hydrated contract
|
|
60
|
-
* carries at least `id` plus
|
|
61
|
-
* `
|
|
60
|
+
* carries at least `id` plus entity-kind slot maps under `entries`
|
|
61
|
+
* (`table`, `collection`, …). Foundation declares only this shape — no IR
|
|
62
|
+
* machinery.
|
|
62
63
|
*/
|
|
63
64
|
export interface StorageNamespace {
|
|
64
65
|
readonly id: string;
|
|
66
|
+
readonly entries: Readonly<Record<string, StorageEntitySlot>>;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -243,3 +245,17 @@ export interface ContractMarkerRecord {
|
|
|
243
245
|
readonly meta: Record<string, unknown>;
|
|
244
246
|
readonly invariants: readonly string[];
|
|
245
247
|
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* One applied migration edge from the per-space ledger journal.
|
|
251
|
+
* Returned by `readLedger` in append (apply) order.
|
|
252
|
+
*/
|
|
253
|
+
export interface LedgerEntryRecord {
|
|
254
|
+
readonly space: string;
|
|
255
|
+
readonly migrationName: string;
|
|
256
|
+
readonly migrationHash: string;
|
|
257
|
+
readonly from: string | null;
|
|
258
|
+
readonly to: string;
|
|
259
|
+
readonly appliedAt: Date;
|
|
260
|
+
readonly operationCount: number;
|
|
261
|
+
}
|
package/src/validate-domain.ts
CHANGED
|
@@ -140,6 +140,12 @@ function validateVariantsAndBases(modelIndex: ModelIndex, errors: string[]): voi
|
|
|
140
140
|
function validateRelationTargets(modelIndex: ModelIndex, errors: string[]): void {
|
|
141
141
|
for (const { namespaceId, name: modelName, model } of iterateIndexedModels(modelIndex)) {
|
|
142
142
|
for (const [relName, relation] of Object.entries(model.relations ?? {})) {
|
|
143
|
+
// Cross-space relations (relation.to.space is defined) target a model in a
|
|
144
|
+
// different contract space. The local domain index only contains this
|
|
145
|
+
// contract's own models, so the target is intentionally absent here.
|
|
146
|
+
// Existence of the target is verified by the aggregate verifier at
|
|
147
|
+
// deploy time (after the spaces are composed), not at parse time.
|
|
148
|
+
if (relation.to.space !== undefined) continue;
|
|
143
149
|
if (!lookupModel(modelIndex, relation.to)) {
|
|
144
150
|
errors.push(
|
|
145
151
|
`Relation "${relName}" on model "${namespaceId}:${modelName}" targets "${relation.to.namespace}:${relation.to.model}" which does not exist in domain.namespaces`,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Space-aware reference coordinate for a domain enum or storage value-set.
|
|
3
|
+
*
|
|
4
|
+
* `plane` names the contract plane the referenced entity lives in:
|
|
5
|
+
* - `'domain'` — the entity lives in the domain plane's `enum` slot.
|
|
6
|
+
* - `'storage'` — the entity lives in the storage plane's `valueSet` slot.
|
|
7
|
+
*
|
|
8
|
+
* `entityKind` names the source entity-kind:
|
|
9
|
+
* - `'enum'` — the referenced entity is a domain enum.
|
|
10
|
+
* - `'value-set'` — the referenced entity is a storage value-set.
|
|
11
|
+
*
|
|
12
|
+
* `namespaceId` admits the `UNBOUND_NAMESPACE_ID` (`__unbound__`) sentinel for
|
|
13
|
+
* single-namespace (unbound) references.
|
|
14
|
+
*
|
|
15
|
+
* Cross-space discrimination is presence-based: when `spaceId` is absent the
|
|
16
|
+
* reference is local (same contract-space); when `spaceId` is present the
|
|
17
|
+
* reference is cross-space. This mirrors the `ForeignKeyReference` carrier
|
|
18
|
+
* convention — no separate tag field — so local refs are JSON-minimal.
|
|
19
|
+
*/
|
|
20
|
+
export interface ValueSetRef {
|
|
21
|
+
readonly plane: 'domain' | 'storage';
|
|
22
|
+
readonly namespaceId: string;
|
|
23
|
+
readonly entityKind: 'enum' | 'value-set';
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly spaceId?: string;
|
|
26
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canonicalization-DFE0HJkI.d.mts","names":[],"sources":["../src/canonicalization.ts"],"mappings":";;;;;;AAcA;;;;;;KAAY,iBAAA,IAAqB,QAAA,EAAU,QAAA,KAAa,UAAU;;AAAA;AAUlE;;;;AAA6D;AAS7D;KATY,sBAAA,IAA0B,IAAuB;;;AASlB;AA6K3C;;;;KA7KY,WAAA,IAAe,OAAgB;AAAA,UA6K1B,2BAAA;EAAA,SACN,aAAA;EAAA;;;;;;;;AA0ByB;AAQpC;EAlCW,SAWA,iBAAA,EAAmB,iBAAA;;;;;;;WAOnB,mBAAA,GAAsB,sBAAA;EAiB/B;;;;;AAEO;AA0BT;EA5BE,SATS,WAAA,GAAc,WAAA;AAAA;;;;;;iBAQT,4BAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAAA,GACR,MAAA;AAAA,iBA0Ba,oBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAA2B"}
|