@remix-run/data-table 0.1.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 (111) hide show
  1. package/README.md +306 -55
  2. package/dist/index.d.ts +9 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -2
  5. package/dist/lib/adapter.d.ts +386 -16
  6. package/dist/lib/adapter.d.ts.map +1 -1
  7. package/dist/lib/column.d.ts +193 -0
  8. package/dist/lib/column.d.ts.map +1 -0
  9. package/dist/lib/column.js +302 -0
  10. package/dist/lib/database/execution-context.d.ts +10 -0
  11. package/dist/lib/database/execution-context.d.ts.map +1 -0
  12. package/dist/lib/database/execution-context.js +1 -0
  13. package/dist/lib/database/helpers.d.ts +26 -0
  14. package/dist/lib/database/helpers.d.ts.map +1 -0
  15. package/dist/lib/database/helpers.js +116 -0
  16. package/dist/lib/database/query-execution.d.ts +7 -0
  17. package/dist/lib/database/query-execution.d.ts.map +1 -0
  18. package/dist/lib/database/query-execution.js +401 -0
  19. package/dist/lib/database/relations.d.ts +4 -0
  20. package/dist/lib/database/relations.d.ts.map +1 -0
  21. package/dist/lib/database/relations.js +207 -0
  22. package/dist/lib/database/write-lifecycle.d.ts +13 -0
  23. package/dist/lib/database/write-lifecycle.d.ts.map +1 -0
  24. package/dist/lib/database/write-lifecycle.js +279 -0
  25. package/dist/lib/database.d.ts +141 -238
  26. package/dist/lib/database.d.ts.map +1 -1
  27. package/dist/lib/database.js +73 -1122
  28. package/dist/lib/errors.d.ts +9 -0
  29. package/dist/lib/errors.d.ts.map +1 -1
  30. package/dist/lib/errors.js +9 -0
  31. package/dist/lib/migrations/filename.d.ts +12 -0
  32. package/dist/lib/migrations/filename.d.ts.map +1 -0
  33. package/dist/lib/migrations/filename.js +20 -0
  34. package/dist/lib/migrations/helpers.d.ts +11 -0
  35. package/dist/lib/migrations/helpers.d.ts.map +1 -0
  36. package/dist/lib/migrations/helpers.js +77 -0
  37. package/dist/lib/migrations/journal-store.d.ts +15 -0
  38. package/dist/lib/migrations/journal-store.d.ts.map +1 -0
  39. package/dist/lib/migrations/journal-store.js +83 -0
  40. package/dist/lib/migrations/registry.d.ts +27 -0
  41. package/dist/lib/migrations/registry.d.ts.map +1 -0
  42. package/dist/lib/migrations/registry.js +51 -0
  43. package/dist/lib/migrations/runner.d.ts +20 -0
  44. package/dist/lib/migrations/runner.d.ts.map +1 -0
  45. package/dist/lib/migrations/runner.js +273 -0
  46. package/dist/lib/migrations/schema-api.d.ts +7 -0
  47. package/dist/lib/migrations/schema-api.d.ts.map +1 -0
  48. package/dist/lib/migrations/schema-api.js +326 -0
  49. package/dist/lib/migrations-node.d.ts +17 -0
  50. package/dist/lib/migrations-node.d.ts.map +1 -0
  51. package/dist/lib/migrations-node.js +65 -0
  52. package/dist/lib/migrations.d.ts +292 -0
  53. package/dist/lib/migrations.d.ts.map +1 -0
  54. package/dist/lib/migrations.js +38 -0
  55. package/dist/lib/operators.d.ts +3 -0
  56. package/dist/lib/operators.d.ts.map +1 -1
  57. package/dist/lib/query.d.ts +159 -0
  58. package/dist/lib/query.d.ts.map +1 -0
  59. package/dist/lib/query.js +401 -0
  60. package/dist/lib/references.d.ts +0 -1
  61. package/dist/lib/references.d.ts.map +1 -1
  62. package/dist/lib/sql-helpers.d.ts +50 -0
  63. package/dist/lib/sql-helpers.d.ts.map +1 -0
  64. package/dist/lib/sql-helpers.js +111 -0
  65. package/dist/lib/sql.d.ts +23 -6
  66. package/dist/lib/sql.d.ts.map +1 -1
  67. package/dist/lib/sql.js +19 -5
  68. package/dist/lib/table.d.ts +355 -40
  69. package/dist/lib/table.d.ts.map +1 -1
  70. package/dist/lib/table.js +113 -90
  71. package/dist/migrations/node.d.ts +2 -0
  72. package/dist/migrations/node.d.ts.map +1 -0
  73. package/dist/migrations/node.js +1 -0
  74. package/dist/migrations.d.ts +8 -0
  75. package/dist/migrations.d.ts.map +1 -0
  76. package/dist/migrations.js +5 -0
  77. package/dist/operators.d.ts +3 -0
  78. package/dist/operators.d.ts.map +1 -0
  79. package/dist/operators.js +1 -0
  80. package/dist/sql-helpers.d.ts +3 -0
  81. package/dist/sql-helpers.d.ts.map +1 -0
  82. package/dist/sql-helpers.js +1 -0
  83. package/package.json +23 -8
  84. package/src/index.ts +93 -10
  85. package/src/lib/adapter.ts +469 -25
  86. package/src/lib/column.ts +384 -0
  87. package/src/lib/database/execution-context.ts +15 -0
  88. package/src/lib/database/helpers.ts +216 -0
  89. package/src/lib/database/query-execution.ts +638 -0
  90. package/src/lib/database/relations.ts +332 -0
  91. package/src/lib/database/write-lifecycle.ts +487 -0
  92. package/src/lib/database.ts +246 -1848
  93. package/src/lib/errors.ts +10 -0
  94. package/src/lib/migrations/filename.ts +25 -0
  95. package/src/lib/migrations/helpers.ts +108 -0
  96. package/src/lib/migrations/journal-store.ts +122 -0
  97. package/src/lib/migrations/registry.ts +62 -0
  98. package/src/lib/migrations/runner.ts +374 -0
  99. package/src/lib/migrations/schema-api.ts +417 -0
  100. package/src/lib/migrations-node.ts +71 -0
  101. package/src/lib/migrations.ts +328 -0
  102. package/src/lib/operators.ts +3 -0
  103. package/src/lib/query.ts +958 -0
  104. package/src/lib/references.ts +0 -1
  105. package/src/lib/sql-helpers.ts +146 -0
  106. package/src/lib/sql.ts +23 -6
  107. package/src/lib/table.ts +484 -156
  108. package/src/migrations/node.ts +1 -0
  109. package/src/migrations.ts +26 -0
  110. package/src/operators.ts +18 -0
  111. package/src/sql-helpers.ts +9 -0
