@prisma-next/sql-contract-ts 0.5.0-dev.9 → 0.6.0-dev.1

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/package.json CHANGED
@@ -1,28 +1,29 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-ts",
3
- "version": "0.5.0-dev.9",
3
+ "version": "0.6.0-dev.1",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "sideEffects": false,
6
7
  "description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
7
8
  "dependencies": {
8
- "arktype": "^2.1.25",
9
+ "arktype": "^2.1.29",
9
10
  "pathe": "^2.0.3",
10
11
  "ts-toolbelt": "^9.6.0",
11
- "@prisma-next/config": "0.5.0-dev.9",
12
- "@prisma-next/contract": "0.5.0-dev.9",
13
- "@prisma-next/contract-authoring": "0.5.0-dev.9",
14
- "@prisma-next/utils": "0.5.0-dev.9",
15
- "@prisma-next/sql-contract": "0.5.0-dev.9",
16
- "@prisma-next/framework-components": "0.5.0-dev.9"
12
+ "@prisma-next/contract": "0.6.0-dev.1",
13
+ "@prisma-next/config": "0.6.0-dev.1",
14
+ "@prisma-next/contract-authoring": "0.6.0-dev.1",
15
+ "@prisma-next/framework-components": "0.6.0-dev.1",
16
+ "@prisma-next/sql-contract": "0.6.0-dev.1",
17
+ "@prisma-next/utils": "0.6.0-dev.1"
17
18
  },
18
19
  "devDependencies": {
19
- "@types/pg": "8.16.0",
20
- "pg": "8.16.3",
21
- "tsdown": "0.18.4",
20
+ "@types/pg": "8.20.0",
21
+ "pg": "8.20.0",
22
+ "tsdown": "0.22.0",
22
23
  "typescript": "5.9.3",
23
- "vitest": "4.0.17",
24
- "@prisma-next/tsconfig": "0.0.0",
24
+ "vitest": "4.1.5",
25
25
  "@prisma-next/test-utils": "0.0.1",
26
+ "@prisma-next/tsconfig": "0.0.0",
26
27
  "@prisma-next/tsdown": "0.0.0"
27
28
  },
28
29
  "files": [
@@ -38,19 +38,21 @@ export type ArgTypeFromDescriptor<Arg extends AuthoringArgumentDescriptor> = Arg
38
38
  readonly kind: 'string';
39
39
  }
40
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;
41
+ : Arg extends { readonly kind: 'boolean' }
42
+ ? boolean
43
+ : Arg extends { readonly kind: 'number' }
44
+ ? number
45
+ : Arg extends { readonly kind: 'stringArray' }
46
+ ? readonly string[]
47
+ : Arg extends {
48
+ readonly kind: 'object';
49
+ readonly properties: infer Properties extends Record<
50
+ string,
51
+ AuthoringArgumentDescriptor
52
+ >;
53
+ }
54
+ ? ObjectArgumentType<Properties>
55
+ : never;
54
56
 
