@remix-run/data-table 0.0.0 → 0.1.0

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -2
  3. package/dist/index.d.ts +11 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +5 -0
  6. package/dist/lib/adapter.d.ts +180 -0
  7. package/dist/lib/adapter.d.ts.map +1 -0
  8. package/dist/lib/adapter.js +1 -0
  9. package/dist/lib/database.d.ts +361 -0
  10. package/dist/lib/database.d.ts.map +1 -0
  11. package/dist/lib/database.js +1368 -0
  12. package/dist/lib/errors.d.ts +50 -0
  13. package/dist/lib/errors.d.ts.map +1 -0
  14. package/dist/lib/errors.js +67 -0
  15. package/dist/lib/inflection.d.ts +3 -0
  16. package/dist/lib/inflection.d.ts.map +1 -0
  17. package/dist/lib/inflection.js +56 -0
  18. package/dist/lib/operators.d.ts +151 -0
  19. package/dist/lib/operators.d.ts.map +1 -0
  20. package/dist/lib/operators.js +218 -0
  21. package/dist/lib/references.d.ts +42 -0
  22. package/dist/lib/references.d.ts.map +1 -0
  23. package/dist/lib/references.js +33 -0
  24. package/dist/lib/sql.d.ts +28 -0
  25. package/dist/lib/sql.d.ts.map +1 -0
  26. package/dist/lib/sql.js +51 -0
  27. package/dist/lib/table.d.ts +254 -0
  28. package/dist/lib/table.d.ts.map +1 -0
  29. package/dist/lib/table.js +496 -0
  30. package/dist/lib/types.d.ts +4 -0
  31. package/dist/lib/types.d.ts.map +1 -0
  32. package/dist/lib/types.js +1 -0
  33. package/package.json +41 -7
  34. package/src/index.ts +115 -0
  35. package/src/lib/adapter.ts +209 -0
  36. package/src/lib/database.ts +2458 -0
  37. package/src/lib/errors.ts +109 -0
  38. package/src/lib/inflection.ts +69 -0
  39. package/src/lib/operators.ts +433 -0
  40. package/src/lib/references.ts +79 -0
  41. package/src/lib/sql.ts +67 -0
  42. package/src/lib/table.ts +981 -0
  43. package/src/lib/types.ts +3 -0
