@prisma-next/sql-contract-ts 0.12.0-dev.57 → 0.12.0-dev.59
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/{build-contract-B1odoaHR.mjs → build-contract-C9dZNMRg.mjs} +57 -11
- package/dist/build-contract-C9dZNMRg.mjs.map +1 -0
- package/dist/config-types.mjs +1 -1
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +118 -2
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +70 -3
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +13 -13
- package/src/build-contract.ts +82 -6
- package/src/contract-builder.ts +4 -0
- package/src/contract-definition.ts +9 -0
- package/src/contract-dsl.ts +19 -1
- package/src/contract-lowering.ts +17 -0
- package/src/enum-type.ts +236 -0
- package/src/exports/contract-builder.ts +2 -0
- package/dist/build-contract-B1odoaHR.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.59",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.12.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.12.0-dev.59",
|
|
10
|
+
"@prisma-next/contract": "0.12.0-dev.59",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.12.0-dev.59",
|
|
12
|
+
"@prisma-next/framework-components": "0.12.0-dev.59",
|
|
13
|
+
"@prisma-next/sql-contract": "0.12.0-dev.59",
|
|
14
|
+
"@prisma-next/utils": "0.12.0-dev.59",
|
|
15
15
|
"arktype": "^2.2.0",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
20
|
+
"@prisma-next/test-utils": "0.12.0-dev.59",
|
|
21
|
+
"@prisma-next/tsconfig": "0.12.0-dev.59",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
|
-
"pg": "8.
|
|
24
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
25
|
-
"tsdown": "0.22.
|
|
23
|
+
"pg": "8.21.0",
|
|
24
|
+
"@prisma-next/tsdown": "0.12.0-dev.59",
|
|
25
|
+
"tsdown": "0.22.1",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
|
-
"vitest": "4.1.
|
|
27
|
+
"vitest": "4.1.7"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"typescript": ">=5.9"
|
package/src/build-contract.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ColumnDefault,
|
|
9
9
|
type ColumnDefaultLiteralInputValue,
|
|
10
10
|
type Contract,
|
|
11
|
+
type ContractEnum,
|
|
11
12
|
type ContractField,
|
|
12
13
|
type ContractModel,
|
|
13
14
|
type ContractRelation,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type ExecutionMutationDefault,
|
|
19
20
|
type JsonValue,
|
|
20
21
|
type StorageHashBase,
|
|
22
|
+
type ValueSetRef,
|
|
21
23
|
} from '@prisma-next/contract/types';
|
|
22
24
|
import { type CapabilityMatrix, mergeCapabilityMatrices } from '@prisma-next/contract-authoring';
|
|
23
25
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
StorageTable,
|
|
42
44
|
type StorageTableInput,
|
|
43
45
|
type StorageTypeInstance,
|
|
46
|
+
type StorageValueSetInput,
|
|
44
47
|
toStorageTypeInstance,
|
|
45
48
|
} from '@prisma-next/sql-contract/types';
|
|
46
49
|
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
@@ -166,6 +169,7 @@ function resolveModelNamespaceId(
|
|
|
166
169
|
|
|
167
170
|
function buildStorageColumn(
|
|
168
171
|
field: FieldNode | ValueObjectFieldNode,
|
|
172
|
+
storageValueSetRef: ValueSetRef | undefined,
|
|
169
173
|
codecLookup?: CodecLookup,
|
|
170
174
|
): StorageColumn {
|
|
171
175
|
if (isValueObjectField(field)) {
|
|
@@ -203,12 +207,14 @@ function buildStorageColumn(
|
|
|
203
207
|
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
204
208
|
...ifDefined('default', encodedDefault),
|
|
205
209
|
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
210
|
+
...ifDefined('valueSet', storageValueSetRef),
|
|
206
211
|
};
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
function buildDomainField(
|
|
210
215
|
field: FieldNode | ValueObjectFieldNode,
|
|
211
216
|
column: StorageColumn,
|
|
217
|
+
domainValueSetRef: ValueSetRef | undefined,
|
|
212
218
|
): ContractField {
|
|
213
219
|
if (isValueObjectField(field)) {
|
|
214
220
|
return {
|
|
@@ -226,6 +232,7 @@ function buildDomainField(
|
|
|
226
232
|
},
|
|
227
233
|
nullable: column.nullable,
|
|
228
234
|
...(field.many ? { many: true } : {}),
|
|
235
|
+
...ifDefined('valueSet', domainValueSetRef),
|
|
229
236
|
};
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -369,11 +376,34 @@ export function buildSqlContractFromDefinition(
|
|
|
369
376
|
}
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
const
|
|
379
|
+
const enumHandle = !isValueObjectField(field) ? field.enumTypeHandle : undefined;
|
|
380
|
+
// Authored enums are always registered under the contract's defaultNamespaceId
|
|
381
|
+
// (see the enum registration loop below), so refs must point there regardless
|
|
382
|
+
// of which namespace the consuming model lives in.
|
|
383
|
+
const storageValueSetRef: ValueSetRef | undefined =
|
|
384
|
+
enumHandle !== undefined
|
|
385
|
+
? {
|
|
386
|
+
plane: 'storage',
|
|
387
|
+
entityKind: 'value-set',
|
|
388
|
+
namespaceId: defaultNamespaceId,
|
|
389
|
+
name: enumHandle.enumName,
|
|
390
|
+
}
|
|
391
|
+
: undefined;
|
|
392
|
+
const domainValueSetRef: ValueSetRef | undefined =
|
|
393
|
+
enumHandle !== undefined
|
|
394
|
+
? {
|
|
395
|
+
plane: 'domain',
|
|
396
|
+
entityKind: 'enum',
|
|
397
|
+
namespaceId: defaultNamespaceId,
|
|
398
|
+
name: enumHandle.enumName,
|
|
399
|
+
}
|
|
400
|
+
: undefined;
|
|
401
|
+
|
|
402
|
+
const column = buildStorageColumn(field, storageValueSetRef, codecLookup);
|
|
373
403
|
columns[field.columnName] = column;
|
|
374
404
|
fieldToColumn[field.fieldName] = field.columnName;
|
|
375
405
|
|
|
376
|
-
domainFields[field.fieldName] = buildDomainField(field, column);
|
|
406
|
+
domainFields[field.fieldName] = buildDomainField(field, column, domainValueSetRef);
|
|
377
407
|
|
|
378
408
|
if (isValueObjectField(field)) {
|
|
379
409
|
domainFieldRefs[field.fieldName] = {
|
|
@@ -585,6 +615,39 @@ export function buildSqlContractFromDefinition(
|
|
|
585
615
|
for (const id of Object.keys(namespaceEnumTypesById)) {
|
|
586
616
|
namespaceCoordinateIds.add(id);
|
|
587
617
|
}
|
|
618
|
+
|
|
619
|
+
// Build per-namespace registries for `enumType()` handles.
|
|
620
|
+
// All authored enums target the contract's default namespace.
|
|
621
|
+
const domainEnumsByNs: Record<string, Record<string, ContractEnum>> = {};
|
|
622
|
+
const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
|
|
623
|
+
for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
|
|
624
|
+
if (enumName !== handle.enumName) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const nsId = defaultNamespaceId;
|
|
630
|
+
let domainSlot = domainEnumsByNs[nsId];
|
|
631
|
+
if (domainSlot === undefined) {
|
|
632
|
+
domainSlot = {};
|
|
633
|
+
domainEnumsByNs[nsId] = domainSlot;
|
|
634
|
+
}
|
|
635
|
+
domainSlot[enumName] = {
|
|
636
|
+
codecId: handle.codecId,
|
|
637
|
+
members: handle.enumMembers,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
let storageSlot = storageValueSetsByNs[nsId];
|
|
641
|
+
if (storageSlot === undefined) {
|
|
642
|
+
storageSlot = {};
|
|
643
|
+
storageValueSetsByNs[nsId] = storageSlot;
|
|
644
|
+
}
|
|
645
|
+
storageSlot[enumName] = {
|
|
646
|
+
kind: 'value-set',
|
|
647
|
+
values: handle.values,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
588
651
|
const { createNamespace } = definition;
|
|
589
652
|
const namespaces = blindCast<
|
|
590
653
|
SqlStorageInput['namespaces'],
|
|
@@ -593,10 +656,14 @@ export function buildSqlContractFromDefinition(
|
|
|
593
656
|
Object.fromEntries(
|
|
594
657
|
[...namespaceCoordinateIds].sort().map((id) => {
|
|
595
658
|
const enumTypes = namespaceEnumTypesById[id];
|
|
659
|
+
const valueSetEntries = storageValueSetsByNs[id];
|
|
596
660
|
const nsInput: SqlNamespaceTablesInput = {
|
|
597
661
|
id,
|
|
598
662
|
entries: {
|
|
599
663
|
table: tablesByNamespace[id] ?? {},
|
|
664
|
+
...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
|
|
665
|
+
? { valueSet: valueSetEntries }
|
|
666
|
+
: {}),
|
|
600
667
|
},
|
|
601
668
|
};
|
|
602
669
|
return [
|
|
@@ -711,13 +778,22 @@ export function buildSqlContractFromDefinition(
|
|
|
711
778
|
if (valueObjects !== undefined) {
|
|
712
779
|
domainNamespaceIds.add(defaultNamespaceId);
|
|
713
780
|
}
|
|
781
|
+
for (const nsId of Object.keys(domainEnumsByNs)) {
|
|
782
|
+
domainNamespaceIds.add(nsId);
|
|
783
|
+
}
|
|
714
784
|
const domainNamespaces = Object.fromEntries(
|
|
715
785
|
[...domainNamespaceIds].sort().map((namespaceId) => {
|
|
716
786
|
const modelsInNs = modelsByNamespace[namespaceId] ?? {};
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
787
|
+
const enumsInNs = domainEnumsByNs[namespaceId];
|
|
788
|
+
const namespaceSlice = {
|
|
789
|
+
models: modelsInNs,
|
|
790
|
+
...(namespaceId === defaultNamespaceId && valueObjects !== undefined
|
|
791
|
+
? { valueObjects }
|
|
792
|
+
: {}),
|
|
793
|
+
...(enumsInNs !== undefined && Object.keys(enumsInNs).length > 0
|
|
794
|
+
? { enum: enumsInNs }
|
|
795
|
+
: {}),
|
|
796
|
+
};
|
|
721
797
|
return [namespaceId, namespaceSlice];
|
|
722
798
|
}),
|
|
723
799
|
);
|
package/src/contract-builder.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from './contract-dsl';
|
|
35
35
|
import { buildContractDefinition } from './contract-lowering';
|
|
36
36
|
import type { SqlContractResult } from './contract-types';
|
|
37
|
+
import type { EnumTypeHandle } from './enum-type';
|
|
37
38
|
|
|
38
39
|
export { buildSqlContractFromDefinition } from './build-contract';
|
|
39
40
|
|
|
@@ -73,6 +74,7 @@ type ContractDefinition<
|
|
|
73
74
|
readonly types?: Types;
|
|
74
75
|
readonly models?: Models;
|
|
75
76
|
readonly codecLookup?: CodecLookup;
|
|
77
|
+
readonly enums?: Record<string, EnumTypeHandle>;
|
|
76
78
|
};
|
|
77
79
|
|
|
78
80
|
type ContractScaffold<
|
|
@@ -96,6 +98,7 @@ type ContractScaffold<
|
|
|
96
98
|
readonly types?: never;
|
|
97
99
|
readonly models?: never;
|
|
98
100
|
readonly codecLookup?: CodecLookup;
|
|
101
|
+
readonly enums?: Record<string, EnumTypeHandle>;
|
|
99
102
|
};
|
|
100
103
|
|
|
101
104
|
type ContractFactory<
|
|
@@ -317,6 +320,7 @@ type BoundDefinitionInput<
|
|
|
317
320
|
readonly types?: Types;
|
|
318
321
|
readonly models?: Models;
|
|
319
322
|
readonly codecLookup?: CodecLookup;
|
|
323
|
+
readonly enums?: Record<string, EnumTypeHandle>;
|
|
320
324
|
};
|
|
321
325
|
|
|
322
326
|
// Merges a bound input with the pre-bound family/target to produce a full ContractDefinition.
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SqlNamespaceTablesInput,
|
|
14
14
|
StorageTypeInstance,
|
|
15
15
|
} from '@prisma-next/sql-contract/types';
|
|
16
|
+
import type { EnumTypeHandle } from './enum-type';
|
|
16
17
|
|
|
17
18
|
export type { ExecutionMutationDefaultPhases };
|
|
18
19
|
|
|
@@ -24,6 +25,8 @@ export interface FieldNode {
|
|
|
24
25
|
readonly default?: ColumnDefault;
|
|
25
26
|
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
26
27
|
readonly many?: boolean;
|
|
28
|
+
/** Present when the field was authored with `field.namedType(enumHandle)`. */
|
|
29
|
+
readonly enumTypeHandle?: EnumTypeHandle;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface PrimaryKeyNode {
|
|
@@ -163,4 +166,10 @@ export interface ContractDefinition {
|
|
|
163
166
|
) => Namespace;
|
|
164
167
|
readonly models: readonly ModelNode[];
|
|
165
168
|
readonly valueObjects?: readonly ValueObjectNode[];
|
|
169
|
+
/**
|
|
170
|
+
* Domain enum handles authored via `enumType()`. Each entry lowers to a
|
|
171
|
+
* domain `enum` entry and a storage `valueSet` entry in the contract's
|
|
172
|
+
* default namespace.
|
|
173
|
+
*/
|
|
174
|
+
readonly enums?: Record<string, EnumTypeHandle>;
|
|
166
175
|
}
|
package/src/contract-dsl.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
} from '@prisma-next/sql-contract/types';
|
|
24
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
25
25
|
import type { NamedConstraintSpec } from './authoring-type-utils';
|
|
26
|
+
import type { EnumTypeHandle } from './enum-type';
|
|
27
|
+
import { isEnumTypeHandle } from './enum-type';
|
|
26
28
|
|
|
27
29
|
export type NamingStrategy = 'identity' | 'snake_case';
|
|
28
30
|
|
|
@@ -31,7 +33,7 @@ export type NamingConfig = {
|
|
|
31
33
|
readonly columns?: NamingStrategy;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
type NamedStorageTypeRef = string | StorageTypeInstance | PostgresEnumStorageEntry;
|
|
36
|
+
type NamedStorageTypeRef = string | StorageTypeInstance | PostgresEnumStorageEntry | EnumTypeHandle;
|
|
35
37
|
|
|
36
38
|
type NamedConstraintNameSpec<Name extends string = string> = {
|
|
37
39
|
readonly name: Name;
|
|
@@ -358,9 +360,19 @@ function namedTypeField<TypeRef extends StorageTypeInstance>(
|
|
|
358
360
|
function namedTypeField<TypeRef extends PostgresEnumStorageEntry>(
|
|
359
361
|
typeRef: TypeRef,
|
|
360
362
|
): ScalarFieldBuilder<ScalarFieldState<string, TypeRef, false, undefined>>;
|
|
363
|
+
function namedTypeField<Handle extends EnumTypeHandle>(
|
|
364
|
+
typeRef: Handle,
|
|
365
|
+
): ScalarFieldBuilder<ScalarFieldState<Handle['codecId'], Handle, false, undefined>>;
|
|
361
366
|
function namedTypeField(
|
|
362
367
|
typeRef: NamedStorageTypeRef,
|
|
363
368
|
): ScalarFieldBuilder<ScalarFieldState<string, NamedStorageTypeRef, false, undefined>> {
|
|
369
|
+
if (isEnumTypeHandle(typeRef)) {
|
|
370
|
+
return new ScalarFieldBuilder({
|
|
371
|
+
kind: 'scalar',
|
|
372
|
+
typeRef,
|
|
373
|
+
nullable: false,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
364
376
|
return new ScalarFieldBuilder({
|
|
365
377
|
kind: 'scalar',
|
|
366
378
|
typeRef,
|
|
@@ -1267,6 +1279,12 @@ export type ContractInput<
|
|
|
1267
1279
|
readonly types?: Types;
|
|
1268
1280
|
readonly models?: Models;
|
|
1269
1281
|
readonly codecLookup?: CodecLookup;
|
|
1282
|
+
/**
|
|
1283
|
+
* Domain enum handles authored via `enumType()`. Each handle lowers to a
|
|
1284
|
+
* domain `enum` entry and a storage `valueSet` entry in the target's
|
|
1285
|
+
* default namespace. Fields reference the enum via `field.namedType(handle)`.
|
|
1286
|
+
*/
|
|
1287
|
+
readonly enums?: Record<string, import('./enum-type').EnumTypeHandle>;
|
|
1270
1288
|
};
|
|
1271
1289
|
|
|
1272
1290
|
export function model<
|
package/src/contract-lowering.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
emitTypedCrossModelFallbackWarnings,
|
|
36
36
|
emitTypedNamedTypeFallbackWarnings,
|
|
37
37
|
} from './contract-warnings';
|
|
38
|
+
import { isEnumTypeHandle } from './enum-type';
|
|
38
39
|
|
|
39
40
|
type RuntimeModel = ContractModelBuilder<
|
|
40
41
|
string | undefined,
|
|
@@ -84,6 +85,13 @@ function resolveFieldDescriptor(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
if ('typeRef' in fieldState && fieldState.typeRef) {
|
|
88
|
+
if (isEnumTypeHandle(fieldState.typeRef)) {
|
|
89
|
+
return {
|
|
90
|
+
codecId: fieldState.typeRef.codecId,
|
|
91
|
+
nativeType: fieldState.typeRef.nativeType,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
const typeRef =
|
|
88
96
|
typeof fieldState.typeRef === 'string'
|
|
89
97
|
? fieldState.typeRef
|
|
@@ -567,6 +575,11 @@ function resolveModelNode(
|
|
|
567
575
|
throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
|
|
568
576
|
}
|
|
569
577
|
|
|
578
|
+
const enumHandle =
|
|
579
|
+
'typeRef' in fieldState && isEnumTypeHandle(fieldState.typeRef)
|
|
580
|
+
? fieldState.typeRef
|
|
581
|
+
: undefined;
|
|
582
|
+
|
|
570
583
|
fields.push({
|
|
571
584
|
fieldName,
|
|
572
585
|
columnName,
|
|
@@ -574,6 +587,7 @@ function resolveModelNode(
|
|
|
574
587
|
nullable: fieldState.nullable,
|
|
575
588
|
...(fieldState.default ? { default: fieldState.default } : {}),
|
|
576
589
|
...(fieldState.executionDefaults ? { executionDefaults: fieldState.executionDefaults } : {}),
|
|
590
|
+
...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}),
|
|
577
591
|
});
|
|
578
592
|
}
|
|
579
593
|
|
|
@@ -717,6 +731,9 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
|
|
|
717
731
|
: {}),
|
|
718
732
|
...(definition.namespaces ? { namespaces: definition.namespaces } : {}),
|
|
719
733
|
...(definition.createNamespace ? { createNamespace: definition.createNamespace } : {}),
|
|
734
|
+
...(definition.enums && Object.keys(definition.enums).length > 0
|
|
735
|
+
? { enums: definition.enums }
|
|
736
|
+
: {}),
|
|
720
737
|
models,
|
|
721
738
|
};
|
|
722
739
|
}
|
package/src/enum-type.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// EnumMember — a single member declaration with literal type preservation
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single enum member produced by `member()`. The `Name` and `Value` generics
|
|
10
|
+
* are preserved as literal types so `enumType()` can carry the ordered value
|
|
11
|
+
* tuple in its return type.
|
|
12
|
+
*/
|
|
13
|
+
export interface EnumMember<Name extends string, Value extends string> {
|
|
14
|
+
readonly name: Name;
|
|
15
|
+
readonly value: Value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Declare an enum member. The `value` defaults to `name` when omitted.
|
|
20
|
+
* Both generics are preserved as literals so downstream `enumType` can
|
|
21
|
+
* carry the ordered value tuple in its type.
|
|
22
|
+
*/
|
|
23
|
+
export function member<const Name extends string>(name: Name): EnumMember<Name, Name>;
|
|
24
|
+
export function member<const Name extends string, const Value extends string>(
|
|
25
|
+
name: Name,
|
|
26
|
+
value: Value,
|
|
27
|
+
): EnumMember<Name, Value>;
|
|
28
|
+
export function member<const Name extends string, const Value extends string = Name>(
|
|
29
|
+
name: Name,
|
|
30
|
+
value?: Value,
|
|
31
|
+
): EnumMember<Name, Value> {
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
value: blindCast<
|
|
35
|
+
Value,
|
|
36
|
+
'overload signatures enforce Value=Name when value is omitted; default generic Value=Name makes this safe'
|
|
37
|
+
>(value ?? name),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Internal types for inferring the literal tuple from the members spread
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
type MembersToValues<Members extends readonly EnumMember<string, string>[]> = {
|
|
46
|
+
readonly [K in keyof Members]: Members[K] extends EnumMember<string, infer V> ? V : never;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MembersToNames<Members extends readonly EnumMember<string, string>[]> = {
|
|
50
|
+
readonly [K in keyof Members]: Members[K] extends EnumMember<infer N, string> ? N : never;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type MembersAccessorMap<Members extends readonly EnumMember<string, string>[]> = {
|
|
54
|
+
readonly [M in Members[number] as M['name']]: M['value'];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// EnumTypeHandle — the authoring handle returned by enumType()
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal brand that identifies an EnumTypeHandle in the lowering pipeline.
|
|
63
|
+
* Not exported — callers only interact with `EnumTypeHandle`.
|
|
64
|
+
*/
|
|
65
|
+
export const ENUM_TYPE_HANDLE_BRAND = Symbol('EnumTypeHandle');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Authoring handle returned by `enumType()`. Carries:
|
|
69
|
+
*
|
|
70
|
+
* - The ordered literal value tuple (`.values`) and name tuple (`.names`)
|
|
71
|
+
* so downstream type-tests can assert literal preservation.
|
|
72
|
+
* - A namespaced member accessor map (`.members`) to avoid collisions with
|
|
73
|
+
* `.values` / `.has` / `.nameOf` / `.ordinalOf`.
|
|
74
|
+
* - Runtime helpers `.has()`, `.nameOf()`, `.ordinalOf()`.
|
|
75
|
+
* - Internal metadata (`enumName`, `codecId`, `nativeType`,
|
|
76
|
+
* `enumMembers`) for the lowering pipeline.
|
|
77
|
+
*
|
|
78
|
+
* The type is generic over the ordered value tuple so callers that assign
|
|
79
|
+
* `const Role = enumType(...)` retain the literal tuple on `.values`.
|
|
80
|
+
*/
|
|
81
|
+
export interface EnumTypeHandle<
|
|
82
|
+
Name extends string = string,
|
|
83
|
+
Values extends readonly string[] = readonly string[],
|
|
84
|
+
Names extends readonly string[] = readonly string[],
|
|
85
|
+
MembersMap extends Record<string, string> = Record<string, string>,
|
|
86
|
+
> {
|
|
87
|
+
/** Internal brand for lowering-pipeline detection. */
|
|
88
|
+
readonly [ENUM_TYPE_HANDLE_BRAND]: true;
|
|
89
|
+
|
|
90
|
+
/** The enum's declared name (used as the key in domain `enum` / storage `valueSet`). */
|
|
91
|
+
readonly enumName: Name;
|
|
92
|
+
|
|
93
|
+
/** codecId from the codec passed to `enumType`. */
|
|
94
|
+
readonly codecId: string;
|
|
95
|
+
|
|
96
|
+
/** nativeType from the codec passed to `enumType`. */
|
|
97
|
+
readonly nativeType: string;
|
|
98
|
+
|
|
99
|
+
/** Ordered member list for lowering (name + value pairs). */
|
|
100
|
+
readonly enumMembers: readonly { readonly name: string; readonly value: string }[];
|
|
101
|
+
|
|
102
|
+
/** Ordered literal value tuple. Declaration order is preserved. */
|
|
103
|
+
readonly values: Values;
|
|
104
|
+
|
|
105
|
+
/** Ordered literal name tuple. Declaration order is preserved. */
|
|
106
|
+
readonly names: Names;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Namespaced accessor map: `Role.members.User === 'user'`.
|
|
110
|
+
* Namespaced under `.members` to avoid collisions with `.values` / `.has`.
|
|
111
|
+
*/
|
|
112
|
+
readonly members: MembersMap;
|
|
113
|
+
|
|
114
|
+
/** Returns `true` if `v` is a declared member value. */
|
|
115
|
+
has(v: string): boolean;
|
|
116
|
+
|
|
117
|
+
/** Returns the member name for a value, or `undefined` if not found. */
|
|
118
|
+
nameOf(v: string): string | undefined;
|
|
119
|
+
|
|
120
|
+
/** Returns the zero-based declaration index of a value, or `-1` if not found. */
|
|
121
|
+
ordinalOf(v: string): number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// enumType()
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Declare a domain enum for use in TS-authoring contracts.
|
|
130
|
+
*
|
|
131
|
+
* - The codec is an explicit required argument — the `codecId` and
|
|
132
|
+
* `nativeType` are taken from the passed `ColumnTypeDescriptor` (e.g.
|
|
133
|
+
* `{ codecId: 'pg/text@1', nativeType: 'text' }` from a field preset
|
|
134
|
+
* output or a direct inline object).
|
|
135
|
+
* - `const` generics on the members spread preserve the ordered literal
|
|
136
|
+
* value tuple so `Role.values` is `readonly ['user','admin']`, not
|
|
137
|
+
* `string[]`.
|
|
138
|
+
* - Well-formedness assertions at construction: non-empty member list;
|
|
139
|
+
* unique names; unique values.
|
|
140
|
+
*
|
|
141
|
+
* The returned handle wires into `field.namedType(handle)` to set
|
|
142
|
+
* `valueSet` refs on both the domain field and the storage column.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const Role = enumType('Role', { codecId: 'pg/text@1', nativeType: 'text' },
|
|
147
|
+
* member('User', 'user'),
|
|
148
|
+
* member('Admin', 'admin'),
|
|
149
|
+
* );
|
|
150
|
+
* // Role.values → readonly ['user', 'admin']
|
|
151
|
+
* // Role.members.User → 'user'
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function enumType<
|
|
155
|
+
const Name extends string,
|
|
156
|
+
const Codec extends Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
157
|
+
const Members extends readonly [EnumMember<string, string>, ...EnumMember<string, string>[]],
|
|
158
|
+
>(
|
|
159
|
+
name: Name,
|
|
160
|
+
codec: Codec,
|
|
161
|
+
...members: Members
|
|
162
|
+
): EnumTypeHandle<
|
|
163
|
+
Name,
|
|
164
|
+
MembersToValues<[...Members]>,
|
|
165
|
+
MembersToNames<[...Members]>,
|
|
166
|
+
MembersAccessorMap<[...Members]>
|
|
167
|
+
>;
|
|
168
|
+
export function enumType(
|
|
169
|
+
name: string,
|
|
170
|
+
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
171
|
+
...members: EnumMember<string, string>[]
|
|
172
|
+
): EnumTypeHandle;
|
|
173
|
+
export function enumType(
|
|
174
|
+
name: string,
|
|
175
|
+
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
176
|
+
...members: EnumMember<string, string>[]
|
|
177
|
+
): EnumTypeHandle {
|
|
178
|
+
if (members.length === 0) {
|
|
179
|
+
throw new Error(`enumType("${name}"): must have at least one member.`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const seenNames = new Set<string>();
|
|
183
|
+
const seenValues = new Set<string>();
|
|
184
|
+
for (const m of members) {
|
|
185
|
+
if (seenNames.has(m.name)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`enumType("${name}"): duplicate member name "${m.name}". Member names must be unique.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
seenNames.add(m.name);
|
|
191
|
+
|
|
192
|
+
if (seenValues.has(m.value)) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`enumType("${name}"): duplicate member value "${m.value}". Member values must be unique.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
seenValues.add(m.value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const values = Object.freeze(members.map((m) => m.value));
|
|
201
|
+
const names = Object.freeze(members.map((m) => m.name));
|
|
202
|
+
const enumMembers = Object.freeze(members.map((m) => ({ name: m.name, value: m.value })));
|
|
203
|
+
|
|
204
|
+
const membersAccessor = Object.freeze(Object.fromEntries(members.map((m) => [m.name, m.value])));
|
|
205
|
+
|
|
206
|
+
const valueSet = new Set(values);
|
|
207
|
+
const valueToName = new Map(members.map((m) => [m.value, m.name]));
|
|
208
|
+
const valueToOrdinal = new Map(values.map((v, i) => [v, i]));
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
[ENUM_TYPE_HANDLE_BRAND]: true,
|
|
212
|
+
enumName: name,
|
|
213
|
+
codecId: codec.codecId,
|
|
214
|
+
nativeType: codec.nativeType,
|
|
215
|
+
enumMembers,
|
|
216
|
+
values,
|
|
217
|
+
names,
|
|
218
|
+
members: membersAccessor,
|
|
219
|
+
has: (v: string) => valueSet.has(v),
|
|
220
|
+
nameOf: (v: string) => valueToName.get(v),
|
|
221
|
+
ordinalOf: (v: string) => valueToOrdinal.get(v) ?? -1,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Returns true when the value is an `EnumTypeHandle` produced by
|
|
227
|
+
* `enumType()`. Used in the lowering pipeline to detect enum handles
|
|
228
|
+
* in field state without importing the BRAND symbol at every call site.
|
|
229
|
+
*/
|
|
230
|
+
export function isEnumTypeHandle(value: unknown): value is EnumTypeHandle {
|
|
231
|
+
return (
|
|
232
|
+
typeof value === 'object' &&
|
|
233
|
+
value !== null &&
|
|
234
|
+
Reflect.get(value, ENUM_TYPE_HANDLE_BRAND) === true
|
|
235
|
+
);
|
|
236
|
+
}
|