@remix-run/data-table 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +307 -56
  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 +24 -10
  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
@@ -0,0 +1,487 @@
1
+ import type { DatabaseAdapter, ReturningSelection } from '../adapter.ts'
2
+ import { DataTableQueryError, DataTableValidationError } from '../errors.ts'
3
+ import type { ReturningInput } from '../database.ts'
4
+ import type {
5
+ AnyRelation,
6
+ AnyTable,
7
+ TableAfterDeleteContext,
8
+ TableAfterWriteContext,
9
+ TableBeforeDeleteContext,
10
+ TableLifecycleOperation,
11
+ TableRow,
12
+ TableWriteOperation,
13
+ ValidationIssue,
14
+ } from '../table.ts'
15
+ import {
16
+ getTableAfterDelete,
17
+ getTableAfterRead,
18
+ getTableAfterWrite,
19
+ getTableBeforeDelete,
20
+ getTableBeforeWrite,
21
+ getTableColumns,
22
+ getTableName,
23
+ getTableTimestamps,
24
+ getTableValidator,
25
+ } from '../table.ts'
26
+
27
+ type LifecycleCallbackSource =
28
+ | 'beforeWrite'
29
+ | 'validate'
30
+ | 'afterWrite'
31
+ | 'beforeDelete'
32
+ | 'afterDelete'
33
+ | 'afterRead'
34
+
35
+ export function prepareInsertValues<table extends AnyTable>(
36
+ table: table,
37
+ values: Partial<TableRow<table>>,
38
+ now: unknown,
39
+ touch: boolean,
40
+ ): Record<string, unknown> {
41
+ let output = validateWriteValues(table, values, 'create')
42
+ let timestamps = getTableTimestamps(table)
43
+ let columns = getTableColumns(table)
44
+
45
+ if (touch && timestamps) {
46
+ let createdAt = timestamps.createdAt
47
+ let updatedAt = timestamps.updatedAt
48
+
49
+ if (
50
+ Object.prototype.hasOwnProperty.call(columns, createdAt) &&
51
+ output[createdAt] === undefined
52
+ ) {
53
+ output[createdAt] = now
54
+ }
55
+
56
+ if (
57
+ Object.prototype.hasOwnProperty.call(columns, updatedAt) &&
58
+ output[updatedAt] === undefined
59
+ ) {
60
+ output[updatedAt] = now
61
+ }
62
+ }
63
+
64
+ return output
65
+ }
66
+
67
+ export function prepareUpdateValues<table extends AnyTable>(
68
+ table: table,
69
+ values: Partial<TableRow<table>>,
70
+ now: unknown,
71
+ touch: boolean,
72
+ operation: TableWriteOperation = 'update',
73
+ ): Record<string, unknown> {
74
+ let output = validateWriteValues(table, values, operation)
75
+ let timestamps = getTableTimestamps(table)
76
+ let columns = getTableColumns(table)
77
+
78
+ if (touch && timestamps) {
79
+ let updatedAt = timestamps.updatedAt
80
+
81
+ if (
82
+ Object.prototype.hasOwnProperty.call(columns, updatedAt) &&
83
+ output[updatedAt] === undefined
84
+ ) {
85
+ output[updatedAt] = now
86
+ }
87
+ }
88
+
89
+ return output
90
+ }
91
+
92
+ export function applyAfterReadHooksToRows<table extends AnyTable>(
93
+ table: table,
94
+ rows: Record<string, unknown>[],
95
+ ): Record<string, unknown>[] {
96
+ let callback = getTableAfterRead(table)
97
+
98
+ if (!callback || rows.length === 0) {
99
+ return rows
100
+ }
101
+
102
+ let tableName = getTableName(table)
103
+
104
+ return rows.map((row) => {
105
+ let callbackResult = callback({
106
+ tableName,
107
+ value: row as Partial<TableRow<table>>,
108
+ })
109
+ assertSynchronousCallbackResult(tableName, 'read', 'afterRead', callbackResult)
110
+
111
+ if (hasIssues(callbackResult)) {
112
+ throwValidationIssues(tableName, callbackResult.issues, 'read', 'afterRead')
113
+ }
114
+
115
+ if (!hasValue(callbackResult)) {
116
+ throw new DataTableValidationError(
117
+ 'Invalid afterRead callback result for table "' + tableName + '"',
118
+ [{ message: 'Expected afterRead to return { value } or { issues }' }],
119
+ {
120
+ metadata: {
121
+ table: tableName,
122
+ operation: 'read',
123
+ source: 'afterRead',
124
+ },
125
+ },
126
+ )
127
+ }
128
+
129
+ return normalizeReadObject(tableName, callbackResult.value)
130
+ })
131
+ }
132
+
133
+ export function applyAfterReadHooksToLoadedRows(
134
+ table: AnyTable,
135
+ rows: Record<string, unknown>[],
136
+ relationMap: Record<string, AnyRelation>,
137
+ ): Record<string, unknown>[] {
138
+ if (rows.length === 0) {
139
+ return rows
140
+ }
141
+
142
+ let relationNames = Object.keys(relationMap)
143
+
144
+ if (relationNames.length > 0) {
145
+ for (let row of rows) {
146
+ for (let relationName of relationNames) {
147
+ let relation = relationMap[relationName]
148
+ let relationValue = row[relationName]
149
+
150
+ if (relation.cardinality === 'many') {
151
+ if (!Array.isArray(relationValue)) {
152
+ continue
153
+ }
154
+
155
+ row[relationName] = applyAfterReadHooksToLoadedRows(
156
+ relation.targetTable,
157
+ relationValue as Record<string, unknown>[],
158
+ relation.modifiers.with,
159
+ )
160
+ continue
161
+ }
162
+
163
+ if (relationValue === null || relationValue === undefined) {
164
+ continue
165
+ }
166
+
167
+ if (typeof relationValue !== 'object' || Array.isArray(relationValue)) {
168
+ continue
169
+ }
170
+
171
+ let transformed = applyAfterReadHooksToLoadedRows(
172
+ relation.targetTable,
173
+ [relationValue as Record<string, unknown>],
174
+ relation.modifiers.with,
175
+ )
176
+ row[relationName] = transformed[0] ?? null
177
+ }
178
+ }
179
+ }
180
+
181
+ return applyAfterReadHooksToRows(table, rows)
182
+ }
183
+
184
+ export function runBeforeDeleteHook<table extends AnyTable>(
185
+ table: table,
186
+ context: TableBeforeDeleteContext,
187
+ ): void {
188
+ let callback = getTableBeforeDelete(table)
189
+
190
+ if (!callback) {
191
+ return
192
+ }
193
+
194
+ let callbackResult = callback(context)
195
+ assertSynchronousCallbackResult(context.tableName, 'delete', 'beforeDelete', callbackResult)
196
+
197
+ if (callbackResult === undefined) {
198
+ return
199
+ }
200
+
201
+ if (hasIssues(callbackResult)) {
202
+ throwValidationIssues(context.tableName, callbackResult.issues, 'delete', 'beforeDelete')
203
+ }
204
+
205
+ throw new DataTableValidationError(
206
+ 'Invalid beforeDelete callback result for table "' + context.tableName + '"',
207
+ [{ message: 'Expected beforeDelete to return nothing or { issues }' }],
208
+ {
209
+ metadata: {
210
+ table: context.tableName,
211
+ operation: 'delete',
212
+ source: 'beforeDelete',
213
+ },
214
+ },
215
+ )
216
+ }
217
+
218
+ export function runAfterWriteHook<table extends AnyTable>(
219
+ table: table,
220
+ context: TableAfterWriteContext<TableRow<table>>,
221
+ ): void {
222
+ let callback = getTableAfterWrite(table)
223
+
224
+ if (!callback) {
225
+ return
226
+ }
227
+
228
+ let callbackResult = callback(context)
229
+ assertSynchronousCallbackResult(
230
+ context.tableName,
231
+ context.operation,
232
+ 'afterWrite',
233
+ callbackResult,
234
+ )
235
+ }
236
+
237
+ export function runAfterDeleteHook<table extends AnyTable>(
238
+ table: table,
239
+ context: TableAfterDeleteContext,
240
+ ): void {
241
+ let callback = getTableAfterDelete(table)
242
+
243
+ if (!callback) {
244
+ return
245
+ }
246
+
247
+ let callbackResult = callback(context)
248
+ assertSynchronousCallbackResult(context.tableName, 'delete', 'afterDelete', callbackResult)
249
+ }
250
+
251
+ export function assertReturningCapability<row extends Record<string, unknown>>(
252
+ adapter: DatabaseAdapter,
253
+ operation: 'insert' | 'insertMany' | 'update' | 'delete' | 'upsert',
254
+ returning: ReturningInput<row> | undefined,
255
+ ): void {
256
+ if (returning && !adapter.capabilities.returning) {
257
+ throw new DataTableQueryError(operation + '() returning is not supported by this adapter')
258
+ }
259
+ }
260
+
261
+ export function normalizeReturningSelection<row extends Record<string, unknown>>(
262
+ returning: ReturningInput<row>,
263
+ ): ReturningSelection {
264
+ if (returning === '*') {
265
+ return '*'
266
+ }
267
+
268
+ return [...returning]
269
+ }
270
+
271
+ function validateWriteValues<table extends AnyTable>(
272
+ table: table,
273
+ values: Partial<TableRow<table>>,
274
+ operation: TableWriteOperation,
275
+ ): Record<string, unknown> {
276
+ let tableName = getTableName(table)
277
+ let normalizedInput = normalizeWriteObject(table, values, operation)
278
+ let beforeWrite = getTableBeforeWrite(table)
279
+
280
+ if (beforeWrite) {
281
+ let beforeWriteResult = beforeWrite({
282
+ operation,
283
+ tableName,
284
+ value: normalizedInput as Partial<TableRow<table>>,
285
+ })
286
+ assertSynchronousCallbackResult(tableName, operation, 'beforeWrite', beforeWriteResult)
287
+
288
+ if (hasIssues(beforeWriteResult)) {
289
+ throwValidationIssues(tableName, beforeWriteResult.issues, operation, 'beforeWrite')
290
+ }
291
+
292
+ if (!hasValue(beforeWriteResult)) {
293
+ throw new DataTableValidationError(
294
+ 'Invalid beforeWrite callback result for table "' + tableName + '"',
295
+ [{ message: 'Expected beforeWrite to return { value } or { issues }' }],
296
+ {
297
+ metadata: {
298
+ table: tableName,
299
+ operation,
300
+ source: 'beforeWrite',
301
+ },
302
+ },
303
+ )
304
+ }
305
+
306
+ normalizedInput = normalizeWriteObject(table, beforeWriteResult.value, operation, 'beforeWrite')
307
+ }
308
+
309
+ let validator = getTableValidator(table)
310
+
311
+ if (!validator) {
312
+ return normalizedInput
313
+ }
314
+
315
+ let validationResult = validator({
316
+ operation,
317
+ tableName,
318
+ value: normalizedInput as Partial<TableRow<table>>,
319
+ })
320
+ assertSynchronousCallbackResult(tableName, operation, 'validate', validationResult)
321
+
322
+ if (hasIssues(validationResult)) {
323
+ throwValidationIssues(tableName, validationResult.issues, operation, 'validate')
324
+ }
325
+
326
+ if (!hasValue(validationResult)) {
327
+ throw new DataTableValidationError(
328
+ 'Invalid validator result for table "' + tableName + '"',
329
+ [{ message: 'Expected validator to return { value } or { issues }' }],
330
+ {
331
+ metadata: {
332
+ table: tableName,
333
+ operation,
334
+ source: 'validate',
335
+ },
336
+ },
337
+ )
338
+ }
339
+
340
+ return normalizeWriteObject(table, validationResult.value, operation, 'validate')
341
+ }
342
+
343
+ function hasIssues(value: unknown): value is { issues: ReadonlyArray<ValidationIssue> } {
344
+ return typeof value === 'object' && value !== null && 'issues' in value
345
+ }
346
+
347
+ function hasValue(value: unknown): value is { value: unknown } {
348
+ return typeof value === 'object' && value !== null && 'value' in value
349
+ }
350
+
351
+ function normalizeWriteObject<table extends AnyTable>(
352
+ table: table,
353
+ value: unknown,
354
+ operation: TableWriteOperation,
355
+ source?: LifecycleCallbackSource,
356
+ ): Record<string, unknown> {
357
+ let tableName = getTableName(table)
358
+ let columns = getTableColumns(table)
359
+
360
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
361
+ throw new DataTableValidationError(
362
+ 'Invalid value for table "' + tableName + '"',
363
+ [{ message: 'Expected object' }],
364
+ {
365
+ metadata: {
366
+ table: tableName,
367
+ operation,
368
+ ...(source ? { source } : {}),
369
+ },
370
+ },
371
+ )
372
+ }
373
+
374
+ let output: Record<string, unknown> = {}
375
+
376
+ for (let key in value) {
377
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
378
+ continue
379
+ }
380
+
381
+ if (!Object.prototype.hasOwnProperty.call(columns, key)) {
382
+ throw new DataTableValidationError(
383
+ 'Unknown column "' + key + '" for table "' + tableName + '"',
384
+ [],
385
+ {
386
+ metadata: {
387
+ table: tableName,
388
+ column: key,
389
+ operation,
390
+ ...(source ? { source } : {}),
391
+ },
392
+ },
393
+ )
394
+ }
395
+
396
+ output[key] = (value as Record<string, unknown>)[key]
397
+ }
398
+
399
+ return output
400
+ }
401
+
402
+ function throwValidationIssues(
403
+ tableName: string,
404
+ issues: ReadonlyArray<ValidationIssue>,
405
+ operation: TableLifecycleOperation,
406
+ source?: LifecycleCallbackSource,
407
+ ): never {
408
+ let firstIssue = issues[0]
409
+ let issuePath = firstIssue?.path
410
+ let firstPathSegment = issuePath && issuePath.length > 0 ? issuePath[0] : undefined
411
+ let column = typeof firstPathSegment === 'string' ? firstPathSegment : undefined
412
+
413
+ if (column) {
414
+ throw new DataTableValidationError(
415
+ 'Invalid value for column "' + column + '" in table "' + tableName + '"',
416
+ issues,
417
+ {
418
+ metadata: {
419
+ table: tableName,
420
+ column,
421
+ operation,
422
+ ...(source ? { source } : {}),
423
+ },
424
+ },
425
+ )
426
+ }
427
+
428
+ throw new DataTableValidationError('Invalid value for table "' + tableName + '"', issues, {
429
+ metadata: {
430
+ table: tableName,
431
+ operation,
432
+ ...(source ? { source } : {}),
433
+ },
434
+ })
435
+ }
436
+
437
+ function assertSynchronousCallbackResult(
438
+ tableName: string,
439
+ operation: TableLifecycleOperation,
440
+ callbackName: LifecycleCallbackSource,
441
+ value: unknown,
442
+ ): void {
443
+ if (!isPromiseLike(value)) {
444
+ return
445
+ }
446
+
447
+ throw new DataTableValidationError(
448
+ 'Invalid ' + callbackName + ' callback result for table "' + tableName + '"',
449
+ [{ message: callbackName + ' callbacks must be synchronous and cannot return a Promise' }],
450
+ {
451
+ metadata: {
452
+ table: tableName,
453
+ operation,
454
+ source: callbackName,
455
+ },
456
+ },
457
+ )
458
+ }
459
+
460
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
461
+ return (
462
+ (typeof value === 'object' || typeof value === 'function') &&
463
+ value !== null &&
464
+ 'then' in value &&
465
+ typeof (value as { then?: unknown }).then === 'function'
466
+ )
467
+ }
468
+
469
+ function normalizeReadObject(tableName: string, value: unknown): Record<string, unknown> {
470
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
471
+ throw new DataTableValidationError(
472
+ 'Invalid afterRead callback result for table "' + tableName + '"',
473
+ [{ message: 'Expected afterRead to return an object value' }],
474
+ {
475
+ metadata: {
476
+ table: tableName,
477
+ operation: 'read',
478
+ source: 'afterRead',
479
+ },
480
+ },
481
+ )
482
+ }
483
+
484
+ return {
485
+ ...(value as Record<string, unknown>),
486
+ }
487
+ }