remix 3.0.0-beta.0 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/node-tsx/load-module.d.ts +2 -0
- package/dist/node-tsx/load-module.d.ts.map +1 -0
- package/dist/node-tsx/load-module.js +2 -0
- package/dist/node-tsx.d.ts +3 -0
- package/dist/node-tsx.d.ts.map +1 -0
- package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
- package/dist/render-middleware.d.ts +2 -0
- package/dist/render-middleware.d.ts.map +1 -0
- package/dist/render-middleware.js +2 -0
- package/dist/route-pattern/href.d.ts +2 -0
- package/dist/route-pattern/href.d.ts.map +1 -0
- package/dist/route-pattern/href.js +2 -0
- package/dist/route-pattern/join.d.ts +2 -0
- package/dist/route-pattern/join.d.ts.map +1 -0
- package/dist/route-pattern/join.js +2 -0
- package/dist/route-pattern/match.d.ts +2 -0
- package/dist/route-pattern/match.d.ts.map +1 -0
- package/dist/route-pattern/match.js +2 -0
- package/package.json +158 -44
- package/src/assert/README.md +109 -0
- package/src/assets/README.md +539 -0
- package/src/async-context-middleware/README.md +100 -0
- package/src/auth/README.md +445 -0
- package/src/auth-middleware/README.md +246 -0
- package/src/cli/README.md +78 -0
- package/src/compression-middleware/README.md +176 -0
- package/src/cookie/README.md +106 -0
- package/src/cop-middleware/README.md +117 -0
- package/src/cors-middleware/README.md +174 -0
- package/src/csrf-middleware/README.md +99 -0
- package/src/data-schema/README.md +422 -0
- package/src/data-table/README.md +552 -0
- package/src/data-table-mysql/README.md +97 -0
- package/src/data-table-postgres/README.md +74 -0
- package/src/data-table-sqlite/README.md +84 -0
- package/src/fetch-proxy/README.md +46 -0
- package/src/fetch-router/README.md +902 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +47 -0
- package/src/form-data-middleware/README.md +109 -0
- package/src/form-data-parser/README.md +160 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +629 -0
- package/src/html-template/README.md +101 -0
- package/src/lazy-file/README.md +109 -0
- package/src/logger-middleware/README.md +132 -0
- package/src/method-override-middleware/README.md +71 -0
- package/src/mime/README.md +110 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +352 -0
- package/src/node-tsx/README.md +79 -0
- package/src/node-tsx/load-module.ts +2 -0
- package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
- package/src/render-middleware/README.md +99 -0
- package/src/render-middleware.ts +2 -0
- package/src/route-pattern/README.md +291 -0
- package/src/route-pattern/href.ts +2 -0
- package/src/route-pattern/join.ts +2 -0
- package/src/route-pattern/match.ts +2 -0
- package/src/session/README.md +171 -0
- package/src/session-middleware/README.md +109 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +37 -0
- package/src/static-middleware/README.md +89 -0
- package/src/tar-parser/README.md +74 -0
- package/src/terminal/README.md +92 -0
- package/src/test/README.md +430 -0
- package/src/ui/README.md +219 -0
- package/src/ui/accordion/README.md +166 -0
- package/src/ui/anchor/README.md +153 -0
- package/src/ui/animation/README.md +316 -0
- package/src/ui/breadcrumbs/README.md +55 -0
- package/src/ui/button/README.md +44 -0
- package/src/ui/combobox/README.md +145 -0
- package/src/ui/glyph/README.md +72 -0
- package/src/ui/listbox/README.md +115 -0
- package/src/ui/menu/README.md +96 -0
- package/src/ui/popover/README.md +122 -0
- package/src/ui/scroll-lock/README.md +33 -0
- package/src/ui/select/README.md +107 -0
- package/src/ui/server/README.md +90 -0
- package/src/ui/test/README.md +107 -0
- package/src/ui/theme/README.md +103 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# data-table
|
|
2
|
+
|
|
3
|
+
Typed relational query toolkit for JavaScript runtimes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite adapters
|
|
8
|
+
- **One Query API**: Build reusable `Query` objects with `query(table)` and execute them with `db.exec(...)`, or use `db.query(table)` as shorthand
|
|
9
|
+
- **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys
|
|
10
|
+
- **Optional Runtime Validation**: Add `validate(context)` at the table level for create/update validation and coercion
|
|
11
|
+
- **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading
|
|
12
|
+
- **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction
|
|
13
|
+
- **First-Class Migrations**: Plain SQL `up.sql`/`down.sql` files with a journaling runner and dry-run planning
|
|
14
|
+
- **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)`
|
|
15
|
+
|
|
16
|
+
`data-table` gives you two complementary APIs:
|
|
17
|
+
|
|
18
|
+
- [**Query Objects**](#query-objects) for expressive joins, aggregates, eager loading, and scoped writes
|
|
19
|
+
- [**CRUD Helpers**](#crud-helpers) for common create/read/update/delete flows (`find`, `create`, `update`, `delete`)
|
|
20
|
+
|
|
21
|
+
Both APIs are type-safe. Runtime validation is opt-in with table-level `validate(context)`.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm i remix
|
|
27
|
+
npm i pg
|
|
28
|
+
# or
|
|
29
|
+
npm i mysql2
|
|
30
|
+
# or
|
|
31
|
+
# use the SQLite client built into your runtime
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
Define tables once, then create a database with an adapter.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Pool } from 'pg'
|
|
40
|
+
import { column as c, createDatabase, hasMany, query, table } from 'remix/data-table'
|
|
41
|
+
import { createPostgresDatabaseAdapter } from 'remix/data-table/postgres'
|
|
42
|
+
|
|
43
|
+
let users = table({
|
|
44
|
+
name: 'users',
|
|
45
|
+
columns: {
|
|
46
|
+
id: c.uuid(),
|
|
47
|
+
email: c.varchar(255),
|
|
48
|
+
role: c.enum(['customer', 'admin']),
|
|
49
|
+
created_at: c.integer(),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
let orders = table({
|
|
54
|
+
name: 'orders',
|
|
55
|
+
columns: {
|
|
56
|
+
id: c.uuid(),
|
|
57
|
+
user_id: c.uuid(),
|
|
58
|
+
status: c.enum(['pending', 'processing', 'shipped', 'delivered']),
|
|
59
|
+
total: c.decimal(10, 2),
|
|
60
|
+
created_at: c.integer(),
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
let userOrders = hasMany(users, orders)
|
|
65
|
+
|
|
66
|
+
let pool = new Pool({ connectionString: process.env.DATABASE_URL })
|
|
67
|
+
let db = createDatabase(createPostgresDatabaseAdapter(pool))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Query Objects
|
|
71
|
+
|
|
72
|
+
Use `query(table)` when you want to build a standalone reusable query object. Execute it later with `db.exec(query)`. Use `db.query(table)` when you want the same chainable `Query` already bound to a database instance.
|
|
73
|
+
|
|
74
|
+
### Standalone Query Builder
|
|
75
|
+
|
|
76
|
+
`query(table)` is the primary query-builder API. It gives you an unbound `Query` value that can be composed, stored, reused, and executed against any compatible database instance.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { eq, ilike, query } from 'remix/data-table'
|
|
80
|
+
|
|
81
|
+
let pendingOrdersForExampleUsers = query(orders)
|
|
82
|
+
.join(users, eq(orders.user_id, users.id))
|
|
83
|
+
.where({ status: 'pending' })
|
|
84
|
+
.where(ilike(users.email, '%@example.com'))
|
|
85
|
+
.select({
|
|
86
|
+
orderId: orders.id,
|
|
87
|
+
customerEmail: users.email,
|
|
88
|
+
total: orders.total,
|
|
89
|
+
placedAt: orders.created_at,
|
|
90
|
+
})
|
|
91
|
+
.orderBy(orders.created_at, 'desc')
|
|
92
|
+
.limit(20)
|
|
93
|
+
|
|
94
|
+
let recentPendingOrders = await db.exec(pendingOrdersForExampleUsers)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Unbound queries stay lazy until you pass them to `db.exec(...)`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
let shippedCustomerQuery = query(users)
|
|
101
|
+
.where({ role: 'customer' })
|
|
102
|
+
.with({
|
|
103
|
+
recentOrders: userOrders.where({ status: 'shipped' }).orderBy('created_at', 'desc').limit(3),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
let customers = await db.exec(shippedCustomerQuery)
|
|
107
|
+
|
|
108
|
+
// customers[0].recentOrders is fully typed
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The same standalone query builder also handles terminal read and write operations:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
let nextPendingOrder = await db.exec(
|
|
115
|
+
query(orders).where({ status: 'pending' }).orderBy('created_at', 'asc').first(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await db.exec(
|
|
119
|
+
query(orders)
|
|
120
|
+
.where({ status: 'pending' })
|
|
121
|
+
.orderBy('created_at', 'asc')
|
|
122
|
+
.limit(100)
|
|
123
|
+
.update({ status: 'processing' }),
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Bound Query Shorthand
|
|
128
|
+
|
|
129
|
+
If you already have a `db` instance in hand and do not need a standalone query value, `db.query(table)` returns the same query builder already bound to that database:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
let recentPendingOrders = await db
|
|
133
|
+
.query(orders)
|
|
134
|
+
.where({ status: 'pending' })
|
|
135
|
+
.orderBy('created_at', 'desc')
|
|
136
|
+
.limit(20)
|
|
137
|
+
.all()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## CRUD Helpers
|
|
141
|
+
|
|
142
|
+
`data-table` provides helpers for common create/read/update/delete operations. Use these helpers for common operations without building a full query chain.
|
|
143
|
+
|
|
144
|
+
### Read operations
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { or } from 'remix/data-table'
|
|
148
|
+
|
|
149
|
+
let user = await db.find(users, 'u_001')
|
|
150
|
+
|
|
151
|
+
let firstPending = await db.findOne(orders, {
|
|
152
|
+
where: { status: 'pending' },
|
|
153
|
+
orderBy: ['created_at', 'asc'],
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
let page = await db.findMany(orders, {
|
|
157
|
+
where: or({ status: 'pending' }, { status: 'processing' }),
|
|
158
|
+
orderBy: [
|
|
159
|
+
['status', 'asc'],
|
|
160
|
+
['created_at', 'desc'],
|
|
161
|
+
],
|
|
162
|
+
limit: 50,
|
|
163
|
+
offset: 0,
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`where` accepts the same single-table object/predicate inputs as `query().where(...)`, and `orderBy` uses tuple form:
|
|
168
|
+
|
|
169
|
+
- `['column', 'asc' | 'desc']`
|
|
170
|
+
- `[['columnA', 'asc'], ['columnB', 'desc']]`
|
|
171
|
+
|
|
172
|
+
### Create helpers
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// Default: metadata (affectedRows/insertId)
|
|
176
|
+
let createResult = await db.create(users, {
|
|
177
|
+
id: 'u_002',
|
|
178
|
+
email: 'sam@example.com',
|
|
179
|
+
role: 'customer',
|
|
180
|
+
created_at: Date.now(),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Return a typed row (with optional relations)
|
|
184
|
+
let createdUser = await db.create(
|
|
185
|
+
users,
|
|
186
|
+
{
|
|
187
|
+
id: 'u_003',
|
|
188
|
+
email: 'pat@example.com',
|
|
189
|
+
role: 'customer',
|
|
190
|
+
created_at: Date.now(),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
returnRow: true,
|
|
194
|
+
with: { recentOrders: userOrders.orderBy('created_at', 'desc').limit(1) },
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Bulk insert metadata
|
|
199
|
+
let createManyResult = await db.createMany(orders, [
|
|
200
|
+
{ id: 'o_101', user_id: 'u_002', status: 'pending', total: 24.99, created_at: Date.now() },
|
|
201
|
+
{ id: 'o_102', user_id: 'u_003', status: 'pending', total: 48.5, created_at: Date.now() },
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
// Return inserted rows (requires adapter RETURNING support)
|
|
205
|
+
let insertedRows = await db.createMany(
|
|
206
|
+
orders,
|
|
207
|
+
[{ id: 'o_103', user_id: 'u_003', status: 'pending', total: 12, created_at: Date.now() }],
|
|
208
|
+
{ returnRows: true },
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`createMany`/`insertMany` throw when every row in the batch is empty (no explicit values).
|
|
213
|
+
|
|
214
|
+
### Update and delete helpers
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
let updatedUser = await db.update(users, 'u_003', { role: 'admin' })
|
|
218
|
+
|
|
219
|
+
let updateManyResult = await db.updateMany(
|
|
220
|
+
orders,
|
|
221
|
+
{ status: 'processing' },
|
|
222
|
+
{
|
|
223
|
+
where: { status: 'pending' },
|
|
224
|
+
orderBy: ['created_at', 'asc'],
|
|
225
|
+
limit: 25,
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
let deletedUser = await db.delete(users, 'u_002')
|
|
230
|
+
|
|
231
|
+
let deleteManyResult = await db.deleteMany(orders, {
|
|
232
|
+
where: { status: 'delivered' },
|
|
233
|
+
orderBy: [['created_at', 'asc']],
|
|
234
|
+
limit: 200,
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`db.update(...)` throws when the target row cannot be found.
|
|
239
|
+
|
|
240
|
+
Return behavior:
|
|
241
|
+
|
|
242
|
+
- `find`/`findOne` -> row or `null`
|
|
243
|
+
- `findMany` -> rows
|
|
244
|
+
- `create` -> `WriteResult` by default, row when `returnRow: true`
|
|
245
|
+
- `createMany` -> `WriteResult` by default, rows when `returnRows: true` (not supported in MySQL because it doesn't support `RETURNING`)
|
|
246
|
+
- `update` -> updated row (throws when target row is missing)
|
|
247
|
+
- `updateMany`/`deleteMany` -> `WriteResult`
|
|
248
|
+
- `delete` -> `boolean`
|
|
249
|
+
|
|
250
|
+
### Validation and Lifecycle
|
|
251
|
+
|
|
252
|
+
Validation is optional and table-scoped. Define `validate(context)` to validate/coerce write
|
|
253
|
+
payloads, and add lifecycle callbacks when you need custom read/write/delete behavior.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { column as c, fail, table } from 'remix/data-table'
|
|
257
|
+
|
|
258
|
+
let payments = table({
|
|
259
|
+
name: 'payments',
|
|
260
|
+
columns: {
|
|
261
|
+
id: c.uuid(),
|
|
262
|
+
amount: c.decimal(10, 2),
|
|
263
|
+
},
|
|
264
|
+
beforeWrite({ value }) {
|
|
265
|
+
return {
|
|
266
|
+
value: {
|
|
267
|
+
...value,
|
|
268
|
+
amount: typeof value.amount === 'string' ? value.amount.trim() : value.amount,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
validate({ operation, value }) {
|
|
273
|
+
if (operation === 'create' && typeof value.amount === 'string') {
|
|
274
|
+
let amount = Number(value.amount)
|
|
275
|
+
|
|
276
|
+
if (!Number.isFinite(amount)) {
|
|
277
|
+
return fail('Expected a numeric amount', ['amount'])
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { value: { ...value, amount } }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { value }
|
|
284
|
+
},
|
|
285
|
+
beforeDelete({ where }) {
|
|
286
|
+
if (where.length === 0) {
|
|
287
|
+
return fail('Refusing unscoped delete')
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
afterRead({ value }) {
|
|
291
|
+
if (!('amount' in value)) {
|
|
292
|
+
return { value }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
value: {
|
|
297
|
+
...value,
|
|
298
|
+
// Example read-time shaping
|
|
299
|
+
amount:
|
|
300
|
+
typeof value.amount === 'number' ? Math.round(value.amount * 100) / 100 : value.amount,
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Use `fail(...)` in hooks when you want to return issues without manually building `{ issues: [...] }`.
|
|
308
|
+
|
|
309
|
+
Validation and lifecycle semantics:
|
|
310
|
+
|
|
311
|
+
- Write order is `beforeWrite -> validate -> timestamp/default touch -> execute -> afterWrite`
|
|
312
|
+
- `validate` runs for writes (`create`, `createMany`, `insert`, `insertMany`, `update`, `updateMany`, `upsert`)
|
|
313
|
+
- Hook context includes `{ operation: 'create' | 'update', tableName, value }`
|
|
314
|
+
- Write payloads are partial objects
|
|
315
|
+
- Unknown columns fail validation before and after hook processing
|
|
316
|
+
- `beforeDelete` can veto deletes by returning `{ issues }`
|
|
317
|
+
- `afterDelete` runs after successful deletes with `affectedRows`
|
|
318
|
+
- `afterRead` runs for each loaded row (root rows, eager-loaded relation rows, and write-returning rows)
|
|
319
|
+
- `afterRead` receives the current read shape, which may be partial/projection rows; guard field access accordingly
|
|
320
|
+
- Predicate values (`where`, `having`, join predicates) are not runtime-validated
|
|
321
|
+
- Lifecycle callbacks are synchronous; returning a Promise throws a validation error
|
|
322
|
+
- Callback validation errors include `metadata.source` (`beforeWrite`, `validate`, `beforeDelete`, `afterRead`, etc.) for easier debugging
|
|
323
|
+
- Callbacks do not introduce implicit transactions (use `db.transaction(...)` when you need rollback guarantees)
|
|
324
|
+
|
|
325
|
+
## Transactions
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
await db.transaction(async (tx) => {
|
|
329
|
+
let user = await tx.create(
|
|
330
|
+
users,
|
|
331
|
+
{ id: 'u_010', email: 'new@example.com', role: 'customer', created_at: Date.now() },
|
|
332
|
+
{ returnRow: true },
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
await tx.create(orders, {
|
|
336
|
+
id: 'o_500',
|
|
337
|
+
user_id: user.id,
|
|
338
|
+
status: 'pending',
|
|
339
|
+
total: 79,
|
|
340
|
+
created_at: Date.now(),
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Migrations
|
|
346
|
+
|
|
347
|
+
`data-table` ships a SQL-first migration system under `remix/data-table/migrations`. Each migration
|
|
348
|
+
is a directory containing hand-written `up.sql` and (optionally) `down.sql`. The runner journals
|
|
349
|
+
applied migrations, detects checksum drift, and wraps each migration in a transaction when the
|
|
350
|
+
adapter supports transactional DDL.
|
|
351
|
+
|
|
352
|
+
### Example Setup
|
|
353
|
+
|
|
354
|
+
```txt
|
|
355
|
+
app/
|
|
356
|
+
db/
|
|
357
|
+
migrations/
|
|
358
|
+
20260228090000_create_users/
|
|
359
|
+
up.sql
|
|
360
|
+
down.sql
|
|
361
|
+
20260301113000_add_user_status/
|
|
362
|
+
up.sql
|
|
363
|
+
down.sql
|
|
364
|
+
migrate.ts
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- Keep migration directories in one parent directory (for example `app/db/migrations`).
|
|
368
|
+
- Each directory is named `YYYYMMDDHHmmss_<slug>`.
|
|
369
|
+
- `up.sql` is required. `down.sql` is optional (omit for irreversible migrations).
|
|
370
|
+
- Scripts may contain multiple statements. `id` and `name` are inferred from the directory name.
|
|
371
|
+
|
|
372
|
+
### Migration File Example
|
|
373
|
+
|
|
374
|
+
`20260228090000_create_users/up.sql`:
|
|
375
|
+
|
|
376
|
+
```sql
|
|
377
|
+
create table users (
|
|
378
|
+
id serial primary key,
|
|
379
|
+
email varchar(255) not null unique,
|
|
380
|
+
created_at timestamptz not null default now()
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
create unique index users_email_idx on users (email);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`20260228090000_create_users/down.sql`:
|
|
387
|
+
|
|
388
|
+
```sql
|
|
389
|
+
drop index if exists users_email_idx;
|
|
390
|
+
drop table if exists users;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Multi-Statement Driver Configuration
|
|
394
|
+
|
|
395
|
+
The runner sends each migration to the adapter as a single multi-statement script. Make sure the
|
|
396
|
+
underlying driver accepts multiple statements:
|
|
397
|
+
|
|
398
|
+
- `better-sqlite3`: works out of the box (`db.exec`).
|
|
399
|
+
- `pg`: works out of the box when no parameter array is passed.
|
|
400
|
+
- `mysql2`: requires `multipleStatements: true` on the connection/pool.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { createPool } from 'mysql2/promise'
|
|
404
|
+
|
|
405
|
+
let pool = createPool({
|
|
406
|
+
uri: process.env.DATABASE_URL,
|
|
407
|
+
multipleStatements: true,
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Runner Script Example
|
|
412
|
+
|
|
413
|
+
In `app/db/migrate.ts`:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import path from 'node:path'
|
|
417
|
+
import { Pool } from 'pg'
|
|
418
|
+
import { createPostgresDatabaseAdapter } from 'remix/data-table/postgres'
|
|
419
|
+
import { createMigrationRunner } from 'remix/data-table/migrations'
|
|
420
|
+
import { loadMigrations } from 'remix/data-table/migrations/node'
|
|
421
|
+
|
|
422
|
+
let directionArg = process.argv[2] ?? 'up'
|
|
423
|
+
let direction = directionArg === 'down' ? 'down' : 'up'
|
|
424
|
+
let to = process.argv[3]
|
|
425
|
+
|
|
426
|
+
let pool = new Pool({ connectionString: process.env.DATABASE_URL })
|
|
427
|
+
let adapter = createPostgresDatabaseAdapter(pool)
|
|
428
|
+
let migrations = await loadMigrations(path.resolve('app/db/migrations'))
|
|
429
|
+
let runner = createMigrationRunner(adapter, migrations)
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
let result = direction === 'up' ? await runner.up({ to }) : await runner.down({ to })
|
|
433
|
+
console.log(direction + ' complete', {
|
|
434
|
+
applied: result.applied.map((entry) => entry.id),
|
|
435
|
+
reverted: result.reverted.map((entry) => entry.id),
|
|
436
|
+
})
|
|
437
|
+
} finally {
|
|
438
|
+
await pool.end()
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Use `journalTable` if you want a custom migrations journal table name:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
let runner = createMigrationRunner(adapter, migrations, {
|
|
446
|
+
journalTable: 'app_migrations',
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Run it with your runtime, for example:
|
|
451
|
+
|
|
452
|
+
```sh
|
|
453
|
+
node ./app/db/migrate.ts up
|
|
454
|
+
node ./app/db/migrate.ts up 20260301113000
|
|
455
|
+
node ./app/db/migrate.ts down
|
|
456
|
+
node ./app/db/migrate.ts down 20260228090000
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Use `step` for bounded rollforward/rollback behavior instead of a target id:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
await runner.up({ step: 1 })
|
|
463
|
+
await runner.down({ step: 1 })
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`to` and `step` are mutually exclusive within a single run.
|
|
467
|
+
|
|
468
|
+
Use `dryRun` to inspect the SQL plan without applying or journaling anything:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
let plan = await runner.up({ dryRun: true })
|
|
472
|
+
for (let script of plan.sql) {
|
|
473
|
+
console.log(script)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Transaction Modes
|
|
478
|
+
|
|
479
|
+
By default each migration is wrapped in a transaction when the adapter supports transactional DDL.
|
|
480
|
+
Override per migration with a directive on the first non-blank line of `up.sql`:
|
|
481
|
+
|
|
482
|
+
```sql
|
|
483
|
+
-- data-table/transaction: none
|
|
484
|
+
create index concurrently users_email_active_idx on users (email) where status = 'active';
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Supported modes:
|
|
488
|
+
|
|
489
|
+
- `auto` (default): wrap when the adapter supports transactional DDL.
|
|
490
|
+
- `required`: wrap; the runner throws if the adapter cannot support it.
|
|
491
|
+
- `none`: never wrap. Use this for statements like postgres `CREATE INDEX CONCURRENTLY` that
|
|
492
|
+
cannot run inside a transaction.
|
|
493
|
+
|
|
494
|
+
You can also set `transaction` directly on a `MigrationDescriptor` when registering migrations
|
|
495
|
+
programmatically.
|
|
496
|
+
|
|
497
|
+
### Programmatic Registration
|
|
498
|
+
|
|
499
|
+
For non-filesystem runtimes, register migrations directly:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { createMigrationRegistry, createMigrationRunner } from 'remix/data-table/migrations'
|
|
503
|
+
|
|
504
|
+
let registry = createMigrationRegistry()
|
|
505
|
+
registry.register({
|
|
506
|
+
id: '20260228090000',
|
|
507
|
+
name: 'create_users',
|
|
508
|
+
up: 'create table users (id serial primary key, email text not null);',
|
|
509
|
+
down: 'drop table users;',
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
let runner = createMigrationRunner(adapter, registry)
|
|
513
|
+
await runner.up()
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Raw SQL Escape Hatch
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { rawSql, sql } from 'remix/data-table'
|
|
520
|
+
|
|
521
|
+
await db.exec(sql`select * from users where id = ${'u_001'}`)
|
|
522
|
+
await db.exec(rawSql('update users set role = ? where id = ?', ['admin', 'u_001']))
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Use `sql` when you need raw SQL plus safe value interpolation:
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { sql } from 'remix/data-table'
|
|
529
|
+
|
|
530
|
+
let email = input.email
|
|
531
|
+
let minCreatedAt = input.minCreatedAt
|
|
532
|
+
|
|
533
|
+
let result = await db.exec(sql`
|
|
534
|
+
select id, email
|
|
535
|
+
from users
|
|
536
|
+
where email = ${email}
|
|
537
|
+
and created_at >= ${minCreatedAt}
|
|
538
|
+
`)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`sql` keeps values parameterized per adapter dialect, so you can avoid manual string concatenation.
|
|
542
|
+
|
|
543
|
+
## Related Packages
|
|
544
|
+
|
|
545
|
+
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Optional schema parsing you can use inside table-level `validate(...)` hooks
|
|
546
|
+
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
|
|
547
|
+
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter
|
|
548
|
+
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter
|
|
549
|
+
|
|
550
|
+
## License
|
|
551
|
+
|
|
552
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# data-table-mysql
|
|
2
|
+
|
|
3
|
+
MySQL adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).
|
|
4
|
+
Use this package when you want `data-table` APIs backed by `mysql2`.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Native `mysql2` Integration**: Works with `mysql2/promise` `Pool` and `PoolConnection` instances
|
|
9
|
+
- **Full `data-table` API Support**: Queries, relations, writes, and transactions
|
|
10
|
+
- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with optional shared pure helpers from `data-table`
|
|
11
|
+
- **Multi-Statement Migrations**: `executeScript()` runs `up.sql` / `down.sql` files via `mysql2` (requires `multipleStatements: true`)
|
|
12
|
+
- **MySQL Capabilities Enabled By Default**:
|
|
13
|
+
- `returning: false`
|
|
14
|
+
- `savepoints: true`
|
|
15
|
+
- `upsert: true`
|
|
16
|
+
- `transactionalDdl: false`
|
|
17
|
+
- `migrationLock: true`
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm i remix mysql2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createPool } from 'mysql2/promise'
|
|
29
|
+
import { createDatabase } from 'remix/data-table'
|
|
30
|
+
import { createMysqlDatabaseAdapter } from 'remix/data-table/mysql'
|
|
31
|
+
|
|
32
|
+
let pool = createPool(process.env.DATABASE_URL as string)
|
|
33
|
+
let db = createDatabase(createMysqlDatabaseAdapter(pool))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `db.query(...)`, relation loading, and transactions from `remix/data-table`.
|
|
37
|
+
Import any driver-specific types you need directly from `mysql2/promise`.
|
|
38
|
+
|
|
39
|
+
## Adapter Capabilities
|
|
40
|
+
|
|
41
|
+
`data-table-mysql` reports this capability set by default:
|
|
42
|
+
|
|
43
|
+
- `returning: false`
|
|
44
|
+
- `savepoints: true`
|
|
45
|
+
- `upsert: true`
|
|
46
|
+
- `transactionalDdl: false`
|
|
47
|
+
- `migrationLock: true`
|
|
48
|
+
|
|
49
|
+
## Advanced Usage
|
|
50
|
+
|
|
51
|
+
### Multi-Statement Migrations
|
|
52
|
+
|
|
53
|
+
`remix/data-table/migrations` sends each migration to the adapter as a single multi-statement SQL
|
|
54
|
+
script. mysql2 only accepts multi-statement scripts when the connection is created with
|
|
55
|
+
`multipleStatements: true`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createPool } from 'mysql2/promise'
|
|
59
|
+
|
|
60
|
+
let pool = createPool({
|
|
61
|
+
uri: process.env.DATABASE_URL,
|
|
62
|
+
multipleStatements: true,
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `returning` On MySQL
|
|
67
|
+
|
|
68
|
+
MySQL does not natively support SQL `RETURNING`. In this adapter, using `returning` on write
|
|
69
|
+
operations throws `DataTableQueryError`.
|
|
70
|
+
|
|
71
|
+
Use write metadata (`affectedRows`, `insertId`) on MySQL, or switch adapters when returned rows
|
|
72
|
+
are required.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { DataTableQueryError } from 'remix/data-table'
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await db
|
|
79
|
+
.query(Accounts)
|
|
80
|
+
.insert({ email: 'a@example.com', status: 'active' }, { returning: ['id'] })
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof DataTableQueryError) {
|
|
83
|
+
// insert() returning is not supported by this adapter
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Related Packages
|
|
89
|
+
|
|
90
|
+
- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API
|
|
91
|
+
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation
|
|
92
|
+
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter
|
|
93
|
+
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|