package/src/lib/table.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { createSchema, parseSafe } from '@remix-run/data-schema'
2
- import type { InferInput, InferOutput, Issue, ParseOptions, Schema } from '@remix-run/data-schema'
1
+ import type { ColumnDefinition } from './adapter.ts'
2
+ import { ColumnBuilder } from './column.ts'
3
+ import type { ColumnInput as ColumnBuilderInput, ColumnOutput } from './column.ts'
3
4
  import type { Predicate, WhereInput } from './operators.ts'
4
5
  import { inferForeignKey } from './inflection.ts'
5
6
  import { normalizeWhereInput } from './operators.ts'
@@ -13,19 +14,174 @@ import type { Pretty } from './types.ts'
13
14
  export { columnMetadataKey, tableMetadataKey } from './references.ts'
14
15
 
15
16
  /**
16
- * Mapping of column names to schemas.
17
+ * Column builder map used when declaring a table.
17
18
  */
18
- export type ColumnSchemas = Record<string, Schema<any, any>>
19
+ export type TableColumnsDefinition = Record<string, ColumnBuilder<any>>
19
20
 
20
- type ColumnNameFromColumns<columns extends ColumnSchemas> = keyof columns & string
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
21
150
 
22
- type DefaultPrimaryKey<columns extends ColumnSchemas> =
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> =
23
179
  'id' extends ColumnNameFromColumns<columns>
24
180
  ? readonly ['id']
25
181
  : readonly ColumnNameFromColumns<columns>[]
26
182
 
27
183
  type NormalizePrimaryKey<
28
- columns extends ColumnSchemas,
184
+ columns extends TableColumnsDefinition,
29
185
  primaryKey extends
30
186
  | ColumnNameFromColumns<columns>
31
187
  | readonly ColumnNameFromColumns<columns>[]
