@prisma-next/sql-contract-ts 0.3.0-dev.133 → 0.3.0-dev.135
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/README.md +141 -4
- package/dist/contract-builder.d.mts +732 -15
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +1254 -131
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +7 -7
- package/src/authoring-helper-runtime.ts +139 -0
- package/src/authoring-type-utils.ts +168 -0
- package/src/composed-authoring-helpers.ts +254 -0
- package/src/contract-builder.ts +236 -335
- package/src/contract-ir-builder.ts +475 -0
- package/src/contract.ts +6 -0
- package/src/exports/contract-builder.ts +24 -2
- package/src/semantic-contract.ts +86 -0
- package/src/staged-contract-dsl.ts +1490 -0
- package/src/staged-contract-lowering.ts +705 -0
- package/src/staged-contract-types.ts +494 -0
- package/src/staged-contract-warnings.ts +245 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoringFieldNamespace,
|
|
3
|
+
AuthoringFieldPresetDescriptor,
|
|
4
|
+
AuthoringTypeNamespace,
|
|
5
|
+
} from '@prisma-next/contract/framework-components';
|
|
6
|
+
import {
|
|
7
|
+
instantiateAuthoringTypeConstructor,
|
|
8
|
+
isAuthoringFieldPresetDescriptor,
|
|
9
|
+
isAuthoringTypeConstructorDescriptor,
|
|
10
|
+
validateAuthoringHelperArguments,
|
|
11
|
+
} from '@prisma-next/contract/framework-components';
|
|
12
|
+
import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
13
|
+
|
|
14
|
+
export type RuntimeNamedConstraintSpec = {
|
|
15
|
+
readonly name?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function isNamedConstraintOptionsLike(value: unknown): value is RuntimeNamedConstraintSpec {
|
|
19
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
24
|
+
if (keys.some((key) => key !== 'name')) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const name = (value as { readonly name?: unknown }).name;
|
|
29
|
+
return name === undefined || typeof name === 'string';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const blockedSegments = new Set(['__proto__', 'constructor', 'prototype']);
|
|
33
|
+
|
|
34
|
+
function assertSafeHelperKey(key: string, path: readonly string[]): void {
|
|
35
|
+
if (blockedSegments.has(key)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid authoring helper "${[...path, key].join('.')}". Helper path segments must not use "${key}".`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createTypeHelpersFromNamespace(
|
|
43
|
+
namespace: AuthoringTypeNamespace,
|
|
44
|
+
path: readonly string[] = [],
|
|
45
|
+
): Record<string, unknown> {
|
|
46
|
+
const helpers: Record<string, unknown> = {};
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
49
|
+
assertSafeHelperKey(key, path);
|
|
50
|
+
const currentPath = [...path, key];
|
|
51
|
+
|
|
52
|
+
if (isAuthoringTypeConstructorDescriptor(value)) {
|
|
53
|
+
const helperPath = currentPath.join('.');
|
|
54
|
+
helpers[key] = (...args: readonly unknown[]) => {
|
|
55
|
+
validateAuthoringHelperArguments(helperPath, value.args, args);
|
|
56
|
+
return instantiateAuthoringTypeConstructor(value, args) as StorageTypeInstance;
|
|
57
|
+
};
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
helpers[key] = createTypeHelpersFromNamespace(value as AuthoringTypeNamespace, currentPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return helpers;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createFieldPresetHelper<Result>(options: {
|
|
68
|
+
readonly helperPath: string;
|
|
69
|
+
readonly descriptor: AuthoringFieldPresetDescriptor;
|
|
70
|
+
readonly build: (options: {
|
|
71
|
+
readonly args: readonly unknown[];
|
|
72
|
+
readonly namedConstraintOptions?: RuntimeNamedConstraintSpec;
|
|
73
|
+
}) => Result;
|
|
74
|
+
}): (...rawArgs: readonly unknown[]) => Result {
|
|
75
|
+
return (...rawArgs: readonly unknown[]) => {
|
|
76
|
+
const acceptsNamedConstraintOptions =
|
|
77
|
+
options.descriptor.output.id === true || options.descriptor.output.unique === true;
|
|
78
|
+
const declaredArguments = options.descriptor.args ?? [];
|
|
79
|
+
|
|
80
|
+
if (acceptsNamedConstraintOptions && rawArgs.length > declaredArguments.length + 1) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${options.helperPath} expects at most ${declaredArguments.length + 1} argument(s), received ${rawArgs.length}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let args = rawArgs;
|
|
87
|
+
let namedConstraintOptions: RuntimeNamedConstraintSpec | undefined;
|
|
88
|
+
|
|
89
|
+
if (acceptsNamedConstraintOptions && rawArgs.length === declaredArguments.length + 1) {
|
|
90
|
+
const maybeNamedConstraintOptions = rawArgs.at(-1);
|
|
91
|
+
if (!isNamedConstraintOptionsLike(maybeNamedConstraintOptions)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${options.helperPath} accepts an optional trailing { name?: string } constraint options object`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
namedConstraintOptions = maybeNamedConstraintOptions;
|
|
97
|
+
args = rawArgs.slice(0, -1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
validateAuthoringHelperArguments(options.helperPath, options.descriptor.args, args);
|
|
101
|
+
|
|
102
|
+
return options.build({
|
|
103
|
+
args,
|
|
104
|
+
...(namedConstraintOptions ? { namedConstraintOptions } : {}),
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createFieldHelpersFromNamespace(
|
|
110
|
+
namespace: AuthoringFieldNamespace,
|
|
111
|
+
createLeafHelper: (options: {
|
|
112
|
+
readonly helperPath: string;
|
|
113
|
+
readonly descriptor: AuthoringFieldPresetDescriptor;
|
|
114
|
+
}) => (...rawArgs: readonly unknown[]) => unknown,
|
|
115
|
+
path: readonly string[] = [],
|
|
116
|
+
): Record<string, unknown> {
|
|
117
|
+
const helpers: Record<string, unknown> = {};
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
120
|
+
assertSafeHelperKey(key, path);
|
|
121
|
+
const currentPath = [...path, key];
|
|
122
|
+
|
|
123
|
+
if (isAuthoringFieldPresetDescriptor(value)) {
|
|
124
|
+
helpers[key] = createLeafHelper({
|
|
125
|
+
helperPath: currentPath.join('.'),
|
|
126
|
+
descriptor: value,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
helpers[key] = createFieldHelpersFromNamespace(
|
|
132
|
+
value as AuthoringFieldNamespace,
|
|
133
|
+
createLeafHelper,
|
|
134
|
+
currentPath,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return helpers;
|
|
139
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoringArgumentDescriptor,
|
|
3
|
+
AuthoringFieldPresetDescriptor,
|
|
4
|
+
} from '@prisma-next/contract/framework-components';
|
|
5
|
+
import type { ScalarFieldBuilder, ScalarFieldState } from './staged-contract-dsl';
|
|
6
|
+
|
|
7
|
+
export type UnionToIntersection<U> = (U extends unknown ? (value: U) => void : never) extends (
|
|
8
|
+
value: infer I,
|
|
9
|
+
) => void
|
|
10
|
+
? I
|
|
11
|
+
: never;
|
|
12
|
+
|
|
13
|
+
export type NamedConstraintSpec<Name extends string | undefined = string | undefined> = {
|
|
14
|
+
readonly name?: Name;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type NamedConstraintState<
|
|
18
|
+
Enabled extends boolean,
|
|
19
|
+
Name extends string | undefined = undefined,
|
|
20
|
+
> = Enabled extends true ? NamedConstraintSpec<Name> : undefined;
|
|
21
|
+
|
|
22
|
+
export type OptionalObjectArgumentKeys<
|
|
23
|
+
Properties extends Record<string, AuthoringArgumentDescriptor>,
|
|
24
|
+
> = {
|
|
25
|
+
readonly [K in keyof Properties]: Properties[K] extends { readonly optional: true } ? K : never;
|
|
26
|
+
}[keyof Properties];
|
|
27
|
+
|
|
28
|
+
export type ObjectArgumentType<Properties extends Record<string, AuthoringArgumentDescriptor>> = {
|
|
29
|
+
readonly [K in Exclude<
|
|
30
|
+
keyof Properties,
|
|
31
|
+
OptionalObjectArgumentKeys<Properties>
|
|
32
|
+
>]: ArgTypeFromDescriptor<Properties[K]>;
|
|
33
|
+
} & {
|
|
34
|
+
readonly [K in OptionalObjectArgumentKeys<Properties>]?: ArgTypeFromDescriptor<Properties[K]>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ArgTypeFromDescriptor<Arg extends AuthoringArgumentDescriptor> = Arg extends {
|
|
38
|
+
readonly kind: 'string';
|
|
39
|
+
}
|
|
40
|
+
? string
|
|
41
|
+
: Arg extends { readonly kind: 'number' }
|
|
42
|
+
? number
|
|
43
|
+
: Arg extends { readonly kind: 'stringArray' }
|
|
44
|
+
? readonly string[]
|
|
45
|
+
: Arg extends {
|
|
46
|
+
readonly kind: 'object';
|
|
47
|
+
readonly properties: infer Properties extends Record<
|
|
48
|
+
string,
|
|
49
|
+
AuthoringArgumentDescriptor
|
|
50
|
+
>;
|
|
51
|
+
}
|
|
52
|
+
? ObjectArgumentType<Properties>
|
|
53
|
+
: never;
|
|
54
|
+
|
|
55
|
+
export type TupleFromArgumentDescriptors<Args extends readonly AuthoringArgumentDescriptor[]> = {
|
|
56
|
+
readonly [K in keyof Args]: Args[K] extends AuthoringArgumentDescriptor
|
|
57
|
+
? ArgTypeFromDescriptor<Args[K]>
|
|
58
|
+
: never;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type SupportsNamedConstraintOptions<Descriptor extends AuthoringFieldPresetDescriptor> =
|
|
62
|
+
Descriptor['output'] extends { readonly id: true }
|
|
63
|
+
? true
|
|
64
|
+
: Descriptor['output'] extends { readonly unique: true }
|
|
65
|
+
? true
|
|
66
|
+
: false;
|
|
67
|
+
|
|
68
|
+
export type ResolveTemplateValue<Template, Args extends readonly unknown[]> = Template extends {
|
|
69
|
+
readonly kind: 'arg';
|
|
70
|
+
readonly index: infer Index extends number;
|
|
71
|
+
readonly path?: infer Path extends readonly string[] | undefined;
|
|
72
|
+
readonly default?: infer Default;
|
|
73
|
+
}
|
|
74
|
+
? ResolveTemplateArgValue<Args[Index], Path, Default, Args>
|
|
75
|
+
: Template extends readonly unknown[]
|
|
76
|
+
? { readonly [K in keyof Template]: ResolveTemplateValue<Template[K], Args> }
|
|
77
|
+
: Template extends Record<string, unknown>
|
|
78
|
+
? { readonly [K in keyof Template]: ResolveTemplateValue<Template[K], Args> }
|
|
79
|
+
: Template;
|
|
80
|
+
|
|
81
|
+
type ResolveTemplatePathValue<
|
|
82
|
+
Value,
|
|
83
|
+
Path extends readonly string[] | undefined,
|
|
84
|
+
> = Path extends readonly [infer Segment extends string, ...infer Rest extends readonly string[]]
|
|
85
|
+
? Segment extends keyof NonNullable<Value>
|
|
86
|
+
? ResolveTemplatePathValue<NonNullable<Value>[Segment], Rest>
|
|
87
|
+
: never
|
|
88
|
+
: Value;
|
|
89
|
+
|
|
90
|
+
type ResolveTemplateDefaultValue<
|
|
91
|
+
Value,
|
|
92
|
+
Default,
|
|
93
|
+
Args extends readonly unknown[],
|
|
94
|
+
> = Default extends undefined
|
|
95
|
+
? Value
|
|
96
|
+
: [Value] extends [never]
|
|
97
|
+
? ResolveTemplateValue<Default, Args>
|
|
98
|
+
: undefined extends Value
|
|
99
|
+
? Exclude<Value, undefined> | ResolveTemplateValue<Default, Args>
|
|
100
|
+
: Value;
|
|
101
|
+
|
|
102
|
+
type ResolveTemplateArgValue<
|
|
103
|
+
Value,
|
|
104
|
+
Path extends readonly string[] | undefined,
|
|
105
|
+
Default,
|
|
106
|
+
Args extends readonly unknown[],
|
|
107
|
+
> = ResolveTemplateDefaultValue<ResolveTemplatePathValue<Value, Path>, Default, Args>;
|
|
108
|
+
|
|
109
|
+
export type FieldBuilderFromPresetDescriptor<
|
|
110
|
+
Descriptor extends AuthoringFieldPresetDescriptor,
|
|
111
|
+
Args extends readonly unknown[] = readonly [],
|
|
112
|
+
ConstraintName extends string | undefined = undefined,
|
|
113
|
+
> = ScalarFieldBuilder<
|
|
114
|
+
ScalarFieldState<
|
|
115
|
+
ResolveTemplateValue<Descriptor['output']['codecId'], Args> extends string
|
|
116
|
+
? ResolveTemplateValue<Descriptor['output']['codecId'], Args>
|
|
117
|
+
: string,
|
|
118
|
+
undefined,
|
|
119
|
+
ResolveTemplateValue<Descriptor['output']['nullable'], Args> extends true ? true : false,
|
|
120
|
+
undefined,
|
|
121
|
+
NamedConstraintState<
|
|
122
|
+
ResolveTemplateValue<Descriptor['output']['id'], Args> extends true ? true : false,
|
|
123
|
+
ConstraintName
|
|
124
|
+
>,
|
|
125
|
+
NamedConstraintState<
|
|
126
|
+
ResolveTemplateValue<Descriptor['output']['unique'], Args> extends true ? true : false,
|
|
127
|
+
ConstraintName
|
|
128
|
+
>
|
|
129
|
+
>
|
|
130
|
+
>;
|
|
131
|
+
|
|
132
|
+
export type FieldHelperFunctionWithoutNamedConstraint<
|
|
133
|
+
Descriptor extends AuthoringFieldPresetDescriptor,
|
|
134
|
+
> = Descriptor extends {
|
|
135
|
+
readonly args: infer Args extends readonly AuthoringArgumentDescriptor[];
|
|
136
|
+
}
|
|
137
|
+
? <const Params extends TupleFromArgumentDescriptors<Args>>(
|
|
138
|
+
...args: Params
|
|
139
|
+
) => FieldBuilderFromPresetDescriptor<Descriptor, Params>
|
|
140
|
+
: () => FieldBuilderFromPresetDescriptor<Descriptor, readonly []>;
|
|
141
|
+
|
|
142
|
+
export type FieldHelperFunctionWithNamedConstraint<
|
|
143
|
+
Descriptor extends AuthoringFieldPresetDescriptor,
|
|
144
|
+
> = Descriptor extends {
|
|
145
|
+
readonly args: infer Args extends readonly AuthoringArgumentDescriptor[];
|
|
146
|
+
}
|
|
147
|
+
? <
|
|
148
|
+
const Params extends TupleFromArgumentDescriptors<Args>,
|
|
149
|
+
const Name extends string | undefined = undefined,
|
|
150
|
+
>(
|
|
151
|
+
...args: [...params: Params, options?: NamedConstraintSpec<Name>]
|
|
152
|
+
) => FieldBuilderFromPresetDescriptor<Descriptor, Params, Name>
|
|
153
|
+
: <const Name extends string | undefined = undefined>(
|
|
154
|
+
options?: NamedConstraintSpec<Name>,
|
|
155
|
+
) => FieldBuilderFromPresetDescriptor<Descriptor, readonly [], Name>;
|
|
156
|
+
|
|
157
|
+
export type FieldHelperFunction<Descriptor extends AuthoringFieldPresetDescriptor> =
|
|
158
|
+
SupportsNamedConstraintOptions<Descriptor> extends true
|
|
159
|
+
? FieldHelperFunctionWithNamedConstraint<Descriptor>
|
|
160
|
+
: FieldHelperFunctionWithoutNamedConstraint<Descriptor>;
|
|
161
|
+
|
|
162
|
+
export type FieldHelpersFromNamespace<Namespace> = {
|
|
163
|
+
readonly [K in keyof Namespace]: Namespace[K] extends AuthoringFieldPresetDescriptor
|
|
164
|
+
? FieldHelperFunction<Namespace[K]>
|
|
165
|
+
: Namespace[K] extends Record<string, unknown>
|
|
166
|
+
? FieldHelpersFromNamespace<Namespace[K]>
|
|
167
|
+
: never;
|
|
168
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoringArgumentDescriptor,
|
|
3
|
+
AuthoringFieldNamespace,
|
|
4
|
+
AuthoringTypeConstructorDescriptor,
|
|
5
|
+
AuthoringTypeNamespace,
|
|
6
|
+
ExtensionPackRef,
|
|
7
|
+
FamilyPackRef,
|
|
8
|
+
TargetPackRef,
|
|
9
|
+
} from '@prisma-next/contract/framework-components';
|
|
10
|
+
import {
|
|
11
|
+
isAuthoringFieldPresetDescriptor,
|
|
12
|
+
isAuthoringTypeConstructorDescriptor,
|
|
13
|
+
} from '@prisma-next/contract/framework-components';
|
|
14
|
+
import {
|
|
15
|
+
createFieldHelpersFromNamespace,
|
|
16
|
+
createFieldPresetHelper,
|
|
17
|
+
createTypeHelpersFromNamespace,
|
|
18
|
+
} from './authoring-helper-runtime';
|
|
19
|
+
import type {
|
|
20
|
+
FieldHelpersFromNamespace,
|
|
21
|
+
ResolveTemplateValue,
|
|
22
|
+
TupleFromArgumentDescriptors,
|
|
23
|
+
UnionToIntersection,
|
|
24
|
+
} from './authoring-type-utils';
|
|
25
|
+
import { buildFieldPreset, field, model, rel } from './staged-contract-dsl';
|
|
26
|
+
|
|
27
|
+
type ExtractTypeNamespaceFromPack<Pack> = Pack extends {
|
|
28
|
+
readonly authoring?: { readonly type?: infer Namespace extends AuthoringTypeNamespace };
|
|
29
|
+
}
|
|
30
|
+
? Namespace
|
|
31
|
+
: Record<never, never>;
|
|
32
|
+
|
|
33
|
+
type ExtractFieldNamespaceFromPack<Pack> = Pack extends {
|
|
34
|
+
readonly authoring?: { readonly field?: infer Namespace extends AuthoringFieldNamespace };
|
|
35
|
+
}
|
|
36
|
+
? Namespace
|
|
37
|
+
: Record<never, never>;
|
|
38
|
+
|
|
39
|
+
type MergeExtensionTypeNamespaces<ExtensionPacks> =
|
|
40
|
+
ExtensionPacks extends Record<string, unknown>
|
|
41
|
+
? keyof ExtensionPacks extends never
|
|
42
|
+
? Record<never, never>
|
|
43
|
+
: UnionToIntersection<
|
|
44
|
+
{
|
|
45
|
+
[K in keyof ExtensionPacks]: ExtractTypeNamespaceFromPack<ExtensionPacks[K]>;
|
|
46
|
+
}[keyof ExtensionPacks]
|
|
47
|
+
>
|
|
48
|
+
: Record<never, never>;
|
|
49
|
+
|
|
50
|
+
type MergeExtensionFieldNamespaces<ExtensionPacks> =
|
|
51
|
+
ExtensionPacks extends Record<string, unknown>
|
|
52
|
+
? keyof ExtensionPacks extends never
|
|
53
|
+
? Record<never, never>
|
|
54
|
+
: UnionToIntersection<
|
|
55
|
+
{
|
|
56
|
+
[K in keyof ExtensionPacks]: ExtractFieldNamespaceFromPack<ExtensionPacks[K]>;
|
|
57
|
+
}[keyof ExtensionPacks]
|
|
58
|
+
>
|
|
59
|
+
: Record<never, never>;
|
|
60
|
+
|
|
61
|
+
type StorageTypeFromDescriptor<
|
|
62
|
+
Descriptor extends AuthoringTypeConstructorDescriptor,
|
|
63
|
+
Args extends readonly unknown[],
|
|
64
|
+
> = {
|
|
65
|
+
readonly codecId: ResolveTemplateValue<Descriptor['output']['codecId'], Args>;
|
|
66
|
+
readonly nativeType: ResolveTemplateValue<Descriptor['output']['nativeType'], Args>;
|
|
67
|
+
} & (Descriptor['output'] extends {
|
|
68
|
+
readonly typeParams: infer TypeParams extends Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
? {
|
|
71
|
+
readonly typeParams: ResolveTemplateValue<TypeParams, Args>;
|
|
72
|
+
}
|
|
73
|
+
: Record<never, never>);
|
|
74
|
+
|
|
75
|
+
type TypeHelperFunction<Descriptor extends AuthoringTypeConstructorDescriptor> =
|
|
76
|
+
Descriptor extends { readonly args: infer Args extends readonly AuthoringArgumentDescriptor[] }
|
|
77
|
+
? <const Params extends TupleFromArgumentDescriptors<Args>>(
|
|
78
|
+
...args: Params
|
|
79
|
+
) => StorageTypeFromDescriptor<Descriptor, Params>
|
|
80
|
+
: () => StorageTypeFromDescriptor<Descriptor, readonly []>;
|
|
81
|
+
|
|
82
|
+
type TypeHelpersFromNamespace<Namespace> = {
|
|
83
|
+
readonly [K in keyof Namespace]: Namespace[K] extends AuthoringTypeConstructorDescriptor
|
|
84
|
+
? TypeHelperFunction<Namespace[K]>
|
|
85
|
+
: Namespace[K] extends Record<string, unknown>
|
|
86
|
+
? TypeHelpersFromNamespace<Namespace[K]>
|
|
87
|
+
: never;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type CoreFieldHelpers = Pick<typeof field, 'column' | 'generated' | 'namedType'>;
|
|
91
|
+
|
|
92
|
+
export type ComposedAuthoringHelpers<
|
|
93
|
+
Family extends FamilyPackRef<string>,
|
|
94
|
+
Target extends TargetPackRef<'sql', string>,
|
|
95
|
+
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
96
|
+
> = {
|
|
97
|
+
readonly field: CoreFieldHelpers &
|
|
98
|
+
FieldHelpersFromNamespace<
|
|
99
|
+
ExtractFieldNamespaceFromPack<Family> &
|
|
100
|
+
ExtractFieldNamespaceFromPack<Target> &
|
|
101
|
+
MergeExtensionFieldNamespaces<ExtensionPacks>
|
|
102
|
+
>;
|
|
103
|
+
readonly model: typeof model;
|
|
104
|
+
readonly rel: typeof rel;
|
|
105
|
+
readonly type: TypeHelpersFromNamespace<
|
|
106
|
+
ExtractTypeNamespaceFromPack<Family> &
|
|
107
|
+
ExtractTypeNamespaceFromPack<Target> &
|
|
108
|
+
MergeExtensionTypeNamespaces<ExtensionPacks>
|
|
109
|
+
>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function extractTypeNamespace<Pack>(pack: Pack): ExtractTypeNamespaceFromPack<Pack> {
|
|
113
|
+
return ((pack as { readonly authoring?: { readonly type?: unknown } }).authoring?.type ??
|
|
114
|
+
{}) as ExtractTypeNamespaceFromPack<Pack>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractFieldNamespace<Pack>(pack: Pack): ExtractFieldNamespaceFromPack<Pack> {
|
|
118
|
+
return ((pack as { readonly authoring?: { readonly field?: unknown } }).authoring?.field ??
|
|
119
|
+
{}) as ExtractFieldNamespaceFromPack<Pack>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergeHelperNamespaces(
|
|
123
|
+
target: Record<string, unknown>,
|
|
124
|
+
source: Record<string, unknown>,
|
|
125
|
+
path: readonly string[],
|
|
126
|
+
leafGuard: (value: unknown) => boolean,
|
|
127
|
+
label: string,
|
|
128
|
+
): void {
|
|
129
|
+
const assertSafePath = (currentPath: readonly string[]) => {
|
|
130
|
+
const blockedSegment = currentPath.find(
|
|
131
|
+
(segment) => segment === '__proto__' || segment === 'constructor' || segment === 'prototype',
|
|
132
|
+
);
|
|
133
|
+
if (blockedSegment) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Invalid authoring ${label} helper "${currentPath.join('.')}". Helper path segments must not use "${blockedSegment}".`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
141
|
+
const currentPath = [...path, key];
|
|
142
|
+
assertSafePath(currentPath);
|
|
143
|
+
const hasExistingValue = Object.hasOwn(target, key);
|
|
144
|
+
const existingValue = hasExistingValue ? target[key] : undefined;
|
|
145
|
+
|
|
146
|
+
if (!hasExistingValue) {
|
|
147
|
+
target[key] = sourceValue;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const existingIsLeaf = leafGuard(existingValue);
|
|
152
|
+
const sourceIsLeaf = leafGuard(sourceValue);
|
|
153
|
+
|
|
154
|
+
if (existingIsLeaf || sourceIsLeaf) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Duplicate authoring ${label} helper "${currentPath.join('.')}". Helper names must be unique across composed packs.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mergeHelperNamespaces(
|
|
161
|
+
existingValue as Record<string, unknown>,
|
|
162
|
+
sourceValue as Record<string, unknown>,
|
|
163
|
+
currentPath,
|
|
164
|
+
leafGuard,
|
|
165
|
+
label,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type AuthoringComponent = {
|
|
171
|
+
readonly authoring?: { readonly type?: unknown; readonly field?: unknown };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function composeTypeNamespace(components: readonly AuthoringComponent[]): AuthoringTypeNamespace {
|
|
175
|
+
const merged: Record<string, unknown> = {};
|
|
176
|
+
for (const component of components) {
|
|
177
|
+
const ns = extractTypeNamespace(component);
|
|
178
|
+
if (Object.keys(ns).length > 0) {
|
|
179
|
+
mergeHelperNamespaces(merged, ns, [], isAuthoringTypeConstructorDescriptor, 'type');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return merged as AuthoringTypeNamespace;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function composeFieldNamespace(components: readonly AuthoringComponent[]): AuthoringFieldNamespace {
|
|
186
|
+
const merged: Record<string, unknown> = {};
|
|
187
|
+
for (const component of components) {
|
|
188
|
+
const ns = extractFieldNamespace(component);
|
|
189
|
+
if (Object.keys(ns).length > 0) {
|
|
190
|
+
mergeHelperNamespaces(merged, ns, [], isAuthoringFieldPresetDescriptor, 'field');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return merged as AuthoringFieldNamespace;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createComposedFieldHelpers(
|
|
197
|
+
components: readonly AuthoringComponent[],
|
|
198
|
+
): CoreFieldHelpers & Record<string, unknown> {
|
|
199
|
+
const helperNamespace = createFieldHelpersFromNamespace(
|
|
200
|
+
composeFieldNamespace(components),
|
|
201
|
+
({ helperPath, descriptor }) =>
|
|
202
|
+
createFieldPresetHelper({
|
|
203
|
+
helperPath,
|
|
204
|
+
descriptor,
|
|
205
|
+
build: ({ args, namedConstraintOptions }) =>
|
|
206
|
+
buildFieldPreset(descriptor, args, namedConstraintOptions),
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
const coreFieldHelpers = {
|
|
210
|
+
column: field.column,
|
|
211
|
+
generated: field.generated,
|
|
212
|
+
namedType: field.namedType,
|
|
213
|
+
} satisfies CoreFieldHelpers;
|
|
214
|
+
|
|
215
|
+
const coreHelperNames = new Set(Object.keys(coreFieldHelpers));
|
|
216
|
+
for (const helperName of Object.keys(helperNamespace)) {
|
|
217
|
+
if (coreHelperNames.has(helperName)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Duplicate authoring field helper "${helperName}". Core field helpers reserve that name.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
...coreFieldHelpers,
|
|
226
|
+
...helperNamespace,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createComposedAuthoringHelpers<
|
|
231
|
+
Family extends FamilyPackRef<string>,
|
|
232
|
+
Target extends TargetPackRef<'sql', string>,
|
|
233
|
+
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
234
|
+
>(options: {
|
|
235
|
+
readonly family: Family;
|
|
236
|
+
readonly target: Target;
|
|
237
|
+
readonly extensionPacks?: ExtensionPacks;
|
|
238
|
+
}): ComposedAuthoringHelpers<Family, Target, ExtensionPacks> {
|
|
239
|
+
const extensionValues: readonly ExtensionPackRef<'sql', string>[] = Object.values(
|
|
240
|
+
(options.extensionPacks ?? {}) as Record<string, ExtensionPackRef<'sql', string>>,
|
|
241
|
+
);
|
|
242
|
+
const components: readonly AuthoringComponent[] = [
|
|
243
|
+
options.family,
|
|
244
|
+
options.target,
|
|
245
|
+
...extensionValues,
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
field: createComposedFieldHelpers(components),
|
|
250
|
+
model,
|
|
251
|
+
rel,
|
|
252
|
+
type: createTypeHelpersFromNamespace(composeTypeNamespace(components)),
|
|
253
|
+
} as ComposedAuthoringHelpers<Family, Target, ExtensionPacks>;
|
|
254
|
+
}
|