@remix-run/data-table 0.0.0 → 0.2.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +549 -2
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +7 -0
  6. package/dist/lib/adapter.d.ts +550 -0
  7. package/dist/lib/adapter.d.ts.map +1 -0
  8. package/dist/lib/adapter.js +1 -0
  9. package/dist/lib/column.d.ts +193 -0
  10. package/dist/lib/column.d.ts.map +1 -0
  11. package/dist/lib/column.js +302 -0
  12. package/dist/lib/database/execution-context.d.ts +10 -0
  13. package/dist/lib/database/execution-context.d.ts.map +1 -0
  14. package/dist/lib/database/execution-context.js +1 -0
  15. package/dist/lib/database/helpers.d.ts +26 -0
  16. package/dist/lib/database/helpers.d.ts.map +1 -0
  17. package/dist/lib/database/helpers.js +116 -0
  18. package/dist/lib/database/query-execution.d.ts +7 -0
  19. package/dist/lib/database/query-execution.d.ts.map +1 -0
  20. package/dist/lib/database/query-execution.js +401 -0
  21. package/dist/lib/database/relations.d.ts +4 -0
  22. package/dist/lib/database/relations.d.ts.map +1 -0
  23. package/dist/lib/database/relations.js +207 -0
  24. package/dist/lib/database/write-lifecycle.d.ts +13 -0
  25. package/dist/lib/database/write-lifecycle.d.ts.map +1 -0
  26. package/dist/lib/database/write-lifecycle.js +279 -0
  27. package/dist/lib/database.d.ts +264 -0
  28. package/dist/lib/database.d.ts.map +1 -0
  29. package/dist/lib/database.js +319 -0
  30. package/dist/lib/errors.d.ts +59 -0
  31. package/dist/lib/errors.d.ts.map +1 -0
  32. package/dist/lib/errors.js +76 -0
  33. package/dist/lib/inflection.d.ts +3 -0
  34. package/dist/lib/inflection.d.ts.map +1 -0
  35. package/dist/lib/inflection.js +56 -0
  36. package/dist/lib/migrations/filename.d.ts +12 -0
  37. package/dist/lib/migrations/filename.d.ts.map +1 -0
  38. package/dist/lib/migrations/filename.js +20 -0
  39. package/dist/lib/migrations/helpers.d.ts +11 -0
  40. package/dist/lib/migrations/helpers.d.ts.map +1 -0
  41. package/dist/lib/migrations/helpers.js +77 -0
  42. package/dist/lib/migrations/journal-store.d.ts +15 -0
  43. package/dist/lib/migrations/journal-store.d.ts.map +1 -0
  44. package/dist/lib/migrations/journal-store.js +83 -0
  45. package/dist/lib/migrations/registry.d.ts +27 -0
  46. package/dist/lib/migrations/registry.d.ts.map +1 -0
  47. package/dist/lib/migrations/registry.js +51 -0
  48. package/dist/lib/migrations/runner.d.ts +20 -0
  49. package/dist/lib/migrations/runner.d.ts.map +1 -0
  50. package/dist/lib/migrations/runner.js +273 -0
  51. package/dist/lib/migrations/schema-api.d.ts +7 -0
  52. package/dist/lib/migrations/schema-api.d.ts.map +1 -0
  53. package/dist/lib/migrations/schema-api.js +326 -0
  54. package/dist/lib/migrations-node.d.ts +17 -0
  55. package/dist/lib/migrations-node.d.ts.map +1 -0
  56. package/dist/lib/migrations-node.js +65 -0
  57. package/dist/lib/migrations.d.ts +292 -0
  58. package/dist/lib/migrations.d.ts.map +1 -0
  59. package/dist/lib/migrations.js +38 -0
  60. package/dist/lib/operators.d.ts +154 -0
  61. package/dist/lib/operators.d.ts.map +1 -0
  62. package/dist/lib/operators.js +218 -0
  63. package/dist/lib/query.d.ts +159 -0
  64. package/dist/lib/query.d.ts.map +1 -0
  65. package/dist/lib/query.js +401 -0
  66. package/dist/lib/references.d.ts +41 -0
  67. package/dist/lib/references.d.ts.map +1 -0
  68. package/dist/lib/references.js +33 -0
  69. package/dist/lib/sql-helpers.d.ts +50 -0
  70. package/dist/lib/sql-helpers.d.ts.map +1 -0
  71. package/dist/lib/sql-helpers.js +111 -0
  72. package/dist/lib/sql.d.ts +45 -0
  73. package/dist/lib/sql.d.ts.map +1 -0
  74. package/dist/lib/sql.js +65 -0
  75. package/dist/lib/table.d.ts +569 -0
  76. package/dist/lib/table.d.ts.map +1 -0
  77. package/dist/lib/table.js +519 -0
  78. package/dist/lib/types.d.ts +4 -0
  79. package/dist/lib/types.d.ts.map +1 -0
  80. package/dist/lib/types.js +1 -0
  81. package/dist/migrations/node.d.ts +2 -0
  82. package/dist/migrations/node.d.ts.map +1 -0
  83. package/dist/migrations/node.js +1 -0
  84. package/dist/migrations.d.ts +8 -0
  85. package/dist/migrations.d.ts.map +1 -0
  86. package/dist/migrations.js +5 -0
  87. package/dist/operators.d.ts +3 -0
  88. package/dist/operators.d.ts.map +1 -0
  89. package/dist/operators.js +1 -0
  90. package/dist/sql-helpers.d.ts +3 -0
  91. package/dist/sql-helpers.d.ts.map +1 -0
  92. package/dist/sql-helpers.js +1 -0
  93. package/package.json +56 -7
  94. package/src/index.ts +198 -0
  95. package/src/lib/adapter.ts +653 -0
  96. package/src/lib/column.ts +384 -0
  97. package/src/lib/database/execution-context.ts +15 -0
  98. package/src/lib/database/helpers.ts +216 -0
  99. package/src/lib/database/query-execution.ts +638 -0
  100. package/src/lib/database/relations.ts +332 -0
  101. package/src/lib/database/write-lifecycle.ts +487 -0
  102. package/src/lib/database.ts +856 -0
  103. package/src/lib/errors.ts +119 -0
  104. package/src/lib/inflection.ts +69 -0
  105. package/src/lib/migrations/filename.ts +25 -0
  106. package/src/lib/migrations/helpers.ts +108 -0
  107. package/src/lib/migrations/journal-store.ts +122 -0
  108. package/src/lib/migrations/registry.ts +62 -0
  109. package/src/lib/migrations/runner.ts +374 -0
  110. package/src/lib/migrations/schema-api.ts +417 -0
  111. package/src/lib/migrations-node.ts +71 -0
  112. package/src/lib/migrations.ts +328 -0
  113. package/src/lib/operators.ts +436 -0
  114. package/src/lib/query.ts +958 -0
  115. package/src/lib/references.ts +78 -0
  116. package/src/lib/sql-helpers.ts +146 -0
  117. package/src/lib/sql.ts +84 -0
  118. package/src/lib/table.ts +1309 -0
  119. package/src/lib/types.ts +3 -0
  120. package/src/migrations/node.ts +1 -0
  121. package/src/migrations.ts +26 -0
  122. package/src/operators.ts +18 -0
  123. package/src/sql-helpers.ts +9 -0