55
57
  export type TupleFromArgumentDescriptors<Args extends readonly AuthoringArgumentDescriptor[]> = {
56
58
  readonly [K in keyof Args]: Args[K] extends AuthoringArgumentDescriptor
@@ -13,11 +13,16 @@ import {
13
13
  type ContractValueObject,
14
14
  coreHash,
15
15
  type ExecutionMutationDefault,
16
- type ExecutionMutationDefaultValue,
17
16
  type JsonValue,
18
17
  type StorageHashBase,
19
18
  } from '@prisma-next/contract/types';
20
19
  import type { CodecLookup } from '@prisma-next/framework-components/codec';
20
+ import { validateIndexTypes } from '@prisma-next/sql-contract/index-type-validation';
21
+ import {
22
+ createIndexTypeRegistry,
23
+ type IndexTypeMap,
24
+ type IndexTypeRegistration,
25
+ } from '@prisma-next/sql-contract/index-types';
21
26
  import {
22
27
  applyFkDefaults,
23
28
  type SqlStorage,
@@ -64,11 +69,37 @@ function encodeColumnDefault(
64
69
  };
65
70
  }
66
71
 
67
- function assertStorageSemantics(storage: SqlStorage): void {
68
- const semanticErrors = validateStorageSemantics(storage);
72
+ function assertStorageSemantics(
73
+ definition: ContractDefinition,
74
+ contract: Contract<SqlStorage>,
75
+ ): void {
76
+ const semanticErrors = validateStorageSemantics(contract.storage);
69
77
  if (semanticErrors.length > 0) {
70
78
  throw new Error(`Contract semantic validation failed: ${semanticErrors.join('; ')}`);
71
79
  }
80
+
81
+ const indexTypeRegistry = createIndexTypeRegistry();
82
+ const packsToRegister: ReadonlyArray<{ readonly id?: string; readonly indexTypes?: unknown }> = [
83
+ definition.target,
84
+ ...Object.values(definition.extensionPacks ?? {}),
85
+ ];
86
+ for (const pack of packsToRegister) {
87
+ const registration = pack.indexTypes;
88
+ if (registration === undefined) continue;
89
+ if (
90
+ typeof registration !== 'object' ||
91
+ registration === null ||
92
+ !Array.isArray((registration as { entries?: unknown }).entries)
93
+ ) {
94
+ throw new Error(
95
+ `Pack "${pack.id ?? '<unknown>'}" declares "indexTypes" but its value is not an IndexTypeRegistration (expected an object with an "entries" array; got ${typeof registration}).`,
96
+ );
97
+ }
98
+ for (const entry of (registration as IndexTypeRegistration<IndexTypeMap>).entries) {
99
+ indexTypeRegistry.register(entry);
100
+ }
101
+ }
102
+ validateIndexTypes(contract, indexTypeRegistry);
72
103
  }
73
104
 
74
105
  function assertKnownTargetModel(
@@ -198,15 +229,19 @@ export function buildSqlContractFromDefinition(
198
229
  const domainFieldRefs: Record<string, DomainFieldRef> = {};
199
230
 
200
231
  for (const field of semanticModel.fields) {
201
- if (field.executionDefault) {
232
+ const executionDefaultPhases =
233
+ field.executionDefaults?.onCreate || field.executionDefaults?.onUpdate
234
+ ? field.executionDefaults
235
+ : undefined;
236
+ if (executionDefaultPhases) {
202
237
  if (field.default !== undefined) {
203
238
  throw new Error(
204
- `Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefault.`,
239
+ `Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefaults.`,
205
240
  );
206
241
  }
207
242
  if (field.nullable) {
208
243
  throw new Error(
209
- `Field "${semanticModel.modelName}.${field.fieldName}" cannot be nullable when executionDefault is present.`,
244
+ `Field "${semanticModel.modelName}.${field.fieldName}" cannot be nullable when executionDefaults are present.`,
210
245
  );
211
246
  }
212
247
  }
@@ -227,28 +262,15 @@ export function buildSqlContractFromDefinition(
227
262
  domainFieldRefs[field.fieldName] = { kind: 'scalar', many: true };
228
263
  }
229
264
 
230
- if ('executionDefault' in field && field.executionDefault) {
265
+ if (executionDefaultPhases) {
231
266
  executionDefaults.push({
232
267
  ref: { table: tableName, column: field.columnName },
233
- onCreate: field.executionDefault as ExecutionMutationDefaultValue,
268
+ ...ifDefined('onCreate', executionDefaultPhases.onCreate),
269
+ ...ifDefined('onUpdate', executionDefaultPhases.onUpdate),
234
270
  });
235
271
  }
236
272
  }
237
273
 
238
- if (semanticModel.id) {
239
- const fieldsByColumnName = new Map(
240
- semanticModel.fields.map((field) => [field.columnName, field]),
241
- );
242
- for (const columnName of semanticModel.id.columns) {
243
- const field = fieldsByColumnName.get(columnName);
244
- if (field?.nullable) {
245
- throw new Error(
246
- `Model "${semanticModel.modelName}" uses nullable field "${field.fieldName}" in its identity.`,
247
- );
248
- }
249
- }
250
- }
251
-
252
274
  const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
253
275
  const targetModel = assertKnownTargetModel(
254
276
  modelsByName,
@@ -287,8 +309,8 @@ export function buildSqlContractFromDefinition(
287
309
  indexes: (semanticModel.indexes ?? []).map((i) => ({
288
310
  columns: i.columns,
289
311
  ...ifDefined('name', i.name),
290
- ...ifDefined('using', i.using),
291
- ...ifDefined('config', i.config),
312
+ ...ifDefined('type', i.type),
313
+ ...ifDefined('options', i.options),
292
314
  })),
293
315
  foreignKeys,
294
316
  ...(semanticModel.id
@@ -457,7 +479,7 @@ export function buildSqlContractFromDefinition(
457
479
  meta: {},
458
480
  };
459
481
 
460
- assertStorageSemantics(contract.storage);
482
+ assertStorageSemantics(definition, contract);
461
483
 
462
484
  return contract;
463
485
  }
@@ -5,8 +5,10 @@ import type {
5
5
  AuthoringTypeNamespace,
6
6
  } from '@prisma-next/framework-components/authoring';
7
7
  import {
8
+ assertNoCrossRegistryCollisions,
8
9
  isAuthoringFieldPresetDescriptor,
9
10
  isAuthoringTypeConstructorDescriptor,
11
+ mergeAuthoringNamespaces,
10
12
  } from '@prisma-next/framework-components/authoring';
11
13
  import type {
12
14
  ExtensionPackRef,
@@ -24,7 +26,14 @@ import type {
24
26
  TupleFromArgumentDescriptors,
25
27
  UnionToIntersection,
26
28
  } from './authoring-type-utils';
29
+ import type {
30
+ AnyRelationBuilder,
31
+ ContractModelBuilder,
32
+ IndexTypeMap,
33
+ ScalarFieldBuilder,
34
+ } from './contract-dsl';
27
35
  import { buildFieldPreset, field, model, rel } from './contract-dsl';
36
+ import type { MergeExtensionIndexTypes } from './contract-types';
28
37
 
29
38
  type ExtractTypeNamespaceFromPack<Pack> = Pack extends {
30
39
  readonly authoring?: { readonly type?: infer Namespace extends AuthoringTypeNamespace };
@@ -91,6 +100,33 @@ type TypeHelpersFromNamespace<Namespace> = {
91
100
 
92
101
  type CoreFieldHelpers = Pick<typeof field, 'column' | 'generated' | 'namedType'>;
93
102
 
103
+ type MergeAllPackIndexTypes<Family, Target, ExtensionPacks> = MergeExtensionIndexTypes<
104
+ { readonly __family: Family; readonly __target: Target } & (ExtensionPacks extends Record<
105
+ string,
106
+ unknown
107
+ >
108
+ ? ExtensionPacks
109
+ : Record<never, never>)
110
+ >;
111
+
112
+ type PackAwareModel<IndexTypes extends IndexTypeMap> = {
113
+ <
114
+ const ModelName extends string,
115
+ Fields extends Record<string, ScalarFieldBuilder>,
116
+ Relations extends Record<string, AnyRelationBuilder> = Record<never, never>,
117
+ >(
118
+ modelName: ModelName,
119
+ input: { readonly fields: Fields; readonly relations?: Relations },
120
+ ): ContractModelBuilder<ModelName, Fields, Relations, undefined, undefined, IndexTypes>;
121
+ <
122
+ Fields extends Record<string, ScalarFieldBuilder>,
123
+ Relations extends Record<string, AnyRelationBuilder> = Record<never, never>,
124
+ >(input: {
125
+ readonly fields: Fields;
126
+ readonly relations?: Relations;
127
+ }): ContractModelBuilder<undefined, Fields, Relations, undefined, undefined, IndexTypes>;
128
+ };
129
+
94
130
  export type ComposedAuthoringHelpers<
95
131
  Family extends FamilyPackRef<string>,
96
132
  Target extends TargetPackRef<'sql', string>,
@@ -102,7 +138,7 @@ export type ComposedAuthoringHelpers<
102
138
  ExtractFieldNamespaceFromPack<Target> &
103
139
  MergeExtensionFieldNamespaces<ExtensionPacks>
104
140
  >;
105
- readonly model: typeof model;
141
+ readonly model: PackAwareModel<MergeAllPackIndexTypes<Family, Target, ExtensionPacks>>;
106
142
  readonly rel: typeof rel;
107
143
  readonly type: TypeHelpersFromNamespace<
108
144
  ExtractTypeNamespaceFromPack<Family> &
@@ -121,54 +157,6 @@ function extractFieldNamespace<Pack>(pack: Pack): ExtractFieldNamespaceFromPack<
121
157
  {}) as ExtractFieldNamespaceFromPack<Pack>;
122
158
  }
123
159
 
124
- function mergeHelperNamespaces(
125
- target: Record<string, unknown>,
126
- source: Record<string, unknown>,
127
- path: readonly string[],
128
- leafGuard: (value: unknown) => boolean,
129
- label: string,
130
- ): void {
131
- const assertSafePath = (currentPath: readonly string[]) => {
132
- const blockedSegment = currentPath.find(
133
- (segment) => segment === '__proto__' || segment === 'constructor' || segment === 'prototype',
134
- );
135
- if (blockedSegment) {
136
- throw new Error(
137
- `Invalid authoring ${label} helper "${currentPath.join('.')}". Helper path segments must not use "${blockedSegment}".`,
138
- );
139
- }
140
- };
141
-
142
- for (const [key, sourceValue] of Object.entries(source)) {
143
- const currentPath = [...path, key];
144
- assertSafePath(currentPath);
145
- const hasExistingValue = Object.hasOwn(target, key);
146
- const existingValue = hasExistingValue ? target[key] : undefined;
147
-
148
- if (!hasExistingValue) {
149
- target[key] = sourceValue;
150
- continue;
151
- }
152
-
153
- const existingIsLeaf = leafGuard(existingValue);
154
- const sourceIsLeaf = leafGuard(sourceValue);
155
-
156
- if (existingIsLeaf || sourceIsLeaf) {
157
- throw new Error(
158
- `Duplicate authoring ${label} helper "${currentPath.join('.')}". Helper names must be unique across composed packs.`,
159
- );
160
- }
161
-
162
- mergeHelperNamespaces(
163
- existingValue as Record<string, unknown>,
164
- sourceValue as Record<string, unknown>,
165
- currentPath,
166
- leafGuard,
167
- label,
168
- );
169
- }
170
- }
171
-
172
160
  type AuthoringComponent = {
173
161
  readonly authoring?: { readonly type?: unknown; readonly field?: unknown };
174
162
  };
@@ -178,7 +166,7 @@ function composeTypeNamespace(components: readonly AuthoringComponent[]): Author
178
166
  for (const component of components) {
179
167
  const ns = extractTypeNamespace(component);
180
168
  if (Object.keys(ns).length > 0) {
181
- mergeHelperNamespaces(merged, ns, [], isAuthoringTypeConstructorDescriptor, 'type');
169
+ mergeAuthoringNamespaces(merged, ns, [], isAuthoringTypeConstructorDescriptor, 'type');
182
170
  }
183
171
  }
184
172
  return merged as AuthoringTypeNamespace;
@@ -189,17 +177,17 @@ function composeFieldNamespace(components: readonly AuthoringComponent[]): Autho
189
177
  for (const component of components) {
190
178
  const ns = extractFieldNamespace(component);
191
179
  if (Object.keys(ns).length > 0) {
192
- mergeHelperNamespaces(merged, ns, [], isAuthoringFieldPresetDescriptor, 'field');
180
+ mergeAuthoringNamespaces(merged, ns, [], isAuthoringFieldPresetDescriptor, 'field');
193
181
  }
194
182
  }
195
183
  return merged as AuthoringFieldNamespace;
196
184
  }
197
185
 
198
186
  function createComposedFieldHelpers(
199
- components: readonly AuthoringComponent[],
187
+ fieldNamespace: AuthoringFieldNamespace,
200
188
  ): CoreFieldHelpers & Record<string, unknown> {
201
189
  const helperNamespace = createFieldHelpersFromNamespace(
202
- composeFieldNamespace(components),
190
+ fieldNamespace,
203
191
  ({ helperPath, descriptor }) =>
204
192
  createFieldPresetHelper({
205
193
  helperPath,
@@ -247,10 +235,16 @@ export function createComposedAuthoringHelpers<
247
235
  ...extensionValues,
248
236
  ];
249
237
 
238
+ const typeNamespace = composeTypeNamespace(components);
239
+ const fieldNamespace = composeFieldNamespace(components);
240
+ // Mirrors the call in `assembleAuthoringContributions`: PSL composes via
241
+ // `createControlStack`, the TS DSL composes here. Both seams need the guard.
242
+ assertNoCrossRegistryCollisions(typeNamespace, fieldNamespace);
243
+
250
244
  return {
251
- field: createComposedFieldHelpers(components),
245
+ field: createComposedFieldHelpers(fieldNamespace),
252
246
  model,
253
247
  rel,
254
- type: createTypeHelpersFromNamespace(composeTypeNamespace(components)),
248
+ type: createTypeHelpersFromNamespace(typeNamespace),
255
249
  } as ComposedAuthoringHelpers<Family, Target, ExtensionPacks>;
256
250
  }
@@ -1,18 +1,18 @@
1
- import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
2
- import type {
3
- ColumnTypeDescriptor,
4
- ForeignKeyDefaultsState,
5
- } from '@prisma-next/contract-authoring';
1
+ import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
2
+ import type { ForeignKeyDefaultsState } from '@prisma-next/contract-authoring';
3
+ import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
6
4
  import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
7
5
  import type { ReferentialAction, StorageTypeInstance } from '@prisma-next/sql-contract/types';
8
6
 
7
+ export type { ExecutionMutationDefaultPhases };
8
+
9
9
  export interface FieldNode {
10
10
  readonly fieldName: string;
11
11
  readonly columnName: string;
12
12
  readonly descriptor: ColumnTypeDescriptor;
13
13
  readonly nullable: boolean;
14
14
  readonly default?: ColumnDefault;
15
- readonly executionDefault?: ExecutionMutationDefaultValue;
15
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
16
16
  readonly many?: boolean;
17
17
  }
18
18
 
@@ -29,8 +29,8 @@ export interface UniqueConstraintNode {
29
29
  export interface IndexNode {
30
30
  readonly columns: readonly string[];
31
31
  readonly name?: string;
32
- readonly using?: string;
33
- readonly config?: Record<string, unknown>;
32
+ readonly type?: string;
33
+ readonly options?: Record<string, unknown>;
34
34
  }
35
35
 
36
36
  export interface ForeignKeyNode {
@@ -71,7 +71,7 @@ export interface ValueObjectFieldNode {
71
71
  readonly valueObjectName: string;
72
72
  readonly nullable: boolean;
73
73
  readonly default?: ColumnDefault;
74
- readonly executionDefault?: ExecutionMutationDefaultValue;
74
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
75
75
  readonly many?: boolean;
76
76
  }
77
77