@@ -36,8 +192,14 @@ type NormalizePrimaryKey<
36
192
  ? readonly [primaryKey]
37
193
  : DefaultPrimaryKey<columns>
38
194
 
195
+ /**
196
+ * Timestamp configuration accepted by {@link table}.
197
+ */
39
198
  export type TimestampOptions = boolean | { createdAt?: string; updatedAt?: string }
40
199
 
200
+ /**
201
+ * Resolved timestamp column names for a table.
202
+ */
41
203
  export type TimestampConfig = {
42
204
  createdAt: string
43
205
  updatedAt: string
@@ -45,94 +207,143 @@ export type TimestampConfig = {
45
207
 
46
208
  type TableMetadata<
47
209
  name extends string,
48
- columns extends ColumnSchemas,
210
+ columns extends TableColumnsDefinition,
49
211
  primaryKey extends readonly ColumnNameFromColumns<columns>[],
50
212
  > = {
51
213
  name: name
52
214
  columns: columns
53
215
  primaryKey: primaryKey
54
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>>
55
226
  }
56
227
 
228
+ /**
229
+ * Typed reference to a table column.
230
+ */
57
231
  export type ColumnReference<
58
232
  tableName extends string,
59
233
  columnName extends string,
60
- schema extends Schema<any, any>,
61
234
  > = ColumnReferenceLike<`${tableName}.${columnName}`> & {
62
235
  [columnMetadataKey]: {
63
236
  tableName: tableName
64
237
  columnName: columnName
65
238
  qualifiedName: `${tableName}.${columnName}`
66
- schema: schema
67
239
  }
68
240
  }
69
241
 
70
- export type AnyColumn = ColumnReference<string, string, Schema<any, any>>
242
+ /**
243
+ * Any column reference.
244
+ */
245
+ export type AnyColumn = ColumnReference<string, string>
71
246
 
247
+ /**
248
+ * Column reference narrowed by a qualified column name string.
249
+ */
72
250
  export type ColumnReferenceForQualifiedName<qualifiedName extends string> = AnyColumn & {
73
251
  [columnMetadataKey]: {
74
252
  qualifiedName: qualifiedName
75
253
  }
76
254
  }
77
255
 
78
- type TableColumnReferences<name extends string, columns extends ColumnSchemas> = {
79
- [column in keyof columns & string]: ColumnReference<name, column, columns[column]>
256
+ type TableColumnReferences<name extends string, columns extends TableColumnsDefinition> = {
257
+ [column in keyof columns & string]: ColumnReference<name, column>
80
258
  }
81
259
 
82
- type TableParseOutput<columns extends ColumnSchemas> = Partial<{
83
- [column in keyof columns & string]: InferOutput<columns[column]>
260
+ type TableRowFromColumns<columns extends TableColumnsDefinition> = Pretty<{
261
+ [column in keyof columns & string]: ColumnOutput<columns[column]>
84
262
  }>
85
263
 
264
+ /**
265
+ * Fully-typed table object returned by {@link table}.
266
+ */
86
267
  export type Table<
87
268
  name extends string,
88
- columns extends ColumnSchemas,
269
+ columns extends TableColumnsDefinition,
89
270
  primaryKey extends readonly ColumnNameFromColumns<columns>[],
90
271
  > = TableMetadataLike<name, columns, primaryKey, TimestampConfig | null> & {
91
272
  [tableMetadataKey]: TableMetadata<name, columns, primaryKey>
92
- '~standard': Schema<unknown, TableParseOutput<columns>>['~standard']
93
273
  } & TableColumnReferences<name, columns>
94
274
 
275
+ /**
276
+ * Table-like object with erased concrete column types.
277
+ */
95
278
  export type AnyTable = TableMetadataLike<
96
279
  string,
97
- ColumnSchemas,
280
+ TableColumnsDefinition,
98
281
  readonly string[],
99
282
  TimestampConfig | null
100
283
  > & {
101
284
  [tableMetadataKey]: {
102
285
  name: string
103
- columns: ColumnSchemas
286
+ columns: TableColumnsDefinition
104
287
  primaryKey: readonly string[]
105
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>>
106
296
  }
107
- '~standard': Schema<unknown, Partial<Record<string, unknown>>>['~standard']
108
297
  } & Record<string, unknown>
109
298
 
299
+ /**
300
+ * Name of a concrete table.
301
+ */
110
302
  export type TableName<table extends AnyTable> = table[typeof tableMetadataKey]['name']
111
303
 
304
+ /**
305
+ * Column builder map for a concrete table.
306
+ */
112
307
  export type TableColumns<table extends AnyTable> = table[typeof tableMetadataKey]['columns']
113
308
 
309
+ /**
310
+ * Primary-key column list for a concrete table.
311
+ */
114
312
  export type TablePrimaryKey<table extends AnyTable> = table[typeof tableMetadataKey]['primaryKey']
115
313
 
116
314
  export type TableTimestamps<table extends AnyTable> = table[typeof tableMetadataKey]['timestamps']
117
315
 
118
- export type TableRow<table extends AnyTable> = Pretty<{
119
- [column in keyof TableColumns<table> & string]: InferOutput<TableColumns<table>[column]>
120
- }>
316
+ /**
317
+ * Row shape produced by a concrete table.
318
+ */
319
+ export type TableRow<table extends AnyTable> = TableRowFromColumns<TableColumns<table>>
121
320
 
321
+ /**
322
+ * Row shape with loaded relations merged in.
323
+ */
122
324
  export type TableRowWith<
123
325
  table extends AnyTable,
124
326
  loaded extends Record<string, unknown> = {},
125
327
  > = Pretty<TableRow<table> & loaded>
126
328
 
329
+ /**
330
+ * Unqualified column names for a concrete table.
331
+ */
127
332
  export type TableColumnName<table extends AnyTable> = keyof TableColumns<table> & string
128
333
 
129
334
  export type QualifiedTableColumnName<table extends AnyTable> =
130
335
  `${TableName<table>}.${TableColumnName<table>}`
131
336
 
337
+ /**
338
+ * Column input accepted for a concrete table.
339
+ */
132
340
  export type TableColumnInput<table extends AnyTable> = ColumnInput<
133
341
  TableColumnName<table> | QualifiedTableColumnName<table>
134
342
  >
135
343
 
344
+ /**
345
+ * Plain metadata snapshot of a table.
346
+ */
136
347
  export type TableReference<table extends AnyTable = AnyTable> = {
137
348
  kind: 'table'
138
349
  name: TableName<table>
@@ -168,14 +379,96 @@ export function getTableName<table extends AnyTable>(table: table): TableName<ta
168
379
  }
169
380
 
170
381
  /**
171
- * Returns a table's schema map.
382
+ * Returns a table's column builder map.
172
383
  * @param table Source table instance.
173
- * @returns Table schema map.
384
+ * @returns Table column builder map.
174
385
  */
175
386
  export function getTableColumns<table extends AnyTable>(table: table): TableColumns<table> {
176
387
  return table[tableMetadataKey].columns as TableColumns<table>
177
388
  }
178
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
+
179
472
  /**
180
473
  * Returns a table's primary key columns.
181
474
  * @param table Source table instance.
@@ -194,15 +487,27 @@ export function getTableTimestamps<table extends AnyTable>(table: table): TableT
194
487
  return table[tableMetadataKey].timestamps as TableTimestamps<table>
195
488
  }
196
489
 
490
+ /**
491
+ * Sort direction accepted by `orderBy`.
492
+ */
197
493
  export type OrderDirection = 'asc' | 'desc'
198
494
 
495
+ /**
496
+ * Normalized `orderBy` clause.
497
+ */
199
498
  export type OrderByClause = {
200
499
  column: string
201
500
  direction: OrderDirection
202
501
  }
203
502
 
503
+ /**
504
+ * Cardinality of a relation.
505
+ */
204
506
  export type RelationCardinality = 'one' | 'many'
205
507
 
508
+ /**
509
+ * Supported relation kinds.
510
+ */
206
511
  export type RelationKind = 'hasMany' | 'hasOne' | 'belongsTo' | 'hasManyThrough'
207
512
 
208
513
  export type RelationResult<relation extends AnyRelation> =
@@ -212,6 +517,9 @@ export type RelationResult<relation extends AnyRelation> =
212
517
  : TableRowWith<target, loaded> | null
213
518
  : never
214
519
 
520
+ /**
521
+ * Named relation map for a source table.
522
+ */
215
523
  export type RelationMapForTable<table extends AnyTable> = Record<
216
524
  string,
217
525
  Relation<table, AnyTable, RelationCardinality, any>
@@ -221,25 +529,40 @@ export type LoadedRelationMap<relations extends RelationMapForTable<any>> = Pret
221
529
  [name in keyof relations]: RelationResult<relations[name]>
222
530
  }>
223
531
 
532
+ /**
533
+ * Column or column list used to join relations.
534
+ */
224
535
  export type KeySelector<table extends AnyTable> =
225
536
  | (keyof TableRow<table> & string)
226
537
  | readonly (keyof TableRow<table> & string)[]
227
538
 
539
+ /**
540
+ * Options for defining a {@link hasMany} relation.
541
+ */
228
542
  export type HasManyOptions<source extends AnyTable, target extends AnyTable> = {
229
543
  foreignKey?: KeySelector<target>
230
544
  targetKey?: KeySelector<source>
231
545
  }
232
546
 
547
+ /**
548
+ * Options for defining a {@link hasOne} relation.
549
+ */
233
550
  export type HasOneOptions<source extends AnyTable, target extends AnyTable> = {
234
551
  foreignKey?: KeySelector<target>
235
552
  targetKey?: KeySelector<source>
236
553
  }
237
554
 
555
+ /**
556
+ * Options for defining a {@link belongsTo} relation.
557
+ */
238
558
  export type BelongsToOptions<source extends AnyTable, target extends AnyTable> = {
239
559
  foreignKey?: KeySelector<source>
240
560
  targetKey?: KeySelector<target>
241
561
  }
242
562
 
563
+ /**
564
+ * Options for defining a {@link hasManyThrough} relation.
565
+ */
243
566
  export type HasManyThroughOptions<source extends AnyTable, target extends AnyTable> = {
244
567
  through: Relation<source, AnyTable, RelationCardinality, any>
245
568
  throughForeignKey?: KeySelector<target>
@@ -260,6 +583,9 @@ export type ThroughRelationMetadata = {
260
583
  throughTargetKey: string[]
261
584
  }
262
585
 
586
+ /**
587
+ * Relation descriptor used by query loading.
588
+ */
263
589
  export type Relation<
264
590
  source extends AnyTable,
265
591
  target extends AnyTable,
@@ -289,11 +615,14 @@ export type Relation<
289
615
  ): Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>
290
616
  }
291
617
 
618
+ /**
619
+ * Relation descriptor with erased table types.
620
+ */
292
621
  export type AnyRelation = Relation<AnyTable, AnyTable, RelationCardinality, any>
293
622
 
294
623
  export type CreateTableOptions<
295
624
  name extends string,
296
- columns extends ColumnSchemas,
625
+ columns extends TableColumnsDefinition,
297
626
  primaryKey extends
298
627
  | ColumnNameFromColumns<columns>
299
628
  | readonly ColumnNameFromColumns<columns>[]
@@ -303,80 +632,68 @@ export type CreateTableOptions<
303
632
  columns: columns
304
633
  primaryKey?: primaryKey
305
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>>
306
641
  }
307
642
 
308
- let defaultTimestampConfig: TimestampConfig = {
643
+ const defaultTimestampConfig: TimestampConfig = {
309
644
  createdAt: 'created_at',
310
645
  updatedAt: 'updated_at',
311
646
  }
312
647
 
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)) {
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') {
328
690
  return {
329
- issues: [{ message: 'Expected object' }],
691
+ issues: [{ message: messageOrIssues, path }],
330
692
  }
331
693
  }