@@ -0,0 +1,1309 @@
1
+ import type { ColumnDefinition } from './adapter.ts'
2
+ import { ColumnBuilder } from './column.ts'
3
+ import type { ColumnInput as ColumnBuilderInput, ColumnOutput } from './column.ts'
4
+ import type { Predicate, WhereInput } from './operators.ts'
5
+ import { inferForeignKey } from './inflection.ts'
6
+ import { normalizeWhereInput } from './operators.ts'
7
+ import { columnMetadataKey, normalizeColumnInput, tableMetadataKey } from './references.ts'
8
+ import type { ColumnInput, ColumnReferenceLike, TableMetadataLike } from './references.ts'
9
+ import type { Pretty } from './types.ts'
10
+
11
+ /**
12
+ * Symbol key used to store non-enumerable table metadata.
13
+ */
14
+ export { columnMetadataKey, tableMetadataKey } from './references.ts'
15
+
16
+ /**
17
+ * Column builder map used when declaring a table.
18
+ */
19
+ export type TableColumnsDefinition = Record<string, ColumnBuilder<any>>
20
+
21
+ /**
22
+ * Validation lifecycle operations.
23
+ */
24
+ export type TableValidationOperation = 'create' | 'update'
25
+ /**
26
+ * Write lifecycle operations.
27
+ */
28
+ export type TableWriteOperation = TableValidationOperation
29
+ /**
30
+ * All lifecycle operations exposed by table hooks.
31
+ */
32
+ export type TableLifecycleOperation = TableWriteOperation | 'delete' | 'read'
33
+
34
+ /**
35
+ * Single validation issue reported by table hooks.
36
+ */
37
+ export type ValidationIssue = {
38
+ message: string
39
+ path?: Array<string | number>
40
+ }
41
+
42
+ /**
43
+ * Validation failure returned from table hooks.
44
+ */
45
+ export type ValidationFailure = {
46
+ issues: ReadonlyArray<ValidationIssue>
47
+ }
48
+
49
+ /**
50
+ * Context passed to the `validate` hook.
51
+ */
52
+ export type TableValidationContext<row extends Record<string, unknown>> = {
53
+ operation: TableValidationOperation
54
+ tableName: string
55
+ value: Partial<row>
56
+ }
57
+
58
+ /**
59
+ * Result returned from the `validate` hook.
60
+ */
61
+ export type TableValidationResult<row extends Record<string, unknown>> =
62
+ | { value: Partial<row> }
63
+ | ValidationFailure
64
+
65
+ /**
66
+ * Validation hook that runs before writes.
67
+ */
68
+ export type TableValidate<row extends Record<string, unknown>> = (
69
+ context: TableValidationContext<row>,
70
+ ) => TableValidationResult<row>
71
+
72
+ /**
73
+ * Context passed to the `beforeWrite` hook.
74
+ */
75
+ export type TableBeforeWriteContext<row extends Record<string, unknown>> = {
76
+ operation: TableWriteOperation
77
+ tableName: string
78
+ value: Partial<row>
79
+ }
80
+
81
+ /**
82
+ * Result returned from the `beforeWrite` hook.
83
+ */
84
+ export type TableBeforeWriteResult<row extends Record<string, unknown>> =
85
+ | { value: Partial<row> }
86
+ | ValidationFailure
87
+
88
+ /**
89
+ * Hook invoked before a row write executes.
90
+ */
91
+ export type TableBeforeWrite<row extends Record<string, unknown>> = (
92
+ context: TableBeforeWriteContext<row>,
93
+ ) => TableBeforeWriteResult<row>
94
+
95
+ /**
96
+ * Context passed to the `afterWrite` hook.
97
+ */
98
+ export type TableAfterWriteContext<row extends Record<string, unknown>> = {
99
+ operation: TableWriteOperation
100
+ tableName: string
101
+ values: ReadonlyArray<Partial<row>>
102
+ affectedRows: number
103
+ insertId?: unknown
104
+ }
105
+
106
+ /**
107
+ * Hook invoked after a row write completes.
108
+ */
109
+ export type TableAfterWrite<row extends Record<string, unknown>> = (
110
+ context: TableAfterWriteContext<row>,
111
+ ) => void
112
+
113
+ /**
114
+ * Context passed to the `beforeDelete` hook.
115
+ */
116
+ export type TableBeforeDeleteContext = {
117
+ tableName: string
118
+ where: ReadonlyArray<Predicate<string>>
119
+ orderBy: ReadonlyArray<OrderByClause>
120
+ limit?: number
121
+ offset?: number
122
+ }
123
+
124
+ /**
125
+ * Result returned from the `beforeDelete` hook.
126
+ */
127
+ export type TableBeforeDeleteResult = void | ValidationFailure
128
+
129
+ /**
130
+ * Hook invoked before a delete operation executes.
131
+ */
132
+ export type TableBeforeDelete = (context: TableBeforeDeleteContext) => TableBeforeDeleteResult
133
+
134
+ /**
135
+ * Context passed to the `afterDelete` hook.
136
+ */
137
+ export type TableAfterDeleteContext = {
138
+ tableName: string
139
+ where: ReadonlyArray<Predicate<string>>
140
+ orderBy: ReadonlyArray<OrderByClause>
141
+ limit?: number
142
+ offset?: number
143
+ affectedRows: number
144
+ }
145
+
146
+ /**
147
+ * Hook invoked after a delete operation completes.
148
+ */
149
+ export type TableAfterDelete = (context: TableAfterDeleteContext) => void
150
+
151
+ /**
152
+ * Context passed to the `afterRead` hook.
153
+ */
154
+ export type TableAfterReadContext<row extends Record<string, unknown>> = {
155
+ tableName: string
156
+ /**
157
+ * The current row shape being returned. This may be a projection/partial row.
158
+ */
159
+ value: Partial<row>
160
+ }
161
+
162
+ /**
163
+ * Result returned from the `afterRead` hook.
164
+ */
165
+ export type TableAfterReadResult<row extends Record<string, unknown>> =
166
+ | { value: Partial<row> }
167
+ | ValidationFailure
168
+
169
+ /**
170
+ * Hook invoked after a row is read.
171
+ */
172
+ export type TableAfterRead<row extends Record<string, unknown>> = (
173
+ context: TableAfterReadContext<row>,
174
+ ) => TableAfterReadResult<row>
175
+
176
+ type ColumnNameFromColumns<columns extends TableColumnsDefinition> = keyof columns & string
177
+
178
+ type DefaultPrimaryKey<columns extends TableColumnsDefinition> =
179
+ 'id' extends ColumnNameFromColumns<columns>
180
+ ? readonly ['id']
181
+ : readonly ColumnNameFromColumns<columns>[]
182
+
183
+ type NormalizePrimaryKey<
184
+ columns extends TableColumnsDefinition,
185
+ primaryKey extends
186
+ | ColumnNameFromColumns<columns>
187
+ | readonly ColumnNameFromColumns<columns>[]
188
+ | undefined,
189
+ > = primaryKey extends readonly (infer column extends ColumnNameFromColumns<columns>)[]
190
+ ? readonly [...column[]]
191
+ : primaryKey extends ColumnNameFromColumns<columns>
192
+ ? readonly [primaryKey]
193
+ : DefaultPrimaryKey<columns>
194
+
195
+ /**
196
+ * Timestamp configuration accepted by {@link table}.
197
+ */
198
+ export type TimestampOptions = boolean | { createdAt?: string; updatedAt?: string }
199
+
200
+ /**
201
+ * Resolved timestamp column names for a table.
202
+ */
203
+ export type TimestampConfig = {
204
+ createdAt: string
205
+ updatedAt: string
206
+ }
207
+
208
+ type TableMetadata<
209
+ name extends string,
210
+ columns extends TableColumnsDefinition,
211
+ primaryKey extends readonly ColumnNameFromColumns<columns>[],
212
+ > = {
213
+ name: name
214
+ columns: columns
215
+ primaryKey: primaryKey
216
+ timestamps: TimestampConfig | null
217
+ columnDefinitions: {
218
+ [column in keyof columns & string]: ColumnDefinition
219
+ }
220
+ beforeWrite?: TableBeforeWrite<TableRowFromColumns<columns>>
221
+ afterWrite?: TableAfterWrite<TableRowFromColumns<columns>>
222
+ beforeDelete?: TableBeforeDelete
223
+ afterDelete?: TableAfterDelete
224
+ afterRead?: TableAfterRead<TableRowFromColumns<columns>>
225
+ validate?: TableValidate<TableRowFromColumns<columns>>
226
+ }
227
+
228
+ /**
229
+ * Typed reference to a table column.
230
+ */
231
+ export type ColumnReference<
232
+ tableName extends string,
233
+ columnName extends string,
234
+ > = ColumnReferenceLike<`${tableName}.${columnName}`> & {
235
+ [columnMetadataKey]: {
236
+ tableName: tableName
237
+ columnName: columnName
238
+ qualifiedName: `${tableName}.${columnName}`
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Any column reference.
244
+ */
245
+ export type AnyColumn = ColumnReference<string, string>
246
+
247
+ /**
248
+ * Column reference narrowed by a qualified column name string.
249
+ */
250
+ export type ColumnReferenceForQualifiedName<qualifiedName extends string> = AnyColumn & {
251
+ [columnMetadataKey]: {
252
+ qualifiedName: qualifiedName
253
+ }
254
+ }
255
+
256
+ type TableColumnReferences<name extends string, columns extends TableColumnsDefinition> = {
257
+ [column in keyof columns & string]: ColumnReference<name, column>
258
+ }
259
+
260
+ type TableRowFromColumns<columns extends TableColumnsDefinition> = Pretty<{
261
+ [column in keyof columns & string]: ColumnOutput<columns[column]>
262
+ }>
263
+
264
+ /**
265
+ * Fully-typed table object returned by {@link table}.
266
+ */
267
+ export type Table<
268
+ name extends string,
269
+ columns extends TableColumnsDefinition,
270
+ primaryKey extends readonly ColumnNameFromColumns<columns>[],
271
+ > = TableMetadataLike<name, columns, primaryKey, TimestampConfig | null> & {
272
+ [tableMetadataKey]: TableMetadata<name, columns, primaryKey>
273
+ } & TableColumnReferences<name, columns>
274
+
275
+ /**
276
+ * Table-like object with erased concrete column types.
277
+ */
278
+ export type AnyTable = TableMetadataLike<
279
+ string,
280
+ TableColumnsDefinition,
281
+ readonly string[],
282
+ TimestampConfig | null
283
+ > & {
284
+ [tableMetadataKey]: {
285
+ name: string
286
+ columns: TableColumnsDefinition
287
+ primaryKey: readonly string[]
288
+ timestamps: TimestampConfig | null
289
+ columnDefinitions: Record<string, ColumnDefinition>
290
+ beforeWrite?: unknown
291
+ afterWrite?: unknown
292
+ beforeDelete?: unknown
293
+ afterDelete?: unknown
294
+ afterRead?: unknown
295
+ validate?: TableValidate<Record<string, unknown>>
296
+ }
297
+ } & Record<string, unknown>
298
+
299
+ /**
300
+ * Name of a concrete table.
301
+ */
302
+ export type TableName<table extends AnyTable> = table[typeof tableMetadataKey]['name']
303
+
304
+ /**
305
+ * Column builder map for a concrete table.
306
+ */
307
+ export type TableColumns<table extends AnyTable> = table[typeof tableMetadataKey]['columns']
308
+
309
+ /**
310
+ * Primary-key column list for a concrete table.
311
+ */
312
+ export type TablePrimaryKey<table extends AnyTable> = table[typeof tableMetadataKey]['primaryKey']
313
+
314
+ export type TableTimestamps<table extends AnyTable> = table[typeof tableMetadataKey]['timestamps']
315
+
316
+ /**
317
+ * Row shape produced by a concrete table.
318
+ */
319
+ export type TableRow<table extends AnyTable> = TableRowFromColumns<TableColumns<table>>
320
+
321
+ /**
322
+ * Row shape with loaded relations merged in.
323
+ */
324
+ export type TableRowWith<
325
+ table extends AnyTable,
326
+ loaded extends Record<string, unknown> = {},
327
+ > = Pretty<TableRow<table> & loaded>
328
+
329
+ /**
330
+ * Unqualified column names for a concrete table.
331
+ */
332
+ export type TableColumnName<table extends AnyTable> = keyof TableColumns<table> & string
333
+
334
+ export type QualifiedTableColumnName<table extends AnyTable> =
335
+ `${TableName<table>}.${TableColumnName<table>}`
336
+
337
+ /**
338
+ * Column input accepted for a concrete table.
339
+ */
340
+ export type TableColumnInput<table extends AnyTable> = ColumnInput<
341
+ TableColumnName<table> | QualifiedTableColumnName<table>
342
+ >
343
+
344
+ /**
345
+ * Plain metadata snapshot of a table.
346
+ */
347
+ export type TableReference<table extends AnyTable = AnyTable> = {
348
+ kind: 'table'
349
+ name: TableName<table>
350
+ columns: TableColumns<table>
351
+ primaryKey: TablePrimaryKey<table>
352
+ timestamps: TableTimestamps<table>
353
+ }
354
+
355
+ /**
356
+ * Creates a plain table reference snapshot from a table instance.
357
+ * @param table Source table instance.
358
+ * @returns Table metadata snapshot.
359
+ */
360
+ export function getTableReference<table extends AnyTable>(table: table): TableReference<table> {
361
+ let metadata = table[tableMetadataKey]
362
+
363
+ return {
364
+ kind: 'table',
365
+ name: metadata.name as TableName<table>,
366
+ columns: metadata.columns as TableColumns<table>,
367
+ primaryKey: metadata.primaryKey as TablePrimaryKey<table>,
368
+ timestamps: metadata.timestamps as TableTimestamps<table>,
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Returns a table's SQL name.
374
+ * @param table Source table instance.
375
+ * @returns Table SQL name.
376
+ */
377
+ export function getTableName<table extends AnyTable>(table: table): TableName<table> {
378
+ return table[tableMetadataKey].name as TableName<table>
379
+ }
380
+
381
+ /**
382
+ * Returns a table's column builder map.
383
+ * @param table Source table instance.
384
+ * @returns Table column builder map.
385
+ */
386
+ export function getTableColumns<table extends AnyTable>(table: table): TableColumns<table> {
387
+ return table[tableMetadataKey].columns as TableColumns<table>
388
+ }
389
+
390
+ /**
391
+ * Returns a table's resolved physical column definitions.
392
+ * @param table Source table instance.
393
+ * @returns Column definition map.
394
+ */
395
+ export function getTableColumnDefinitions<table extends AnyTable>(
396
+ table: table,
397
+ ): {
398
+ [column in keyof TableColumns<table> & string]: ColumnDefinition
399
+ } {
400
+ return table[tableMetadataKey].columnDefinitions as {
401
+ [column in keyof TableColumns<table> & string]: ColumnDefinition
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Returns a table's optional write validator.
407
+ * @param table Source table instance.
408
+ * @returns Validation function or `undefined`.
409
+ */
410
+ export function getTableValidator<table extends AnyTable>(
411
+ table: table,
412
+ ): TableValidate<TableRow<table>> | undefined {
413
+ return table[tableMetadataKey].validate as TableValidate<TableRow<table>> | undefined
414
+ }
415
+
416
+ /**
417
+ * Returns a table's optional before-write lifecycle callback.
418
+ * @param table Source table instance.
419
+ * @returns Before-write callback or `undefined`.
420
+ */
421
+ export function getTableBeforeWrite<table extends AnyTable>(
422
+ table: table,
423
+ ): TableBeforeWrite<TableRow<table>> | undefined {
424
+ return table[tableMetadataKey].beforeWrite as TableBeforeWrite<TableRow<table>> | undefined
425
+ }
426
+
427
+ /**
428
+ * Returns a table's optional after-write lifecycle callback.
429
+ * @param table Source table instance.
430
+ * @returns After-write callback or `undefined`.
431
+ */
432
+ export function getTableAfterWrite<table extends AnyTable>(
433
+ table: table,
434
+ ): TableAfterWrite<TableRow<table>> | undefined {
435
+ return table[tableMetadataKey].afterWrite as TableAfterWrite<TableRow<table>> | undefined
436
+ }
437
+
438
+ /**
439
+ * Returns a table's optional before-delete lifecycle callback.
440
+ * @param table Source table instance.
441
+ * @returns Before-delete callback or `undefined`.
442
+ */
443
+ export function getTableBeforeDelete<table extends AnyTable>(
444
+ table: table,
445
+ ): TableBeforeDelete | undefined {
446
+ return table[tableMetadataKey].beforeDelete as TableBeforeDelete | undefined
447
+ }
448
+
449
+ /**
450
+ * Returns a table's optional after-delete lifecycle callback.
451
+ * @param table Source table instance.
452
+ * @returns After-delete callback or `undefined`.
453
+ */
454
+ export function getTableAfterDelete<table extends AnyTable>(
455
+ table: table,
456
+ ): TableAfterDelete | undefined {
457
+ return table[tableMetadataKey].afterDelete as TableAfterDelete | undefined
458
+ }
459
+
460
+ /**
461
+ * Returns a table's optional after-read lifecycle callback.
462
+ * The callback receives the current read shape, which may be a projected partial row.
463
+ * @param table Source table instance.
464
+ * @returns After-read callback or `undefined`.
465
+ */
466
+ export function getTableAfterRead<table extends AnyTable>(
467
+ table: table,
468
+ ): TableAfterRead<TableRow<table>> | undefined {
469
+ return table[tableMetadataKey].afterRead as TableAfterRead<TableRow<table>> | undefined
470
+ }
471
+
472
+ /**
473
+ * Returns a table's primary key columns.
474
+ * @param table Source table instance.
475
+ * @returns Primary key columns.
476
+ */
477
+ export function getTablePrimaryKey<table extends AnyTable>(table: table): TablePrimaryKey<table> {
478
+ return table[tableMetadataKey].primaryKey as TablePrimaryKey<table>
479
+ }
480
+
481
+ /**
482
+ * Returns a table's resolved timestamp configuration.
483
+ * @param table Source table instance.
484
+ * @returns Timestamp configuration or `null`.
485
+ */
486
+ export function getTableTimestamps<table extends AnyTable>(table: table): TableTimestamps<table> {
487
+ return table[tableMetadataKey].timestamps as TableTimestamps<table>
488
+ }
489
+
490
+ /**
491
+ * Sort direction accepted by `orderBy`.
492
+ */
493
+ export type OrderDirection = 'asc' | 'desc'
494
+
495
+ /**
496
+ * Normalized `orderBy` clause.
497
+ */
498
+ export type OrderByClause = {
499
+ column: string
500
+ direction: OrderDirection
501
+ }
502
+
503
+ /**
504
+ * Cardinality of a relation.
505
+ */
506
+ export type RelationCardinality = 'one' | 'many'
507
+
508
+ /**
509
+ * Supported relation kinds.
510
+ */
511
+ export type RelationKind = 'hasMany' | 'hasOne' | 'belongsTo' | 'hasManyThrough'
512
+
513
+ export type RelationResult<relation extends AnyRelation> =
514
+ relation extends Relation<any, infer target, infer cardinality, infer loaded>
515
+ ? cardinality extends 'many'
516
+ ? Array<TableRowWith<target, loaded>>
517
+ : TableRowWith<target, loaded> | null
518
+ : never
519
+
520
+ /**
521
+ * Named relation map for a source table.
522
+ */
523
+ export type RelationMapForTable<table extends AnyTable> = Record<
524
+ string,
525
+ Relation<table, AnyTable, RelationCardinality, any>
526
+ >
527
+
528
+ export type LoadedRelationMap<relations extends RelationMapForTable<any>> = Pretty<{
529
+ [name in keyof relations]: RelationResult<relations[name]>
530
+ }>
531
+
532
+ /**
533
+ * Column or column list used to join relations.
534
+ */
535
+ export type KeySelector<table extends AnyTable> =
536
+ | (keyof TableRow<table> & string)
537
+ | readonly (keyof TableRow<table> & string)[]
538
+
539
+ /**
540
+ * Options for defining a {@link hasMany} relation.
541
+ */
542
+ export type HasManyOptions<source extends AnyTable, target extends AnyTable> = {
543
+ foreignKey?: KeySelector<target>
544
+ targetKey?: KeySelector<source>
545
+ }
546
+
547
+ /**
548
+ * Options for defining a {@link hasOne} relation.
549
+ */
550
+ export type HasOneOptions<source extends AnyTable, target extends AnyTable> = {
551
+ foreignKey?: KeySelector<target>
552
+ targetKey?: KeySelector<source>
553
+ }
554
+
555
+ /**
556
+ * Options for defining a {@link belongsTo} relation.
557
+ */
558
+ export type BelongsToOptions<source extends AnyTable, target extends AnyTable> = {
559
+ foreignKey?: KeySelector<source>
560
+ targetKey?: KeySelector<target>
561
+ }
562
+
563
+ /**
564
+ * Options for defining a {@link hasManyThrough} relation.
565
+ */
566
+ export type HasManyThroughOptions<source extends AnyTable, target extends AnyTable> = {
567
+ through: Relation<source, AnyTable, RelationCardinality, any>
568
+ throughForeignKey?: KeySelector<target>
569
+ throughTargetKey?: string | string[]
570
+ }
571
+
572
+ export type RelationModifiers<target extends AnyTable> = {
573
+ where: Predicate[]
574
+ orderBy: OrderByClause[]
575
+ limit?: number
576
+ offset?: number
577
+ with: RelationMapForTable<target>
578
+ }
579
+
580
+ export type ThroughRelationMetadata = {
581
+ relation: AnyRelation
582
+ throughSourceKey: string[]
583
+ throughTargetKey: string[]
584
+ }
585
+
586
+ /**
587
+ * Relation descriptor used by query loading.
588
+ */
589
+ export type Relation<
590
+ source extends AnyTable,
591
+ target extends AnyTable,
592
+ cardinality extends RelationCardinality,
593
+ loaded extends Record<string, unknown> = {},
594
+ > = {
595
+ kind: 'relation'
596
+ relationKind: RelationKind
597
+ sourceTable: source
598
+ targetTable: target
599
+ cardinality: cardinality
600
+ sourceKey: string[]
601
+ targetKey: string[]
602
+ through?: ThroughRelationMetadata
603
+ modifiers: RelationModifiers<target>
604
+ where(
605
+ input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>,
606
+ ): Relation<source, target, cardinality, loaded>
607
+ orderBy(
608
+ column: TableColumnInput<target>,
609
+ direction?: OrderDirection,
610
+ ): Relation<source, target, cardinality, loaded>
611
+ limit(value: number): Relation<source, target, cardinality, loaded>
612
+ offset(value: number): Relation<source, target, cardinality, loaded>
613
+ with<relations extends RelationMapForTable<target>>(
614
+ relations: relations,
615
+ ): Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>
616
+ }
617
+
618
+ /**
619
+ * Relation descriptor with erased table types.
620
+ */
621
+ export type AnyRelation = Relation<AnyTable, AnyTable, RelationCardinality, any>
622
+
623
+ export type CreateTableOptions<
624
+ name extends string,
625
+ columns extends TableColumnsDefinition,
626
+ primaryKey extends
627
+ | ColumnNameFromColumns<columns>
628
+ | readonly ColumnNameFromColumns<columns>[]
629
+ | undefined,
630
+ > = {
631
+ name: name
632
+ columns: columns
633
+ primaryKey?: primaryKey
634
+ timestamps?: TimestampOptions
635
+ beforeWrite?: TableBeforeWrite<TableRowFromColumns<columns>>
636
+ afterWrite?: TableAfterWrite<TableRowFromColumns<columns>>
637
+ beforeDelete?: TableBeforeDelete
638
+ afterDelete?: TableAfterDelete
639
+ afterRead?: TableAfterRead<TableRowFromColumns<columns>>
640
+ validate?: TableValidate<TableRowFromColumns<columns>>
641
+ }
642
+
643
+ const defaultTimestampConfig: TimestampConfig = {
644
+ createdAt: 'created_at',
645
+ updatedAt: 'updated_at',
646
+ }
647
+
648
+ /**
649
+ * Creates a lifecycle/validation failure result with one or more issues.
650
+ * @param message A single issue message.
651
+ * @param path Optional issue path.
652
+ * @returns A {@link ValidationFailure} result object for `validate` and lifecycle callbacks.
653
+ * @example
654
+ * ```ts
655
+ * import { column as c, fail, table } from 'remix/data-table'
656
+ *
657
+ * let users = table({
658
+ * name: 'users',
659
+ * columns: {
660
+ * id: c.integer(),
661
+ * email: c.varchar(255),
662
+ * },
663
+ * validate({ value }) {
664
+ * if (!value.email) {
665
+ * // Fail with a single issue message and optional path
666
+ * return fail('Email is required', ['email'])
667
+ *
668
+ * // Or fail with multiple issues at once
669
+ * return fail([
670
+ * { message: 'Id is required', path: ['id'] },
671
+ * { message: 'Email is required', path: ['email'] },
672
+ * ])
673
+ * }
674
+ *
675
+ * return { value }
676
+ * },
677
+ * })
678
+ * ```
679
+ */
680
+ export function fail(message: string, path?: Array<string | number>): ValidationFailure
681
+ /**
682
+ * @param issues An array of issues.
683
+ */
684
+ export function fail(issues: ReadonlyArray<ValidationIssue>): ValidationFailure
685
+ export function fail(
686
+ messageOrIssues: string | ReadonlyArray<ValidationIssue>,
687
+ path?: Array<string | number>,
688
+ ): ValidationFailure {
689
+ if (typeof messageOrIssues === 'string') {
690
+ return {
691
+ issues: [{ message: messageOrIssues, path }],
692
+ }
693
+ }
694
+
695
+ return {
696
+ issues: [...messageOrIssues],
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Creates a table object with symbol-backed metadata and direct column references.
702
+ * @param options Table declaration options.
703
+ * @returns A frozen table object.
704
+ * @example
705
+ * ```ts
706
+ * import { column as c, table } from 'remix/data-table'
707
+ *
708
+ * let users = table({
709
+ * name: 'users',
710
+ * columns: {
711
+ * id: c.integer(),
712
+ * email: c.varchar(255),
713
+ * },
714
+ * primaryKey: 'id',
715
+ * })
716
+ * ```
717
+ */
718
+ export function table<
719
+ name extends string,
720
+ columns extends TableColumnsDefinition,
721
+ primaryKey extends
722
+ | ColumnNameFromColumns<columns>
723
+ | readonly ColumnNameFromColumns<columns>[]
724
+ | undefined = undefined,
725
+ >(
726
+ options: CreateTableOptions<name, columns, primaryKey>,
727
+ ): Table<name, columns, NormalizePrimaryKey<columns, primaryKey>> {
728
+ let tableName = options.name
729
+ let columns = options.columns
730
+
731
+ let resolvedPrimaryKey = normalizePrimaryKey(tableName, columns, options.primaryKey)
732
+ let timestampConfig = normalizeTimestampConfig(options.timestamps)
733
+ let columnDefinitions = resolveTableColumns(tableName, columns)
734
+ let table = Object.create(null) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
735
+
736
+ Object.defineProperty(table, tableMetadataKey, {
737
+ value: Object.freeze({
738
+ name: tableName,
739
+ columns,
740
+ primaryKey: resolvedPrimaryKey,
741
+ timestamps: timestampConfig,
742
+ columnDefinitions,
743
+ beforeWrite: options.beforeWrite as
744
+ | TableBeforeWrite<TableRowFromColumns<columns>>
745
+ | undefined,
746
+ afterWrite: options.afterWrite as TableAfterWrite<TableRowFromColumns<columns>> | undefined,
747
+ beforeDelete: options.beforeDelete as TableBeforeDelete | undefined,
748
+ afterDelete: options.afterDelete as TableAfterDelete | undefined,
749
+ afterRead: options.afterRead as TableAfterRead<TableRowFromColumns<columns>> | undefined,
750
+ validate: options.validate as TableValidate<TableRowFromColumns<columns>> | undefined,
751
+ }),
752
+ enumerable: false,
753
+ writable: false,
754
+ configurable: false,
755
+ })
756
+
757
+ for (let columnName in columns) {
758
+ if (!Object.prototype.hasOwnProperty.call(columns, columnName)) {
759
+ continue
760
+ }
761
+
762
+ let column = createColumnReference(tableName, columnName)
763
+
764
+ Object.defineProperty(table, columnName, {
765
+ value: column,
766
+ enumerable: true,
767
+ writable: false,
768
+ configurable: false,
769
+ })
770
+ }
771
+
772
+ return Object.freeze(table) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
773
+ }
774
+
775
+ function createColumnReference<tableName extends string, columnName extends string>(
776
+ tableName: tableName,
777
+ columnName: columnName,
778
+ ): ColumnReference<tableName, columnName> {
779
+ return Object.freeze({
780
+ kind: 'column',
781
+ [columnMetadataKey]: Object.freeze({
782
+ tableName,
783
+ columnName,
784
+ qualifiedName: tableName + '.' + columnName,
785
+ }),
786
+ }) as ColumnReference<tableName, columnName>
787
+ }
788
+
789
+ function resolveTableColumns<columns extends TableColumnsDefinition>(
790
+ tableName: string,
791
+ columns: columns,
792
+ ): { [column in keyof columns & string]: ColumnDefinition } {
793
+ let columnDefinitions: Record<string, ColumnDefinition> = {}
794
+
795
+ for (let columnName in columns) {
796
+ if (!Object.prototype.hasOwnProperty.call(columns, columnName)) {
797
+ continue
798
+ }
799
+
800
+ let column = columns[columnName]
801
+
802
+ if (!(column instanceof ColumnBuilder)) {
803
+ throw new Error(
804
+ 'Invalid column "' +
805
+ columnName +
806
+ '" for table "' +
807
+ tableName +
808
+ '". Expected a column(...) builder',
809
+ )
810
+ }
811
+
812
+ columnDefinitions[columnName] = column.build()
813
+ }
814
+
815
+ return Object.freeze(columnDefinitions) as {
816
+ [column in keyof columns & string]: ColumnDefinition
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Defines a one-to-many relation from `source` to `target`.
822
+ * @param source Source table.
823
+ * @param target Target table.
824
+ * @param relationOptions Relation key configuration.
825
+ * @returns A relation descriptor.
826
+ */
827
+ export function hasMany<source extends AnyTable, target extends AnyTable>(
828
+ source: source,
829
+ target: target,
830
+ relationOptions?: HasManyOptions<source, target>,
831
+ ): Relation<source, target, 'many'> {
832
+ let sourceKey = normalizeKeySelector(
833
+ source,
834
+ relationOptions?.targetKey,
835
+ 'targetKey',
836
+ getTablePrimaryKey(source) as string[],
837
+ )
838
+ let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [
839
+ inferForeignKey(getTableName(source)),
840
+ ])
841
+
842
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
843
+
844
+ return createRelation({
845
+ relationKind: 'hasMany',
846
+ cardinality: 'many',
847
+ sourceTable: source,
848
+ targetTable: target,
849
+ sourceKey,
850
+ targetKey,
851
+ })
852
+ }
853
+
854
+ /**
855
+ * Defines a one-to-one relation from `source` to `target` where the foreign key lives on `target`.
856
+ * @param source Source table.
857
+ * @param target Target table.
858
+ * @param relationOptions Relation key configuration.
859
+ * @returns A relation descriptor.
860
+ */
861
+ export function hasOne<source extends AnyTable, target extends AnyTable>(
862
+ source: source,
863
+ target: target,
864
+ relationOptions?: HasOneOptions<source, target>,
865
+ ): Relation<source, target, 'one'> {
866
+ let sourceKey = normalizeKeySelector(
867
+ source,
868
+ relationOptions?.targetKey,
869
+ 'targetKey',
870
+ getTablePrimaryKey(source) as string[],
871
+ )
872
+ let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [
873
+ inferForeignKey(getTableName(source)),
874
+ ])
875
+
876
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
877
+
878
+ return createRelation({
879
+ relationKind: 'hasOne',
880
+ cardinality: 'one',
881
+ sourceTable: source,
882
+ targetTable: target,
883
+ sourceKey,
884
+ targetKey,
885
+ })
886
+ }
887
+
888
+ /**
889
+ * Defines a one-to-one relation from `source` to `target`.
890
+ * @param source Source table.
891
+ * @param target Target table.
892
+ * @param relationOptions Relation key configuration.
893
+ * @returns A relation descriptor.
894
+ */
895
+ export function belongsTo<source extends AnyTable, target extends AnyTable>(
896
+ source: source,
897
+ target: target,
898
+ relationOptions?: BelongsToOptions<source, target>,
899
+ ): Relation<source, target, 'one'> {
900
+ let sourceKey = normalizeKeySelector(source, relationOptions?.foreignKey, 'foreignKey', [
901
+ inferForeignKey(getTableName(target)),
902
+ ])
903
+ let targetKey = normalizeKeySelector(
904
+ target,
905
+ relationOptions?.targetKey,
906
+ 'targetKey',
907
+ getTablePrimaryKey(target) as string[],
908
+ )
909
+
910
+ assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)
911
+
912
+ return createRelation({
913
+ relationKind: 'belongsTo',
914
+ cardinality: 'one',
915
+ sourceTable: source,
916
+ targetTable: target,
917
+ sourceKey,
918
+ targetKey,
919
+ })
920
+ }
921
+
922
+ /**
923
+ * Defines a one-to-many relation from `source` to `target` through an intermediate relation.
924
+ * @param source Source table.
925
+ * @param target Target table.
926
+ * @param relationOptions Through relation configuration.
927
+ * @returns A relation descriptor.
928
+ */
929
+ export function hasManyThrough<source extends AnyTable, target extends AnyTable>(
930
+ source: source,
931
+ target: target,
932
+ relationOptions: HasManyThroughOptions<source, target>,
933
+ ): Relation<source, target, 'many'> {
934
+ let throughRelation = relationOptions.through
935
+
936
+ if (throughRelation.sourceTable !== source) {
937
+ throw new Error(
938
+ 'hasManyThrough expects a through relation whose source table matches ' +
939
+ getTableName(source),
940
+ )
941
+ }
942
+
943
+ let throughTargetKey = normalizeKeysForTable(
944
+ throughRelation.targetTable,
945
+ relationOptions.throughTargetKey,
946
+ 'throughTargetKey',
947
+ getTablePrimaryKey(throughRelation.targetTable),
948
+ )
949
+ let throughForeignKey = normalizeKeySelector(
950
+ target,
951
+ relationOptions.throughForeignKey,
952
+ 'throughForeignKey',
953
+ [inferForeignKey(getTableName(throughRelation.targetTable))],
954
+ )
955
+
956
+ assertKeyLengths(
957
+ getTableName(throughRelation.targetTable),
958
+ getTableName(target),
959
+ throughTargetKey,
960
+ throughForeignKey,
961
+ )
962
+
963
+ return createRelation({
964
+ relationKind: 'hasManyThrough',
965
+ cardinality: 'many',
966
+ sourceTable: source,
967
+ targetTable: target,
968
+ sourceKey: [...throughRelation.sourceKey],
969
+ targetKey: [...throughRelation.targetKey],
970
+ through: {
971
+ relation: throughRelation as AnyRelation,
972
+ throughSourceKey: throughTargetKey,
973
+ throughTargetKey: throughForeignKey,
974
+ },
975
+ })
976
+ }
977
+
978
+ /**
979
+ * Convenience helper for standard snake_case timestamp columns.
980
+ * @returns Column-builder map for `created_at`/`updated_at`.
981
+ */
982
+ export function timestamps(): Record<
983
+ 'created_at' | 'updated_at',
984
+ ColumnBuilder<Date | string | number>
985
+ > {
986
+ let timestampColumn = () => new ColumnBuilder<Date | string | number>({ type: 'timestamp' })
987
+
988
+ return {
989
+ created_at: timestampColumn(),
990
+ updated_at: timestampColumn(),
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Primary-key input accepted by `find()`, `update()`, and similar helpers.
996
+ */
997
+ export type PrimaryKeyInput<table extends AnyTable> =
998
+ TablePrimaryKey<table> extends readonly [infer column extends string]
999
+ ? column extends keyof TableColumns<table> & string
1000
+ ? ColumnBuilderInput<TableColumns<table>[column]>
1001
+ : never
1002
+ : Pretty<{
1003
+ [column in TablePrimaryKey<table>[number] &
1004
+ keyof TableColumns<table> &
1005
+ string]: ColumnBuilderInput<TableColumns<table>[column]>
1006
+ }>
1007
+
1008
+ /**
1009
+ * Normalizes a primary-key input into an object keyed by primary-key columns.
1010
+ * @param table Source table.
1011
+ * @param value Primary-key input value.
1012
+ * @returns Primary-key object.
1013
+ */
1014
+ export function getPrimaryKeyObject<table extends AnyTable>(
1015
+ table: table,
1016
+ value: PrimaryKeyInput<table>,
1017
+ ): Partial<TableRow<table>> {
1018
+ let keys = getTablePrimaryKey(table)
1019
+
1020
+ if (keys.length === 1 && (typeof value !== 'object' || value === null || Array.isArray(value))) {
1021
+ let key = keys[0] as keyof TableRow<table>
1022
+ return { [key]: value } as Partial<TableRow<table>>
1023
+ }
1024
+
1025
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1026
+ throw new Error('Composite primary keys require an object value')
1027
+ }
1028
+
1029
+ let objectValue = value as Record<string, unknown>
1030
+ let output: Partial<TableRow<table>> = {}
1031
+
1032
+ for (let key of keys) {
1033
+ if (!(key in objectValue)) {
1034
+ throw new Error(
1035
+ 'Missing key "' + key + '" for primary key lookup on "' + getTableName(table) + '"',
1036
+ )
1037
+ }
1038
+
1039
+ ;(output as Record<string, unknown>)[key] = objectValue[key]
1040
+ }
1041
+
1042
+ return output
1043
+ }
1044
+
1045
+ /**
1046
+ * Builds a stable key for a row tuple.
1047
+ * @param row Source row.
1048
+ * @param columns Columns included in the tuple.
1049
+ * @returns Stable tuple key.
1050
+ */
1051
+ export function getCompositeKey(row: Record<string, unknown>, columns: readonly string[]): string {
1052
+ let values = columns.map((column) => stableSerialize(row[column]))
1053
+
1054
+ return values.join('::')
1055
+ }
1056
+
1057
+ /**
1058
+ * Serializes values into stable string representations for key generation.
1059
+ * @param value Value to serialize.
1060
+ * @returns Stable serialized value.
1061
+ */
1062
+ export function stableSerialize(value: unknown): string {
1063
+ if (value === null) {
1064
+ return 'null'
1065
+ }
1066
+
1067
+ if (value === undefined) {
1068
+ return 'undefined'
1069
+ }
1070
+
1071
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
1072
+ return String(value)
1073
+ }
1074
+
1075
+ if (typeof value === 'string') {
1076
+ return JSON.stringify(value)
1077
+ }
1078
+
1079
+ if (value instanceof Date) {
1080
+ return 'date:' + value.toISOString()
1081
+ }
1082
+
1083
+ return JSON.stringify(value)
1084
+ }
1085
+
1086
+ function normalizePrimaryKey(
1087
+ tableName: string,
1088
+ columns: TableColumnsDefinition,
1089
+ primaryKey?: string | readonly string[],
1090
+ ): string[] {
1091
+ if (primaryKey === undefined) {
1092
+ if (!Object.prototype.hasOwnProperty.call(columns, 'id')) {
1093
+ throw new Error(
1094
+ 'Table "' + tableName + '" must include an "id" column or an explicit primaryKey',
1095
+ )
1096
+ }
1097
+
1098
+ return ['id']
1099
+ }
1100
+
1101
+ let keys = Array.isArray(primaryKey) ? [...primaryKey] : [primaryKey]
1102
+
1103
+ if (keys.length === 0) {
1104
+ throw new Error('Table "' + tableName + '" primaryKey must contain at least one column')
1105
+ }
1106
+
1107
+ for (let key of keys) {
1108
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
1109
+ throw new Error('Table "' + tableName + '" primaryKey column "' + key + '" does not exist')
1110
+ }
1111
+ }
1112
+
1113
+ return keys
1114
+ }
1115
+
1116
+ function normalizeKeySelector<table extends AnyTable>(
1117
+ table: table,
1118
+ selector: KeySelector<table> | undefined,
1119
+ optionName: string,
1120
+ defaultValue: readonly string[],
1121
+ ): string[] {
1122
+ return normalizeKeysForTable(table, selector, optionName, defaultValue)
1123
+ }
1124
+
1125
+ function normalizeKeysForTable(
1126
+ table: AnyTable,
1127
+ selector: string | readonly string[] | undefined,
1128
+ optionName: string,
1129
+ defaultValue: readonly string[],
1130
+ ): string[] {
1131
+ if (selector === undefined) {
1132
+ return [...defaultValue]
1133
+ }
1134
+
1135
+ let keys = Array.isArray(selector) ? [...selector] : [selector]
1136
+
1137
+ if (keys.length === 0) {
1138
+ throw new Error(
1139
+ 'Option "' + optionName + '" for table "' + getTableName(table) + '" must not be empty',
1140
+ )
1141
+ }
1142
+
1143
+ let columns = getTableColumns(table)
1144
+
1145
+ for (let key of keys) {
1146
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
1147
+ throw new Error(
1148
+ 'Unknown column "' +
1149
+ key +
1150
+ '" in option "' +
1151
+ optionName +
1152
+ '" for table "' +
1153
+ getTableName(table) +
1154
+ '"',
1155
+ )
1156
+ }
1157
+ }
1158
+
1159
+ return keys
1160
+ }
1161
+
1162
+ function normalizeTimestampConfig(options: TimestampOptions | undefined): TimestampConfig | null {
1163
+ if (!options) {
1164
+ return null
1165
+ }
1166
+
1167
+ if (options === true) {
1168
+ return { ...defaultTimestampConfig }
1169
+ }
1170
+
1171
+ return {
1172
+ createdAt: options.createdAt ?? defaultTimestampConfig.createdAt,
1173
+ updatedAt: options.updatedAt ?? defaultTimestampConfig.updatedAt,
1174
+ }
1175
+ }
1176
+
1177
+ function assertKeyLengths(
1178
+ sourceTableName: string,
1179
+ targetTableName: string,
1180
+ sourceKey: string[],
1181
+ targetKey: string[],
1182
+ ): void {
1183
+ if (sourceKey.length !== targetKey.length) {
1184
+ throw new Error(
1185
+ 'Relation key mismatch between "' +
1186
+ sourceTableName +
1187
+ '" (' +
1188
+ sourceKey.join(', ') +
1189
+ ') and "' +
1190
+ targetTableName +
1191
+ '" (' +
1192
+ targetKey.join(', ') +
1193
+ ')',
1194
+ )
1195
+ }
1196
+ }
1197
+
1198
+ type CreateRelationOptions<
1199
+ source extends AnyTable,
1200
+ target extends AnyTable,
1201
+ cardinality extends RelationCardinality,
1202
+ > = {
1203
+ relationKind: RelationKind
1204
+ cardinality: cardinality
1205
+ sourceTable: source
1206
+ targetTable: target
1207
+ sourceKey: string[]
1208
+ targetKey: string[]
1209
+ through?: ThroughRelationMetadata
1210
+ modifiers?: Partial<RelationModifiers<target>>
1211
+ }
1212
+
1213
+ function createRelation<
1214
+ source extends AnyTable,
1215
+ target extends AnyTable,
1216
+ cardinality extends RelationCardinality,
1217
+ loaded extends Record<string, unknown> = {},
1218
+ >(
1219
+ options: CreateRelationOptions<source, target, cardinality>,
1220
+ ): Relation<source, target, cardinality, loaded> {
1221
+ let baseModifiers: RelationModifiers<target> = {
1222
+ where: options.modifiers?.where ? [...options.modifiers.where] : [],
1223
+ orderBy: options.modifiers?.orderBy ? [...options.modifiers.orderBy] : [],
1224
+ limit: options.modifiers?.limit,
1225
+ offset: options.modifiers?.offset,
1226
+ with: options.modifiers?.with ? { ...options.modifiers.with } : {},
1227
+ }
1228
+
1229
+ let relation: Relation<source, target, cardinality, loaded> = {
1230
+ kind: 'relation',
1231
+ relationKind: options.relationKind,
1232
+ sourceTable: options.sourceTable,
1233
+ targetTable: options.targetTable,
1234
+ cardinality: options.cardinality,
1235
+ sourceKey: [...options.sourceKey],
1236
+ targetKey: [...options.targetKey],
1237
+ through: options.through,
1238
+ modifiers: baseModifiers,
1239
+
1240
+ where(input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>) {
1241
+ let predicate = normalizeWhereInput(input)
1242
+ return cloneRelation(relation, {
1243
+ where: [...relation.modifiers.where, predicate],
1244
+ })
1245
+ },
1246
+
1247
+ orderBy(column: TableColumnInput<target>, direction: OrderDirection = 'asc') {
1248
+ return cloneRelation(relation, {
1249
+ orderBy: [
1250
+ ...relation.modifiers.orderBy,
1251
+ {
1252
+ column: normalizeColumnInput(column),
1253
+ direction,
1254
+ },
1255
+ ],
1256
+ })
1257
+ },
1258
+
1259
+ limit(value: number) {
1260
+ return cloneRelation(relation, {
1261
+ limit: value,
1262
+ })
1263
+ },
1264
+
1265
+ offset(value: number) {
1266
+ return cloneRelation(relation, {
1267
+ offset: value,
1268
+ })
1269
+ },
1270
+
1271
+ with<relations extends RelationMapForTable<target>>(relations: relations) {
1272
+ return cloneRelation(relation, {
1273
+ with: {
1274
+ ...relation.modifiers.with,
1275
+ ...relations,
1276
+ },
1277
+ }) as Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>
1278
+ },
1279
+ }
1280
+
1281
+ return relation
1282
+ }
1283
+
1284
+ function cloneRelation<
1285
+ source extends AnyTable,
1286
+ target extends AnyTable,
1287
+ cardinality extends RelationCardinality,
1288
+ loaded extends Record<string, unknown>,
1289
+ >(
1290
+ relation: Relation<source, target, cardinality, loaded>,
1291
+ patch: Partial<RelationModifiers<target>>,
1292
+ ): Relation<source, target, cardinality, loaded> {
1293
+ return createRelation({
1294
+ relationKind: relation.relationKind,
1295
+ cardinality: relation.cardinality,
1296
+ sourceTable: relation.sourceTable,
1297
+ targetTable: relation.targetTable,
1298
+ sourceKey: relation.sourceKey,
1299
+ targetKey: relation.targetKey,
1300
+ through: relation.through,
1301
+ modifiers: {
1302
+ where: patch.where ?? relation.modifiers.where,
1303
+ orderBy: patch.orderBy ?? relation.modifiers.orderBy,
1304
+ limit: patch.limit ?? relation.modifiers.limit,
1305
+ offset: patch.offset ?? relation.modifiers.offset,
1306
+ with: patch.with ?? relation.modifiers.with,
1307
+ },
1308
+ })
1309
+ }