@prisma-next/sql-contract-emitter 0.3.0-dev.5 → 0.3.0-dev.50

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/src/index.ts CHANGED
@@ -1,11 +1,42 @@
1
1
  import type { ContractIR } from '@prisma-next/contract/ir';
2
- import type { TypesImportSpec, ValidationContext } from '@prisma-next/contract/types';
2
+ import type {
3
+ GenerateContractTypesOptions,
4
+ TypeRenderContext,
5
+ TypeRenderEntry,
6
+ TypesImportSpec,
7
+ ValidationContext,
8
+ } from '@prisma-next/contract/types';
3
9
  import type {
4
10
  ModelDefinition,
5
11
  ModelField,
6
12
  SqlStorage,
13
+ StorageColumn,
7
14
  StorageTable,
15
+ StorageTypeInstance,
8
16
  } from '@prisma-next/sql-contract/types';
17
+ import { assertDefined } from '@prisma-next/utils/assertions';
18
+
19
+ /**
20
+ * Resolves the typeParams for a column, either from inline typeParams or from typeRef.
21
+ * Returns undefined if no typeParams are available.
22
+ */
23
+ function resolveColumnTypeParams(
24
+ column: StorageColumn,
25
+ storage: SqlStorage,
26
+ ): Record<string, unknown> | undefined {
27
+ // Inline typeParams take precedence
28
+ if (column.typeParams && Object.keys(column.typeParams).length > 0) {
29
+ return column.typeParams;
30
+ }
31
+ // Check typeRef
32
+ if (column.typeRef && storage.types) {
33
+ const typeInstance = storage.types[column.typeRef] as StorageTypeInstance | undefined;
34
+ if (typeInstance?.typeParams) {
35
+ return typeInstance.typeParams;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
9
40
 
10
41
  export const sqlTargetFamilyHook = {
11
42
  id: 'sql',
@@ -64,10 +95,8 @@ export const sqlTargetFamilyHook = {
64
95
  throw new Error(`Model "${modelName}" references non-existent table "${tableName}"`);
65
96
  }
66
97
 
67
- const table = storage.tables[tableName];
68
- if (!table) {
69
- throw new Error(`Model "${modelName}" references non-existent table "${tableName}"`);
70
- }
98
+ const table: StorageTable | undefined = storage.tables[tableName];
99
+ assertDefined(table, `Model "${modelName}" references non-existent table "${tableName}"`);
71
100
 
72
101
  if (!table.primaryKey) {
73
102
  throw new Error(`Model "${modelName}" table "${tableName}" is missing a primary key`);
@@ -168,12 +197,12 @@ export const sqlTargetFamilyHook = {
168
197
  );
169
198
  }
170
199
 
171
- const referencedTable = storage.tables[fk.references.table];
172
- if (!referencedTable) {
173
- throw new Error(
174
- `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
175
- );
176
- }
200
+ // Table existence guaranteed by Set.has() check above
201
+ const referencedTable: StorageTable | undefined = storage.tables[fk.references.table];
202
+ assertDefined(
203
+ referencedTable,
204
+ `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
205
+ );
177
206
 
178
207
  const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
179
208
  for (const colName of fk.references.columns) {
@@ -197,45 +226,109 @@ export const sqlTargetFamilyHook = {
197
226
  ir: ContractIR,
198
227
  codecTypeImports: ReadonlyArray<TypesImportSpec>,
199
228
  operationTypeImports: ReadonlyArray<TypesImportSpec>,
229
+ hashes: {
230
+ readonly storageHash: string;
231
+ readonly executionHash?: string;
232
+ readonly profileHash: string;
233
+ },
234
+ options?: GenerateContractTypesOptions,
200
235
  ): string {
201
- const allImports = [...codecTypeImports, ...operationTypeImports];
202
- const importLines = allImports.map(
203
- (imp) => `import type { ${imp.named} as ${imp.alias} } from '${imp.package}';`,
204
- );
205
-
206
- const codecTypes = codecTypeImports.map((imp) => imp.alias).join(' & ');
207
- const operationTypes = operationTypeImports.map((imp) => imp.alias).join(' & ');
208
-
236
+ const parameterizedRenderers = options?.parameterizedRenderers;
237
+ const parameterizedTypeImports = options?.parameterizedTypeImports;
209
238
  const storage = ir.storage as SqlStorage;
210
239
  const models = ir.models as Record<string, ModelDefinition>;
211
240
 
241
+ // Collect all type imports from three sources:
242
+ // 1. Codec type imports (from adapters, targets, and extensions)
243
+ // 2. Operation type imports (from adapters, targets, and extensions)
244
+ // 3. Parameterized type imports (for parameterized codec renderers, may contain duplicates)
245
+ const allImports: TypesImportSpec[] = [...codecTypeImports, ...operationTypeImports];
246
+
247
+ if (parameterizedTypeImports) {
248
+ allImports.push(...parameterizedTypeImports);
249
+ }
250
+
251
+ // Deduplicate imports by package+named to avoid duplicate import statements.
252
+ // Strategy: When the same package::named appears multiple times, keep the first
253
+ // occurrence (and its alias); later duplicates with different aliases are silently ignored.
254
+ //
255
+ // Note: uniqueImports must be an array (not a Set) because:
256
+ // - We need to preserve the full TypesImportSpec objects (package, named, alias)
257
+ // - We need to preserve insertion order (first occurrence wins)
258
+ // - seenImportKeys is a Set used only for O(1) duplicate detection
259
+ const seenImportKeys = new Set<string>();
260
+ const uniqueImports: TypesImportSpec[] = [];
261
+ for (const imp of allImports) {
262
+ const key = `${imp.package}::${imp.named}`;
263
+ if (!seenImportKeys.has(key)) {
264
+ seenImportKeys.add(key);
265
+ uniqueImports.push(imp);
266
+ }
267
+ }
268
+
269
+ // Generate import statements, omitting redundant "as Alias" when named === alias
270
+ const importLines = uniqueImports.map((imp) => {
271
+ // Simplify import when named === alias (e.g., `import type { Vector }` instead of `{ Vector as Vector }`)
272
+ const importClause = imp.named === imp.alias ? imp.named : `${imp.named} as ${imp.alias}`;
273
+ return `import type { ${importClause} } from '${imp.package}';`;
274
+ });
275
+
276
+ // Only intersect actual codec/operation type maps. Extra type-only imports (e.g. Vector<N>) are
277
+ // included in importLines via codecTypeImports but must not be intersected into CodecTypes.
278
+ const codecTypes = codecTypeImports
279
+ .filter((imp) => imp.named === 'CodecTypes')
280
+ .map((imp) => imp.alias)
281
+ .join(' & ');
282
+ const operationTypes = operationTypeImports
283
+ .filter((imp) => imp.named === 'OperationTypes')
284
+ .map((imp) => imp.alias)
285
+ .join(' & ');
286
+
212
287
  const storageType = this.generateStorageType(storage);
213
- const modelsType = this.generateModelsType(models, storage);
288
+ const modelsType = this.generateModelsType(models, storage, parameterizedRenderers);
214
289
  const relationsType = this.generateRelationsType(ir.relations);
215
290
  const mappingsType = this.generateMappingsType(models, storage, codecTypes, operationTypes);
216
291
 
217
- return `// ⚠️ GENERATED FILE - DO NOT EDIT
218
- // This file is automatically generated by 'prisma-next emit'.
219
- // To regenerate, run: prisma-next emit
220
- ${importLines.join('\n')}
221
-
222
- import type { SqlContract, SqlStorage, SqlMappings, ModelDefinition } from '@prisma-next/sql-contract/types';
223
-
224
- export type CodecTypes = ${codecTypes || 'Record<string, never>'};
225
- export type LaneCodecTypes = CodecTypes;
226
- export type OperationTypes = ${operationTypes || 'Record<string, never>'};
292
+ const executionHashType = hashes.executionHash
293
+ ? `ExecutionHashBase<'${hashes.executionHash}'>`
294
+ : 'ExecutionHashBase<string>';
227
295
 
228
- export type Contract = SqlContract<
296
+ return `// ⚠️ GENERATED FILE - DO NOT EDIT
297
+ // This file is automatically generated by 'prisma-next contract emit'.
298
+ // To regenerate, run: prisma-next contract emit
299
+ ${importLines.join('\n')}
300
+
301
+ import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from '@prisma-next/contract/types';
302
+ import type { SqlContract, SqlStorage, SqlMappings, ModelDefinition } from '@prisma-next/sql-contract/types';
303
+
304
+ export type StorageHash = StorageHashBase<'${hashes.storageHash}'>;
305
+ export type ExecutionHash = ${executionHashType};
306
+ export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;
307
+
308
+ export type CodecTypes = ${codecTypes || 'Record<string, never>'};
309
+ export type LaneCodecTypes = CodecTypes;
310
+ export type OperationTypes = ${operationTypes || 'Record<string, never>'};
311
+ type DefaultLiteralValue<CodecId extends string, Encoded> =
312
+ CodecId extends keyof CodecTypes
313
+ ? CodecTypes[CodecId] extends { readonly output: infer O }
314
+ ? O extends Date | bigint ? O : Encoded
315
+ : Encoded
316
+ : Encoded;
317
+
318
+ export type Contract = SqlContract<
229
319
  ${storageType},
230
320
  ${modelsType},
231
321
  ${relationsType},
232
- ${mappingsType}
233
- >;
234
-
235
- export type Tables = Contract['storage']['tables'];
236
- export type Models = Contract['models'];
237
- export type Relations = Contract['relations'];
238
- `;
322
+ ${mappingsType},
323
+ StorageHash,
324
+ ExecutionHash,
325
+ ProfileHash
326
+ >;
327
+
328
+ export type Tables = Contract['storage']['tables'];
329
+ export type Models = Contract['models'];
330
+ export type Relations = Contract['relations'];
331
+ `;
239
332
  },
240
333
 
241
334
  generateStorageType(storage: SqlStorage): string {
@@ -246,8 +339,17 @@ export type Relations = Contract['relations'];
246
339
  const nullable = col.nullable ? 'true' : 'false';
247
340
  const nativeType = `'${col.nativeType}'`;
248
341
  const codecId = `'${col.codecId}'`;
342
+ const defaultSpec = col.default
343
+ ? col.default.kind === 'literal'
344
+ ? `; readonly default: { readonly kind: 'literal'; readonly value: DefaultLiteralValue<${codecId}, ${this.serializeValue(
345
+ col.default.value,
346
+ )}> }`
347
+ : `; readonly default: { readonly kind: 'function'; readonly expression: ${this.serializeValue(
348
+ col.default.expression,
349
+ )} }`
350
+ : '';
249
351
  columns.push(
250
- `readonly ${colName}: { readonly nativeType: ${nativeType}; readonly codecId: ${codecId}; readonly nullable: ${nullable} }`,
352
+ `readonly ${colName}: { readonly nativeType: ${nativeType}; readonly codecId: ${codecId}; readonly nullable: ${nullable}${defaultSpec} }`,
251
353
  );
252
354
  }
253
355
 
@@ -282,7 +384,7 @@ export type Relations = Contract['relations'];
282
384
  const cols = fk.columns.map((c: string) => `'${c}'`).join(', ');
283
385
  const refCols = fk.references.columns.map((c: string) => `'${c}'`).join(', ');
284
386
  const name = fk.name ? `; readonly name: '${fk.name}'` : '';
285
- return `{ readonly columns: readonly [${cols}]; readonly references: { readonly table: '${fk.references.table}'; readonly columns: readonly [${refCols}] }${name} }`;
387
+ return `{ readonly columns: readonly [${cols}]; readonly references: { readonly table: '${fk.references.table}'; readonly columns: readonly [${refCols}] }${name}; readonly constraint: ${fk.constraint}; readonly index: ${fk.index} }`;
286
388
  })
287
389
  .join(', ');
288
390
  tableParts.push(`foreignKeys: readonly [${fks}]`);
@@ -290,17 +392,97 @@ export type Relations = Contract['relations'];
290
392
  tables.push(`readonly ${tableName}: { ${tableParts.join('; ')} }`);
291
393
  }
292
394
 
293
- return `{ readonly tables: { ${tables.join('; ')} } }`;
395
+ const typesType = this.generateStorageTypesType(storage.types);
396
+
397
+ return `{ readonly tables: { ${tables.join('; ')} }; readonly types: ${typesType} }`;
398
+ },
399
+
400
+ /**
401
+ * Generates the TypeScript type for storage.types with literal types.
402
+ * This preserves type params as literal values for precise typing.
403
+ */
404
+ generateStorageTypesType(types: SqlStorage['types']): string {
405
+ if (!types || Object.keys(types).length === 0) {
406
+ return 'Record<string, never>';
407
+ }
408
+
409
+ const typeEntries: string[] = [];
410
+ for (const [typeName, typeInstance] of Object.entries(types)) {
411
+ const codecId = `'${typeInstance.codecId}'`;
412
+ const nativeType = `'${typeInstance.nativeType}'`;
413
+ const typeParamsStr = this.serializeTypeParamsLiteral(typeInstance.typeParams);
414
+ typeEntries.push(
415
+ `readonly ${typeName}: { readonly codecId: ${codecId}; readonly nativeType: ${nativeType}; readonly typeParams: ${typeParamsStr} }`,
416
+ );
417
+ }
418
+
419
+ return `{ ${typeEntries.join('; ')} }`;
420
+ },
421
+
422
+ /**
423
+ * Serializes a typeParams object to a TypeScript literal type.
424
+ * Converts { length: 1536 } to "{ readonly length: 1536 }".
425
+ */
426
+ serializeTypeParamsLiteral(params: Record<string, unknown>): string {
427
+ if (!params || Object.keys(params).length === 0) {
428
+ return 'Record<string, never>';
429
+ }
430
+
431
+ const entries: string[] = [];
432
+ for (const [key, value] of Object.entries(params)) {
433
+ const serialized = this.serializeValue(value);
434
+ entries.push(`readonly ${key}: ${serialized}`);
435
+ }
436
+
437
+ return `{ ${entries.join('; ')} }`;
438
+ },
439
+
440
+ /**
441
+ * Serializes a value to a TypeScript literal type expression.
442
+ */
443
+ serializeValue(value: unknown): string {
444
+ if (value === null) {
445
+ return 'null';
446
+ }
447
+ if (value === undefined) {
448
+ return 'undefined';
449
+ }
450
+ if (typeof value === 'string') {
451
+ // Escape backslashes first, then single quotes
452
+ const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
453
+ return `'${escaped}'`;
454
+ }
455
+ if (typeof value === 'number' || typeof value === 'boolean') {
456
+ return String(value);
457
+ }
458
+ if (typeof value === 'bigint') {
459
+ return `${value}n`;
460
+ }
461
+ if (Array.isArray(value)) {
462
+ const items = value.map((v) => this.serializeValue(v)).join(', ');
463
+ return `readonly [${items}]`;
464
+ }
465
+ if (typeof value === 'object') {
466
+ const entries: string[] = [];
467
+ for (const [k, v] of Object.entries(value)) {
468
+ entries.push(`readonly ${k}: ${this.serializeValue(v)}`);
469
+ }
470
+ return `{ ${entries.join('; ')} }`;
471
+ }
472
+ return 'unknown';
294
473
  },
295
474
 
296
475
  generateModelsType(
297
476
  models: Record<string, ModelDefinition> | undefined,
298
477
  storage: SqlStorage,
478
+ parameterizedRenderers?: Map<string, TypeRenderEntry>,
299
479
  ): string {
300
480
  if (!models) {
301
481
  return 'Record<string, never>';
302
482
  }
303
483
 
484
+ const renderCtx: TypeRenderContext = { codecTypesName: 'CodecTypes' };
485
+
304
486
  const modelTypes: string[] = [];
305
487
  for (const [modelName, model] of Object.entries(models)) {
306
488
  const fields: string[] = [];
@@ -315,12 +497,12 @@ export type Relations = Contract['relations'];
315
497
  continue;
316
498
  }
317
499
 
318
- const typeId = column.codecId;
319
- const nullable = column.nullable ?? false;
320
- const jsType = nullable
321
- ? `CodecTypes['${typeId}']['output'] | null`
322
- : `CodecTypes['${typeId}']['output']`;
323
-
500
+ const jsType = this.generateColumnType(
501
+ column,
502
+ storage,
503
+ parameterizedRenderers,
504
+ renderCtx,
505
+ );
324
506
  fields.push(`readonly ${fieldName}: ${jsType}`);
325
507
  }
326
508
  } else {
@@ -358,6 +540,26 @@ export type Relations = Contract['relations'];
358
540
  return `{ ${modelTypes.join('; ')} }`;
359
541
  },
360
542
 
543
+ /**
544
+ * Generates the TypeScript type expression for a column.
545
+ * Uses parameterized renderer if the column has typeParams and a matching renderer exists,
546
+ * otherwise falls back to CodecTypes[codecId]['output'].
547
+ */
548
+ generateColumnType(
549
+ column: StorageColumn,
550
+ storage: SqlStorage,
551
+ parameterizedRenderers: Map<string, TypeRenderEntry> | undefined,
552
+ renderCtx: TypeRenderContext,
553
+ ): string {
554
+ const typeParams = resolveColumnTypeParams(column, storage);
555
+ const nullable = column.nullable ?? false;
556
+ const fallbackType = `CodecTypes['${column.codecId}']['output']`;
557
+ const renderer = typeParams && parameterizedRenderers?.get(column.codecId);
558
+ const baseType = renderer ? renderer.render(typeParams, renderCtx) : fallbackType;
559
+
560
+ return nullable ? `${baseType} | null` : baseType;
561
+ },
562
+
361
563
  generateRelationsType(relations: Record<string, unknown> | undefined): string {
362
564
  if (!relations || Object.keys(relations).length === 0) {
363
565
  return 'Record<string, never>';
package/dist/index.d.ts DELETED
@@ -1,14 +0,0 @@
1
- import type { ContractIR } from '@prisma-next/contract/ir';
2
- import type { TypesImportSpec, ValidationContext } from '@prisma-next/contract/types';
3
- import type { ModelDefinition, SqlStorage } from '@prisma-next/sql-contract/types';
4
- export declare const sqlTargetFamilyHook: {
5
- readonly id: "sql";
6
- readonly validateTypes: (ir: ContractIR, _ctx: ValidationContext) => void;
7
- readonly validateStructure: (ir: ContractIR) => void;
8
- readonly generateContractTypes: (ir: ContractIR, codecTypeImports: ReadonlyArray<TypesImportSpec>, operationTypeImports: ReadonlyArray<TypesImportSpec>) => string;
9
- readonly generateStorageType: (storage: SqlStorage) => string;
10
- readonly generateModelsType: (models: Record<string, ModelDefinition> | undefined, storage: SqlStorage) => string;
11
- readonly generateRelationsType: (relations: Record<string, unknown> | undefined) => string;
12
- readonly generateMappingsType: (models: Record<string, ModelDefinition> | undefined, storage: SqlStorage, codecTypes: string, operationTypes: string) => string;
13
- };
14
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,KAAK,EACV,eAAe,EAEf,UAAU,EAEX,MAAM,iCAAiC,CAAC;AAEzC,eAAO,MAAM,mBAAmB;;iCAGZ,UAAU,QAAQ,iBAAiB,KAAG,IAAI;qCA6BtC,UAAU,KAAG,IAAI;yCA2JjC,UAAU,oBACI,aAAa,CAAC,eAAe,CAAC,wBAC1B,aAAa,CAAC,eAAe,CAAC,KACnD,MAAM;4CAyCoB,UAAU,KAAG,MAAM;0CAwDtC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SAAS,WAC1C,UAAU,KAClB,MAAM;gDA8DwB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,MAAM;4CAkEnE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SAAS,WAC1C,UAAU,cACP,MAAM,kBACF,MAAM,KACrB,MAAM;CAsDD,CAAC"}