332
694
 
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
695
  return {
379
- value: result.value as Partial<TableRow<table>>,
696
+ issues: [...messageOrIssues],
380
697
  }
381
698
  }
382
699
 
@@ -384,10 +701,23 @@ export function validatePartialRow<table extends AnyTable>(
384
701
  * Creates a table object with symbol-backed metadata and direct column references.
385
702
  * @param options Table declaration options.
386
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
+ * ```
387
717
  */
388
- export function createTable<
718
+ export function table<
389
719
  name extends string,
390
- columns extends ColumnSchemas,
720
+ columns extends TableColumnsDefinition,
391
721
  primaryKey extends
392
722
  | ColumnNameFromColumns<columns>
393
723
  | readonly ColumnNameFromColumns<columns>[]
@@ -398,14 +728,9 @@ export function createTable<
398
728
  let tableName = options.name
399
729
  let columns = options.columns
400
730
 
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
731
  let resolvedPrimaryKey = normalizePrimaryKey(tableName, columns, options.primaryKey)
408
732
  let timestampConfig = normalizeTimestampConfig(options.timestamps)
733
+ let columnDefinitions = resolveTableColumns(tableName, columns)
409
734
  let table = Object.create(null) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
410
735
 
411
736
  Object.defineProperty(table, tableMetadataKey, {
@@ -414,19 +739,15 @@ export function createTable<
414
739
  columns,
415
740
  primaryKey: resolvedPrimaryKey,
416
741
  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
- },
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,
430
751
  }),
431
752
  enumerable: false,
432
753
  writable: false,
@@ -438,8 +759,7 @@ export function createTable<
438
759
  continue
439
760
  }
440
761
 
441
- let schema = columns[columnName]
442
- let column = createColumnReference(tableName, columnName, schema)
762
+ let column = createColumnReference(tableName, columnName)
443
763
 
444
764
  Object.defineProperty(table, columnName, {
445
765
  value: column,
@@ -452,24 +772,49 @@ export function createTable<
452
772
  return Object.freeze(table) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>
453
773
  }
454
774
 
455
- function createColumnReference<
456
- tableName extends string,
457
- columnName extends string,
458
- schema extends Schema<any, any>,
459
- >(
775
+ function createColumnReference<tableName extends string, columnName extends string>(
460
776
  tableName: tableName,
461
777
  columnName: columnName,
462
- schema: schema,
463
- ): ColumnReference<tableName, columnName, schema> {
778
+ ): ColumnReference<tableName, columnName> {
464
779
  return Object.freeze({
465
780
  kind: 'column',
466
781
  [columnMetadataKey]: Object.freeze({
467
782
  tableName,
468
783
  columnName,
469
784
  qualifiedName: tableName + '.' + columnName,
470
- schema,
471
785
  }),
472
- }) as ColumnReference<tableName, columnName, schema>
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
+ }
473
818
  }
474
819
 
475
820
  /**
@@ -630,51 +975,34 @@ export function hasManyThrough<source extends AnyTable, target extends AnyTable>
630
975
  })
631
976
  }
632
977
 
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
978
  /**
656
979
  * 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`.
980
+ * @returns Column-builder map for `created_at`/`updated_at`.
659
981
  */
660
- export function timestamps(
661
- schema: Schema<any, any> = defaultTimestampSchema,
662
- ): Record<'created_at' | 'updated_at', Schema<any, any>> {
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
+
663
988
  return {
664
- created_at: schema,
665
- updated_at: schema,
989
+ created_at: timestampColumn(),
990
+ updated_at: timestampColumn(),
666
991
  }
667
992
  }
668
993
 
994
+ /**
995
+ * Primary-key input accepted by `find()`, `update()`, and similar helpers.
996
+ */
669
997
  export type PrimaryKeyInput<table extends AnyTable> =
670
998
  TablePrimaryKey<table> extends readonly [infer column extends string]
671
999
  ? column extends keyof TableColumns<table> & string
672
- ? InferInput<TableColumns<table>[column]>
1000
+ ? ColumnBuilderInput<TableColumns<table>[column]>
673
1001
  : never
674
1002
  : Pretty<{
675
- [column in TablePrimaryKey<table>[number] & keyof TableColumns<table> & string]: InferInput<
676
- TableColumns<table>[column]
677
- >
1003
+ [column in TablePrimaryKey<table>[number] &
1004
+ keyof TableColumns<table> &
1005
+ string]: ColumnBuilderInput<TableColumns<table>[column]>
678
1006
  }>
679
1007
 
680
1008
  /**
@@ -757,13 +1085,13 @@ export function stableSerialize(value: unknown): string {
757
1085
 
758
1086
  function normalizePrimaryKey(
759
1087
  tableName: string,
760
- columns: ColumnSchemas,
1088
+ columns: TableColumnsDefinition,
761
1089
  primaryKey?: string | readonly string[],
762
1090
  ): string[] {
763
1091
  if (primaryKey === undefined) {
764
1092
  if (!Object.prototype.hasOwnProperty.call(columns, 'id')) {
765
1093
  throw new Error(
766
- 'Table "' + tableName + '" must define an "id" column or an explicit primaryKey',
1094
+ 'Table "' + tableName + '" must include an "id" column or an explicit primaryKey',
767
1095
  )
768
1096
  }
769
1097