@@ -0,0 +1,981 @@
1
+ import { createSchema, parseSafe } from '@remix-run/data-schema'
2
+ import type { InferInput, InferOutput, Issue, ParseOptions, Schema } from '@remix-run/data-schema'
3
+ import type { Predicate, WhereInput } from './operators.ts'
4
+ import { inferForeignKey } from './inflection.ts'
5
+ import { normalizeWhereInput } from './operators.ts'
6
+ import { columnMetadataKey, normalizeColumnInput, tableMetadataKey } from './references.ts'
7
+ import type { ColumnInput, ColumnReferenceLike, TableMetadataLike } from './references.ts'
8
+ import type { Pretty } from './types.ts'
9
+
10
+ /**
11
+ * Symbol key used to store non-enumerable table metadata.
12
+ */
13
+ export { columnMetadataKey, tableMetadataKey } from './references.ts'
14
+
15
+ /**
16
+ * Mapping of column names to schemas.
17
+ */
18
+ export type ColumnSchemas = Record<string, Schema<any, any>>
19
+
20
+ type ColumnNameFromColumns<columns extends ColumnSchemas> = keyof columns & string
21
+
22
+ type DefaultPrimaryKey<columns extends ColumnSchemas> =
23
+ 'id' extends ColumnNameFromColumns<columns>
24
+ ? readonly ['id']
25
+ : readonly ColumnNameFromColumns<columns>[]
26
+
27
+ type NormalizePrimaryKey<
28
+ columns extends ColumnSchemas,
29
+ primaryKey extends
30
+ | ColumnNameFromColumns<columns>
31
+ | readonly ColumnNameFromColumns<columns>[]
32
+ | undefined,
33
+ > = primaryKey extends readonly (infer column extends ColumnNameFromColumns<columns>)[]
34
+ ? readonly [...column[]]
35
+ : primaryKey extends ColumnNameFromColumns<columns>
36
+ ? readonly [primaryKey]
37
+ : DefaultPrimaryKey<columns>
38
+
39
+ export type TimestampOptions = boolean | { createdAt?: string; updatedAt?: string }
40
+
41
+ export type TimestampConfig = {
42
+ createdAt: string
43
+ updatedAt: string
44
+ }
45
+
46
+ type TableMetadata<
47
+ name extends string,
48
+ columns extends ColumnSchemas,
49
+ primaryKey extends readonly ColumnNameFromColumns<columns>[],
50
+ > = {
51
+ name: name
52
+ columns: columns
53
+ primaryKey: primaryKey
54
+ timestamps: TimestampConfig | null
55
+ }
56
+
57
+ export type ColumnReference<
58
+ tableName extends string,
59
+ columnName extends string,
60
+ schema extends Schema<any, any>,
61
+ > = ColumnReferenceLike<`${tableName}.${columnName}`> & {
62
+ [columnMetadataKey]: {
63
+ tableName: tableName
64
+ columnName: columnName
65
+ qualifiedName: `${tableName}.${columnName}`
66
+ schema: schema
67
+ }
68
+ }
69
+
70
+ export type AnyColumn = ColumnReference<string, string, Schema<any, any>>
71
+
72
+ export type ColumnReferenceForQualifiedName<qualifiedName extends string> = AnyColumn & {
73
+ [columnMetadataKey]: {
74
+ qualifiedName: qualifiedName
75
+ }
76
+ }
77
+
78
+ type TableColumnReferences<name extends string, columns extends ColumnSchemas> = {
79
+ [column in keyof columns & string]: ColumnReference<name, column, columns[column]>
80
+ }
81
+
82
+ type TableParseOutput<columns extends ColumnSchemas> = Partial<{
83
+ [column in keyof columns & string]: InferOutput<columns[column]>
84
+ }>
85
+
86
+ export type Table<
87
+ name extends string,
88
+ columns extends ColumnSchemas,
89
+ primaryKey extends readonly ColumnNameFromColumns<columns>[],
90
+ > = TableMetadataLike<name, columns, primaryKey, TimestampConfig | null> & {
91
+ [tableMetadataKey]: TableMetadata<name, columns, primaryKey>
92
+ '~standard': Schema<unknown, TableParseOutput<columns>>['~standard']
93
+ } & TableColumnReferences<name, columns>
94
+
95
+ export type AnyTable = TableMetadataLike<
96
+ string,
97
+ ColumnSchemas,
98
+ readonly string[],
99
+ TimestampConfig | null
100
+ > & {
101
+ [tableMetadataKey]: {
102
+ name: string
103
+ columns: ColumnSchemas
104
+ primaryKey: readonly string[]
105
+ timestamps: TimestampConfig | null
106
+ }
107
+ '~standard': Schema<unknown, Partial<Record<string, unknown>>>['~standard']
108
+ } & Record<string, unknown>
109
+
110
+ export type TableName<table extends AnyTable> = table[typeof tableMetadataKey]['name']
111
+
112
+ export type TableColumns<table extends AnyTable> = table[typeof tableMetadataKey]['columns']
113
+
114
+ export type TablePrimaryKey<table extends AnyTable> = table[typeof tableMetadataKey]['primaryKey']
115
+
116
+ export type TableTimestamps<table extends AnyTable> = table[typeof tableMetadataKey]['timestamps']
117
+
118
+ export type TableRow<table extends AnyTable> = Pretty<{
119
+ [column in keyof TableColumns<table> & string]: InferOutput<TableColumns<table>[column]>
120
+ }>
121
+
122
+ export type TableRowWith<
123
+ table extends AnyTable,
124
+ loaded extends Record<string, unknown> = {},
125
+ > = Pretty<TableRow<table> & loaded>
126
+
127
+ export type TableColumnName<table extends AnyTable> = keyof TableColumns<table> & string
128
+
129
+ export type QualifiedTableColumnName<table extends AnyTable> =
130
+ `${TableName<table>}.${TableColumnName<table>}`
131
+
132
+ export type TableColumnInput<table extends AnyTable> = ColumnInput<
133
+ TableColumnName<table> | QualifiedTableColumnName<table>
134
+ >
135
+
136
+ export type TableReference<table extends AnyTable = AnyTable> = {
137
+ kind: 'table'
138
+ name: TableName<table>
139
+ columns: TableColumns<table>
140
+ primaryKey: TablePrimaryKey<table>
141
+ timestamps: TableTimestamps<table>
142
+ }
143
+
144
+ /**
145
+ * Creates a plain table reference snapshot from a table instance.
146
+ * @param table Source table instance.
147
+ * @returns Table metadata snapshot.
148
+ */
149
+ export function getTableReference<table extends AnyTable>(table: table): TableReference<table> {
150
+ let metadata = table[tableMetadataKey]
151
+
152
+ return {
153
+ kind: 'table',
154
+ name: metadata.name as TableName<table>,
155
+ columns: metadata.columns as TableColumns<table>,
156
+ primaryKey: metadata.primaryKey as TablePrimaryKey<table>,
157
+ timestamps: metadata.timestamps as TableTimestamps<table>,
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Returns a table's SQL name.
163
+ * @param table Source table instance.
164
+ * @returns Table SQL name.
165
+ */
166
+ export function getTableName<table extends AnyTable>(table: table): TableName<table> {
167
+ return table[tableMetadataKey].name as TableName<table>
168
+ }
169
+
170
+ /**
171
+ * Returns a table's schema map.
172
+ * @param table Source table instance.
173
+ * @returns Table schema map.
174
+ */
175
+ export function getTableColumns<table extends AnyTable>(table: table): TableColumns<table> {
176
+ return table[tableMetadataKey].columns as TableColumns<table>
177
+ }
178
+
179
+ /**
180
+ * Returns a table's primary key columns.
181
+ * @param table Source table instance.
182
+ * @returns Primary key columns.
183
+ */
184
+ export function getTablePrimaryKey<table extends AnyTable>(table: table): TablePrimaryKey<table> {
185
+ return table[tableMetadataKey].primaryKey as TablePrimaryKey<table>
186
+ }
187
+
188
+ /**
189
+ * Returns a table's resolved timestamp configuration.
190
+ * @param table Source table instance.
191
+ * @returns Timestamp configuration or `null`.
192
+ */
193
+ export function getTableTimestamps<table extends AnyTable>(table: table): TableTimestamps<table> {
194
+ return table[tableMetadataKey].timestamps as TableTimestamps<table>
195
+ }
196
+
197
+ export type OrderDirection = 'asc' | 'desc'
198
+
199
+ export type OrderByClause = {
200
+ column: string
201
+ direction: OrderDirection
202
+ }
203
+
204
+ export type RelationCardinality = 'one' | 'many'
205
+
206
+ export type RelationKind = 'hasMany' | 'hasOne' | 'belongsTo' | 'hasManyThrough'
207
+
208
+ export type RelationResult<relation extends AnyRelation> =
209
+ relation extends Relation<any, infer target, infer cardinality, infer loaded>
210
+ ? cardinality extends 'many'
211
+ ? Array<TableRowWith<target, loaded>>
212
+ : TableRowWith<target, loaded> | null
213
+ : never
214
+
215
+ export type RelationMapForTable<table extends AnyTable> = Record<
216
+ string,
217
+ Relation<table, AnyTable, RelationCardinality, any>
218
+ >
219
+
220
+ export type LoadedRelationMap<relations extends RelationMapForTable<any>> = Pretty<{
221
+ [name in keyof relations]: RelationResult<relations[name]>
222
+ }>
223
+
224
+ export type KeySelector<table extends AnyTable> =
225
+ | (keyof TableRow<table> & string)
226
+ | readonly (keyof TableRow<table> & string)[]
227
+
228
+ export type HasManyOptions<source extends AnyTable, target extends AnyTable> = {
229
+ foreignKey?: KeySelector<target>
230
+ targetKey?: KeySelector<source>
231
+ }
232
+
233
+ export type HasOneOptions<source extends AnyTable, target extends AnyTable> = {
234
+ foreignKey?: KeySelector<target>
235
+ targetKey?: KeySelector<source>
236
+ }
237
+
238
+ export type BelongsToOptions<source extends AnyTable, target extends AnyTable> = {
239
+ foreignKey?: KeySelector<source>
240
+ targetKey?: KeySelector<target>
241
+ }
242
+
243
+ export type HasManyThroughOptions<source extends AnyTable, target extends AnyTable> = {
244
+ through: Relation<source, AnyTable, RelationCardinality, any>
245
+ throughForeignKey?: KeySelector<target>
246
+ throughTargetKey?: string | string[]
247
+ }
248
+
249
+ export type RelationModifiers<target extends AnyTable> = {
250
+ where: Predicate[]
251
+ orderBy: OrderByClause[]
252
+ limit?: number
253
+ offset?: number
254
+ with: RelationMapForTable<target>
255
+ }
256
+
257
+ export type ThroughRelationMetadata = {
258
+ relation: AnyRelation
259
+ throughSourceKey: string[]
260
+ throughTargetKey: string[]
261
+ }
262
+
263
+ export type Relation<
264
+ source extends AnyTable,
265
+ target extends AnyTable,
266
+ cardinality extends RelationCardinality,
267
+ loaded extends Record<string, unknown> = {},
268
+ > = {
269
+ kind: 'relation'
270
+ relationKind: RelationKind
271
+ sourceTable: source
272
+ targetTable: target
273
+ cardinality: cardinality
274
+ sourceKey: string[]
275
+ targetKey: string[]
276
+ through?: ThroughRelationMetadata
277
+ modifiers: RelationModifiers<target>
278
+ where(
279
+ input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>,
280
+ ): Relation<source, target, cardinality, loaded>
281
+ orderBy(
282
+ column: TableColumnInput<target>,
283
+ direction?: OrderDirection,
284
+ ): Relation<source, target, cardinality, loaded>
285
+ limit(value: number): Relation<source, target, cardinality, loaded>
286
+ offset(value: number): Relation<source, target, cardinality, loaded>
287
+ with<relations extends RelationMapForTable<target>>(
288
+ relations: relations,
289
+ ): Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>
290
+ }
291
+
292
+ export type AnyRelation = Relation<AnyTable, AnyTable, RelationCardinality, any>
293
+
294
+ export type CreateTableOptions<
295
+ name extends string,
296
+ columns extends ColumnSchemas,
297
+ primaryKey extends
298
+ | ColumnNameFromColumns<columns>
299
+ | readonly ColumnNameFromColumns<columns>[]
300
+ | undefined,
301
+ > = {
302
+ name: name
303
+ columns: columns
304
+ primaryKey?: primaryKey
305
+ timestamps?: TimestampOptions
306
+ }
307
+
308
+ let defaultTimestampConfig: TimestampConfig = {
309
+ createdAt: 'created_at',
310
+ updatedAt: 'updated_at',
311
+ }
312
+
313
+ function prefixIssuePath(issue: Issue, key: string): Issue {
314
+ let issuePath = issue.path ?? []
315
+ return {
316
+ ...issue,
317
+ path: [key, ...issuePath],
318
+ }
319
+ }
320
+
321
+ function validatePartialRowInput<columns extends ColumnSchemas>(
322
+ tableName: string,
323
+ columns: columns,
324
+ value: unknown,
325
+ options?: ParseOptions,
326
+ ): { value: TableParseOutput<columns> } | { issues: ReadonlyArray<Issue> } {
327
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
328
+ return {
329
+ issues: [{ message: 'Expected object' }],
330
+ }
331
+ }
332
+
333
+ let input = value as Record<string, unknown>
334
+ let output: Record<string, unknown> = {}
335
+ let issues: Issue[] = []
336
+
337
+ for (let key in input) {
338
+ if (!Object.prototype.hasOwnProperty.call(input, key)) {
339
+ continue
340
+ }
341
+
342
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
343
+ issues.push({
344
+ message: 'Unknown column "' + key + '" for table "' + tableName + '"',
345
+ path: [key],
346
+ })
347
+ continue
348
+ }
349
+
350
+ let result = parseSafe(columns[key], input[key], options)
351
+
352
+ if (!result.success) {
353
+ issues.push(...result.issues.map((issue) => prefixIssuePath(issue, key)))
354
+ continue
355
+ }
356
+
357
+ output[key] = result.value
358
+ }
359
+
360
+ if (issues.length > 0) {
361
+ return { issues }
362
+ }
363
+
364
+ return { value: output as TableParseOutput<columns> }
365
+ }
366
+
367
+ export function validatePartialRow<table extends AnyTable>(
368
+ table: table,
369
+ value: unknown,
370
+ options?: ParseOptions,
371
+ ): { value: Partial<TableRow<table>> } | { issues: ReadonlyArray<Issue> } {
372
+ let result = validatePartialRowInput(getTableName(table), getTableColumns(table), value, options)
373
+
374
+ if ('issues' in result) {
375
+ return result
376
+ }
377
+
378
+ return {
379
+ value: result.value as Partial<TableRow<table>>,
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Creates a table object with symbol-backed metadata and direct column references.
385
+ * @param options Table declaration options.
386
+ * @returns A frozen table object.
387
+ */
388
+ export function createTable<
389
+ name extends string,
390
+ columns extends ColumnSchemas,
391
+ primaryKey extends
392
+ | ColumnNameFromColumns<columns>
393
+ | readonly ColumnNameFromColumns<columns>[]
394
+ | undefined = undefined,
395
+ >(
396
+ options: CreateTableOptions<name, columns, primaryKey>,
397
+ ): Table<name, columns, NormalizePrimaryKey<columns, primaryKey>> {
398
+ let tableName = options.name
399
+ let columns = options.columns
400
+
401
+ if (Object.prototype.hasOwnProperty.call(columns, '~standard')) {
402
+ throw new Error(
403
+ 'Column name "~standard" is reserved for table validation on "' + tableName + '"',
404
+ )
405
+ }
406
+
407
+ let resolvedPrimaryKey = normalizePrimaryKey(tableName, columns, options.primaryKey)
408
+ let timestampConfig = normalizeTimestampConfig(options.timestamps)
409
+ let table = Object.create(null) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
410
+
411
+ Object.defineProperty(table, tableMetadataKey, {
412
+ value: Object.freeze({
413
+ name: tableName,
414
+ columns,
415
+ primaryKey: resolvedPrimaryKey,
416
+ timestamps: timestampConfig,
417
+ }),
418
+ enumerable: false,
419
+ writable: false,
420
+ configurable: false,
421
+ })
422
+
423
+ Object.defineProperty(table, '~standard', {
424
+ value: Object.freeze({
425
+ version: 1,
426
+ vendor: 'data-table',
427
+ validate(value: unknown, parseOptions?: ParseOptions) {
428
+ return validatePartialRowInput(tableName, columns, value, parseOptions)
429
+ },
430
+ }),
431
+ enumerable: false,
432
+ writable: false,
433
+ configurable: false,
434
+ })
435
+
436
+ for (let columnName in columns) {
437
+ if (!Object.prototype.hasOwnProperty.call(columns, columnName)) {
438
+ continue
439
+ }
440
+
441
+ let schema = columns[columnName]
442
+ let column = createColumnReference(tableName, columnName, schema)
443
+
444
+ Object.defineProperty(table, columnName, {
445
+ value: column,
446
+ enumerable: true,
447
+ writable: false,
448
+ configurable: false,
449
+ })
450
+ }
451
+
452
+ return Object.freeze(table) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
453
+ }
454
+
455
+ function createColumnReference<
456
+ tableName extends string,
457
+ columnName extends string,
458
+ schema extends Schema<any, any>,
459
+ >(
460
+ tableName: tableName,
461
+ columnName: columnName,
462
+ schema: schema,
463
+ ): ColumnReference<tableName, columnName, schema> {
464
+ return Object.freeze({
465
+ kind: 'column',
466
+ [columnMetadataKey]: Object.freeze({
467
+ tableName,
468
+ columnName,
469
+ qualifiedName: tableName + '.' + columnName,
470
+ schema,
471
+ }),
472
+ }) as ColumnReference<tableName, columnName, schema>
473
+ }
474
+
475
+ /**
476
+ * Defines a one-to-many relation from `source` to `target`.
477
+ * @param source Source table.
478
+ * @param target Target table.
479
+ * @param relationOptions Relation key configuration.
480
+ * @returns A relation descriptor.
481
+ */
482
+ export function hasMany<source extends AnyTable, target extends AnyTable>(
483
+ source: source,
484
+ target: target,
485
+ relationOptions?: HasManyOptions<source, target>,
486
+ ): Relation<source, target, 'many'> {
487
+ let sourceKey = normalizeKeySelector(
488
+ source,
489
+ relationOptions?.targetKey,
490
+ 'targetKey',
491
+ getTablePrimaryKey(source) as string[],
492
+ )
493
+ let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [
494
+ inferForeignKey(getTableName(source)),
495
+ ])
496
+
497
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
498
+
499
+ return createRelation({
500
+ relationKind: 'hasMany',
501
+ cardinality: 'many',
502
+ sourceTable: source,
503
+ targetTable: target,
504
+ sourceKey,
505
+ targetKey,
506
+ })
507
+ }
508
+
509
+ /**
510
+ * Defines a one-to-one relation from `source` to `target` where the foreign key lives on `target`.
511
+ * @param source Source table.
512
+ * @param target Target table.
513
+ * @param relationOptions Relation key configuration.
514
+ * @returns A relation descriptor.
515
+ */
516
+ export function hasOne<source extends AnyTable, target extends AnyTable>(
517
+ source: source,
518
+ target: target,
519
+ relationOptions?: HasOneOptions<source, target>,
520
+ ): Relation<source, target, 'one'> {
521
+ let sourceKey = normalizeKeySelector(
522
+ source,
523
+ relationOptions?.targetKey,
524
+ 'targetKey',
525
+ getTablePrimaryKey(source) as string[],
526
+ )
527
+ let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [
528
+ inferForeignKey(getTableName(source)),
529
+ ])
530
+
531
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
532
+
533
+ return createRelation({
534
+ relationKind: 'hasOne',
535
+ cardinality: 'one',
536
+ sourceTable: source,
537
+ targetTable: target,
538
+ sourceKey,
539
+ targetKey,
540
+ })
541
+ }
542
+
543
+ /**
544
+ * Defines a one-to-one relation from `source` to `target`.
545
+ * @param source Source table.
546
+ * @param target Target table.
547
+ * @param relationOptions Relation key configuration.
548
+ * @returns A relation descriptor.
549
+ */
550
+ export function belongsTo<source extends AnyTable, target extends AnyTable>(
551
+ source: source,
552
+ target: target,
553
+ relationOptions?: BelongsToOptions<source, target>,
554
+ ): Relation<source, target, 'one'> {
555
+ let sourceKey = normalizeKeySelector(source, relationOptions?.foreignKey, 'foreignKey', [
556
+ inferForeignKey(getTableName(target)),
557
+ ])
558
+ let targetKey = normalizeKeySelector(
559
+ target,
560
+ relationOptions?.targetKey,
561
+ 'targetKey',
562
+ getTablePrimaryKey(target) as string[],
563
+ )
564
+
565
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
566
+
567
+ return createRelation({
568
+ relationKind: 'belongsTo',
569
+ cardinality: 'one',
570
+ sourceTable: source,
571
+ targetTable: target,
572
+ sourceKey,
573
+ targetKey,
574
+ })
575
+ }
576
+
577
+ /**
578
+ * Defines a one-to-many relation from `source` to `target` through an intermediate relation.
579
+ * @param source Source table.
580
+ * @param target Target table.
581
+ * @param relationOptions Through relation configuration.
582
+ * @returns A relation descriptor.
583
+ */
584
+ export function hasManyThrough<source extends AnyTable, target extends AnyTable>(
585
+ source: source,
586
+ target: target,
587
+ relationOptions: HasManyThroughOptions<source, target>,
588
+ ): Relation<source, target, 'many'> {
589
+ let throughRelation = relationOptions.through
590
+
591
+ if (throughRelation.sourceTable !== source) {
592
+ throw new Error(
593
+ 'hasManyThrough expects a through relation whose source table matches ' +
594
+ getTableName(source),
595
+ )
596
+ }
597
+
598
+ let throughTargetKey = normalizeKeysForTable(
599
+ throughRelation.targetTable,
600
+ relationOptions.throughTargetKey,
601
+ 'throughTargetKey',
602
+ getTablePrimaryKey(throughRelation.targetTable),
603
+ )
604
+ let throughForeignKey = normalizeKeySelector(
605
+ target,
606
+ relationOptions.throughForeignKey,
607
+ 'throughForeignKey',
608
+ [inferForeignKey(getTableName(throughRelation.targetTable))],
609
+ )
610
+
611
+ assertKeyLengths(
612
+ getTableName(throughRelation.targetTable),
613
+ getTableName(target),
614
+ throughTargetKey,
615
+ throughForeignKey,
616
+ )
617
+
618
+ return createRelation({
619
+ relationKind: 'hasManyThrough',
620
+ cardinality: 'many',
621
+ sourceTable: source,
622
+ targetTable: target,
623
+ sourceKey: [...throughRelation.sourceKey],
624
+ targetKey: [...throughRelation.targetKey],
625
+ through: {
626
+ relation: throughRelation as AnyRelation,
627
+ throughSourceKey: throughTargetKey,
628
+ throughTargetKey: throughForeignKey,
629
+ },
630
+ })
631
+ }
632
+
633
+ /**
634
+ * Creates a schema that accepts `Date`, string, and numeric timestamp inputs.
635
+ * @returns Timestamp schema for generated timestamp helpers.
636
+ */
637
+ export function timestampSchema(): Schema<unknown, Date | string | number> {
638
+ return createSchema<unknown, Date | string | number>((value) => {
639
+ if (value instanceof Date) {
640
+ return { value }
641
+ }
642
+
643
+ if (typeof value === 'string' || typeof value === 'number') {
644
+ return { value }
645
+ }
646
+
647
+ return {
648
+ issues: [{ message: 'Expected Date, string, or number' }],
649
+ }
650
+ })
651
+ }
652
+
653
+ let defaultTimestampSchema = timestampSchema()
654
+
655
+ /**
656
+ * Convenience helper for standard snake_case timestamp columns.
657
+ * @param schema Schema used for both timestamp columns.
658
+ * @returns Column schema map for `created_at`/`updated_at`.
659
+ */
660
+ export function timestamps(
661
+ schema: Schema<any, any> = defaultTimestampSchema,
662
+ ): Record<'created_at' | 'updated_at', Schema<any, any>> {
663
+ return {
664
+ created_at: schema,
665
+ updated_at: schema,
666
+ }
667
+ }
668
+
669
+ export type PrimaryKeyInput<table extends AnyTable> =
670
+ TablePrimaryKey<table> extends readonly [infer column extends string]
671
+ ? column extends keyof TableColumns<table> & string
672
+ ? InferInput<TableColumns<table>[column]>
673
+ : never
674
+ : Pretty<{
675
+ [column in TablePrimaryKey<table>[number] & keyof TableColumns<table> & string]: InferInput<
676
+ TableColumns<table>[column]
677
+ >
678
+ }>
679
+
680
+ /**
681
+ * Normalizes a primary-key input into an object keyed by primary-key columns.
682
+ * @param table Source table.
683
+ * @param value Primary-key input value.
684
+ * @returns Primary-key object.
685
+ */
686
+ export function getPrimaryKeyObject<table extends AnyTable>(
687
+ table: table,
688
+ value: PrimaryKeyInput<table>,
689
+ ): Partial<TableRow<table>> {
690
+ let keys = getTablePrimaryKey(table)
691
+
692
+ if (keys.length === 1 && (typeof value !== 'object' || value === null || Array.isArray(value))) {
693
+ let key = keys[0] as keyof TableRow<table>
694
+ return { [key]: value } as Partial<TableRow<table>>
695
+ }
696
+
697
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
698
+ throw new Error('Composite primary keys require an object value')
699
+ }
700
+
701
+ let objectValue = value as Record<string, unknown>
702
+ let output: Partial<TableRow<table>> = {}
703
+
704
+ for (let key of keys) {
705
+ if (!(key in objectValue)) {
706
+ throw new Error(
707
+ 'Missing key "' + key + '" for primary key lookup on "' + getTableName(table) + '"',
708
+ )
709
+ }
710
+
711
+ ;(output as Record<string, unknown>)[key] = objectValue[key]
712
+ }
713
+
714
+ return output
715
+ }
716
+
717
+ /**
718
+ * Builds a stable key for a row tuple.
719
+ * @param row Source row.
720
+ * @param columns Columns included in the tuple.
721
+ * @returns Stable tuple key.
722
+ */
723
+ export function getCompositeKey(row: Record<string, unknown>, columns: readonly string[]): string {
724
+ let values = columns.map((column) => stableSerialize(row[column]))
725
+
726
+ return values.join('::')
727
+ }
728
+
729
+ /**
730
+ * Serializes values into stable string representations for key generation.
731
+ * @param value Value to serialize.
732
+ * @returns Stable serialized value.
733
+ */
734
+ export function stableSerialize(value: unknown): string {
735
+ if (value === null) {
736
+ return 'null'
737
+ }
738
+
739
+ if (value === undefined) {
740
+ return 'undefined'
741
+ }
742
+
743
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
744
+ return String(value)
745
+ }
746
+
747
+ if (typeof value === 'string') {
748
+ return JSON.stringify(value)
749
+ }
750
+
751
+ if (value instanceof Date) {
752
+ return 'date:' + value.toISOString()
753
+ }
754
+
755
+ return JSON.stringify(value)
756
+ }
757
+
758
+ function normalizePrimaryKey(
759
+ tableName: string,
760
+ columns: ColumnSchemas,
761
+ primaryKey?: string | readonly string[],
762
+ ): string[] {
763
+ if (primaryKey === undefined) {
764
+ if (!Object.prototype.hasOwnProperty.call(columns, 'id')) {
765
+ throw new Error(
766
+ 'Table "' + tableName + '" must define an "id" column or an explicit primaryKey',
767
+ )
768
+ }
769
+
770
+ return ['id']
771
+ }
772
+
773
+ let keys = Array.isArray(primaryKey) ? [...primaryKey] : [primaryKey]
774
+
775
+ if (keys.length === 0) {
776
+ throw new Error('Table "' + tableName + '" primaryKey must contain at least one column')
777
+ }
778
+
779
+ for (let key of keys) {
780
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
781
+ throw new Error('Table "' + tableName + '" primaryKey column "' + key + '" does not exist')
782
+ }
783
+ }
784
+
785
+ return keys
786
+ }
787
+
788
+ function normalizeKeySelector<table extends AnyTable>(
789
+ table: table,
790
+ selector: KeySelector<table> | undefined,
791
+ optionName: string,
792
+ defaultValue: readonly string[],
793
+ ): string[] {
794
+ return normalizeKeysForTable(table, selector, optionName, defaultValue)
795
+ }
796
+
797
+ function normalizeKeysForTable(
798
+ table: AnyTable,
799
+ selector: string | readonly string[] | undefined,
800
+ optionName: string,
801
+ defaultValue: readonly string[],
802
+ ): string[] {
803
+ if (selector === undefined) {
804
+ return [...defaultValue]
805
+ }
806
+
807
+ let keys = Array.isArray(selector) ? [...selector] : [selector]
808
+
809
+ if (keys.length === 0) {
810
+ throw new Error(
811
+ 'Option "' + optionName + '" for table "' + getTableName(table) + '" must not be empty',
812
+ )
813
+ }
814
+
815
+ let columns = getTableColumns(table)
816
+
817
+ for (let key of keys) {
818
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
819
+ throw new Error(
820
+ 'Unknown column "' +
821
+ key +
822
+ '" in option "' +
823
+ optionName +
824
+ '" for table "' +
825
+ getTableName(table) +
826
+ '"',
827
+ )
828
+ }
829
+ }
830
+
831
+ return keys
832
+ }
833
+
834
+ function normalizeTimestampConfig(options: TimestampOptions | undefined): TimestampConfig | null {
835
+ if (!options) {
836
+ return null
837
+ }
838
+
839
+ if (options === true) {
840
+ return { ...defaultTimestampConfig }
841
+ }
842
+
843
+ return {
844
+ createdAt: options.createdAt ?? defaultTimestampConfig.createdAt,
845
+ updatedAt: options.updatedAt ?? defaultTimestampConfig.updatedAt,
846
+ }
847
+ }
848
+
849
+ function assertKeyLengths(
850
+ sourceTableName: string,
851
+ targetTableName: string,
852
+ sourceKey: string[],
853
+ targetKey: string[],
854
+ ): void {
855
+ if (sourceKey.length !== targetKey.length) {
856
+ throw new Error(
857
+ 'Relation key mismatch between "' +
858
+ sourceTableName +
859
+ '" (' +
860
+ sourceKey.join(', ') +
861
+ ') and "' +
862
+ targetTableName +
863
+ '" (' +
864
+ targetKey.join(', ') +
865
+ ')',
866
+ )
867
+ }
868
+ }
869
+
870
+ type CreateRelationOptions<
871
+ source extends AnyTable,
872
+ target extends AnyTable,
873
+ cardinality extends RelationCardinality,
874
+ > = {
875
+ relationKind: RelationKind
876
+ cardinality: cardinality
877
+ sourceTable: source
878
+ targetTable: target
879
+ sourceKey: string[]
880
+ targetKey: string[]
881
+ through?: ThroughRelationMetadata
882
+ modifiers?: Partial<RelationModifiers<target>>
883
+ }
884
+
885
+ function createRelation<
886
+ source extends AnyTable,
887
+ target extends AnyTable,
888
+ cardinality extends RelationCardinality,
889
+ loaded extends Record<string, unknown> = {},
890
+ >(
891
+ options: CreateRelationOptions<source, target, cardinality>,
892
+ ): Relation<source, target, cardinality, loaded> {
893
+ let baseModifiers: RelationModifiers<target> = {
894
+ where: options.modifiers?.where ? [...options.modifiers.where] : [],
895
+ orderBy: options.modifiers?.orderBy ? [...options.modifiers.orderBy] : [],
896
+ limit: options.modifiers?.limit,
897
+ offset: options.modifiers?.offset,
898
+ with: options.modifiers?.with ? { ...options.modifiers.with } : {},
899
+ }
900
+
901
+ let relation: Relation<source, target, cardinality, loaded> = {
902
+ kind: 'relation',
903
+ relationKind: options.relationKind,
904
+ sourceTable: options.sourceTable,
905
+ targetTable: options.targetTable,
906
+ cardinality: options.cardinality,
907
+ sourceKey: [...options.sourceKey],
908
+ targetKey: [...options.targetKey],
909
+ through: options.through,
910
+ modifiers: baseModifiers,
911
+
912
+ where(input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>) {
913
+ let predicate = normalizeWhereInput(input)
914
+ return cloneRelation(relation, {
915
+ where: [...relation.modifiers.where, predicate],
916
+ })
917
+ },
918
+
919
+ orderBy(column: TableColumnInput<target>, direction: OrderDirection = 'asc') {
920
+ return cloneRelation(relation, {
921
+ orderBy: [
922
+ ...relation.modifiers.orderBy,
923
+ {
924
+ column: normalizeColumnInput(column),
925
+ direction,
926
+ },
927
+ ],
928
+ })
929
+ },
930
+
931
+ limit(value: number) {
932
+ return cloneRelation(relation, {
933
+ limit: value,
934
+ })
935
+ },
936
+
937
+ offset(value: number) {
938
+ return cloneRelation(relation, {
939
+ offset: value,
940
+ })
941
+ },
942
+
943
+ with<relations extends RelationMapForTable<target>>(relations: relations) {
944
+ return cloneRelation(relation, {
945
+ with: {
946
+ ...relation.modifiers.with,
947
+ ...relations,
948
+ },
949
+ }) as Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>
950
+ },
951
+ }
952
+
953
+ return relation
954
+ }
955
+
956
+ function cloneRelation<
957
+ source extends AnyTable,
958
+ target extends AnyTable,
959
+ cardinality extends RelationCardinality,
960
+ loaded extends Record<string, unknown>,
961
+ >(
962
+ relation: Relation<source, target, cardinality, loaded>,
963
+ patch: Partial<RelationModifiers<target>>,
964
+ ): Relation<source, target, cardinality, loaded> {
965
+ return createRelation({
966
+ relationKind: relation.relationKind,
967
+ cardinality: relation.cardinality,
968
+ sourceTable: relation.sourceTable,
969
+ targetTable: relation.targetTable,
970
+ sourceKey: relation.sourceKey,
971
+ targetKey: relation.targetKey,
972
+ through: relation.through,
973
+ modifiers: {
974
+ where: patch.where ?? relation.modifiers.where,
975
+ orderBy: patch.orderBy ?? relation.modifiers.orderBy,
976
+ limit: patch.limit ?? relation.modifiers.limit,
977
+ offset: patch.offset ?? relation.modifiers.offset,
978
+ with: patch.with ?? relation.modifiers.with,
979
+ },
980
+ })
981
+ }