@remix-run/data-table 0.2.1 → 0.4.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.
- package/README.md +251 -123
- package/dist/cli.d.ts +54 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +53 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/lib/column.d.ts +1 -1
- package/dist/lib/column.d.ts.map +1 -1
- package/dist/lib/column.js +10 -1
- package/dist/lib/database/execution-context.d.ts +5 -5
- package/dist/lib/database/execution-context.d.ts.map +1 -1
- package/dist/lib/database/execution-context.js +1 -0
- package/dist/lib/database/helpers.js +6 -6
- package/dist/lib/database/query-execution.d.ts.map +1 -1
- package/dist/lib/database/query-execution.js +17 -17
- package/dist/lib/database/relations.js +5 -5
- package/dist/lib/database/write-lifecycle.d.ts +2 -2
- package/dist/lib/database/write-lifecycle.d.ts.map +1 -1
- package/dist/lib/database/write-lifecycle.js +5 -5
- package/dist/lib/database.d.ts +74 -59
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +176 -83
- package/dist/lib/driver.d.ts +287 -0
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/errors.d.ts +2 -2
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +4 -4
- package/dist/lib/migrations/directive.d.ts +12 -0
- package/dist/lib/migrations/directive.d.ts.map +1 -0
- package/dist/lib/migrations/directive.js +28 -0
- package/dist/lib/migrations/directory-name.d.ts +12 -0
- package/dist/lib/migrations/directory-name.d.ts.map +1 -0
- package/dist/lib/migrations/directory-name.js +18 -0
- package/dist/lib/migrations/journal-store.d.ts +7 -7
- package/dist/lib/migrations/journal-store.d.ts.map +1 -1
- package/dist/lib/migrations/journal-store.js +37 -28
- package/dist/lib/migrations/registry.d.ts +1 -1
- package/dist/lib/migrations/registry.js +1 -1
- package/dist/lib/migrations/runner.d.ts +12 -19
- package/dist/lib/migrations/runner.d.ts.map +1 -1
- package/dist/lib/migrations/runner.js +170 -175
- package/dist/lib/migrations-node.d.ts +26 -5
- package/dist/lib/migrations-node.d.ts.map +1 -1
- package/dist/lib/migrations-node.js +73 -41
- package/dist/lib/migrations.d.ts +63 -209
- package/dist/lib/migrations.d.ts.map +1 -1
- package/dist/lib/migrations.js +1 -38
- package/dist/lib/operators.d.ts +1 -1
- package/dist/lib/operators.js +1 -1
- package/dist/lib/query.d.ts +34 -1
- package/dist/lib/query.d.ts.map +1 -1
- package/dist/lib/query.js +32 -4
- package/dist/lib/sql-helpers.d.ts +1 -7
- package/dist/lib/sql-helpers.d.ts.map +1 -1
- package/dist/lib/sql-helpers.js +0 -16
- package/dist/lib/sql.d.ts +1 -1
- package/dist/lib/sql.js +1 -1
- package/dist/lib/table.d.ts +1 -1
- package/dist/lib/table.d.ts.map +1 -1
- package/dist/lib/table.js +5 -5
- package/dist/migrations/node.d.ts +1 -1
- package/dist/migrations/node.d.ts.map +1 -1
- package/dist/migrations/node.js +1 -1
- package/dist/migrations.d.ts +2 -6
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -5
- package/dist/operators.js +1 -1
- package/dist/sql-helpers.d.ts +1 -1
- package/dist/sql-helpers.d.ts.map +1 -1
- package/dist/sql-helpers.js +1 -1
- package/package.json +13 -9
- package/src/cli.ts +120 -0
- package/src/index.ts +20 -54
- package/src/lib/column.ts +14 -2
- package/src/lib/database/execution-context.ts +10 -6
- package/src/lib/database/helpers.ts +1 -1
- package/src/lib/database/query-execution.ts +15 -11
- package/src/lib/database/write-lifecycle.ts +4 -4
- package/src/lib/database.ts +223 -96
- package/src/lib/driver.ts +333 -0
- package/src/lib/errors.ts +4 -4
- package/src/lib/migrations/directive.ts +38 -0
- package/src/lib/migrations/directory-name.ts +23 -0
- package/src/lib/migrations/journal-store.ts +42 -28
- package/src/lib/migrations/registry.ts +1 -1
- package/src/lib/migrations/runner.ts +224 -205
- package/src/lib/migrations-node.ts +80 -38
- package/src/lib/migrations.ts +62 -228
- package/src/lib/operators.ts +1 -1
- package/src/lib/query.ts +34 -1
- package/src/lib/sql-helpers.ts +1 -22
- package/src/lib/sql.ts +1 -1
- package/src/lib/table.ts +1 -1
- package/src/migrations/node.ts +1 -1
- package/src/migrations.ts +5 -16
- package/src/sql-helpers.ts +0 -1
- package/dist/lib/adapter.d.ts +0 -550
- package/dist/lib/adapter.d.ts.map +0 -1
- package/dist/lib/migrations/filename.d.ts +0 -12
- package/dist/lib/migrations/filename.d.ts.map +0 -1
- package/dist/lib/migrations/filename.js +0 -20
- package/dist/lib/migrations/helpers.d.ts +0 -11
- package/dist/lib/migrations/helpers.d.ts.map +0 -1
- package/dist/lib/migrations/helpers.js +0 -77
- package/dist/lib/migrations/schema-api.d.ts +0 -7
- package/dist/lib/migrations/schema-api.d.ts.map +0 -1
- package/dist/lib/migrations/schema-api.js +0 -326
- package/src/lib/adapter.ts +0 -653
- package/src/lib/migrations/filename.ts +0 -25
- package/src/lib/migrations/helpers.ts +0 -108
- package/src/lib/migrations/schema-api.ts +0 -417
- /package/dist/lib/{adapter.js → driver.js} +0 -0
package/README.md
CHANGED
|
@@ -4,13 +4,14 @@ Typed relational query toolkit for JavaScript runtimes.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite
|
|
7
|
+
- **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite
|
|
8
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
9
|
- **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys
|
|
10
10
|
- **Optional Runtime Validation**: Add `validate(context)` at the table level for create/update validation and coercion
|
|
11
11
|
- **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading
|
|
12
12
|
- **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction
|
|
13
|
-
- **First-Class Migrations**:
|
|
13
|
+
- **First-Class Migrations**: Plain SQL `up.sql`/`down.sql` files with a journaling runner and dry-run planning
|
|
14
|
+
- **Database Lifecycle Commands**: Wipe, migrate, inspect, seed, and reset through `remix db`
|
|
14
15
|
- **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)`
|
|
15
16
|
|
|
16
17
|
`data-table` gives you two complementary APIs:
|
|
@@ -33,12 +34,11 @@ npm i mysql2
|
|
|
33
34
|
|
|
34
35
|
## Setup
|
|
35
36
|
|
|
36
|
-
Define tables once, then create a database
|
|
37
|
+
Define tables once, then create a database for your SQL dialect.
|
|
37
38
|
|
|
38
39
|
```ts
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'
|
|
40
|
+
import { column as c, hasMany, query, table } from 'remix/data-table'
|
|
41
|
+
import { createPostgresDatabase } from 'remix/data-table/postgres'
|
|
42
42
|
|
|
43
43
|
let users = table({
|
|
44
44
|
name: 'users',
|
|
@@ -63,8 +63,7 @@ let orders = table({
|
|
|
63
63
|
|
|
64
64
|
let userOrders = hasMany(users, orders)
|
|
65
65
|
|
|
66
|
-
let
|
|
67
|
-
let db = createDatabase(createPostgresDatabaseAdapter(pool))
|
|
66
|
+
let db = createPostgresDatabase({ connectionString: process.env.DATABASE_URL })
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
## Query Objects
|
|
@@ -201,7 +200,7 @@ let createManyResult = await db.createMany(orders, [
|
|
|
201
200
|
{ id: 'o_102', user_id: 'u_003', status: 'pending', total: 48.5, created_at: Date.now() },
|
|
202
201
|
])
|
|
203
202
|
|
|
204
|
-
// Return inserted rows (requires
|
|
203
|
+
// Return inserted rows (requires database RETURNING support)
|
|
205
204
|
let insertedRows = await db.createMany(
|
|
206
205
|
orders,
|
|
207
206
|
[{ id: 'o_103', user_id: 'u_003', status: 'pending', total: 12, created_at: Date.now() }],
|
|
@@ -249,8 +248,7 @@ Return behavior:
|
|
|
249
248
|
|
|
250
249
|
### Validation and Lifecycle
|
|
251
250
|
|
|
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.
|
|
251
|
+
Validation is optional and table-scoped. Define `validate(context)` to validate/coerce write payloads, and add lifecycle callbacks when you need custom read/write/delete behavior.
|
|
254
252
|
|
|
255
253
|
```ts
|
|
256
254
|
import { column as c, fail, table } from 'remix/data-table'
|
|
@@ -344,11 +342,7 @@ await db.transaction(async (tx) => {
|
|
|
344
342
|
|
|
345
343
|
## Migrations
|
|
346
344
|
|
|
347
|
-
`data-table`
|
|
348
|
-
Migrations are adapter-driven: adapters execute SQL for their dialect/runtime, and SQL compilation
|
|
349
|
-
is handled by adapter-owned compilers (with optional shared pure helpers from `data-table`).
|
|
350
|
-
For adapter authors (including third-party adapters), shared SQL helper utilities are available at
|
|
351
|
-
`remix/data-table/sql-helpers`.
|
|
345
|
+
`data-table` ships a SQL-first migration system under `remix/data-table/migrations`. Each migration is a directory containing hand-written `up.sql` and (optionally) `down.sql`. The runner journals applied migrations, detects checksum drift and missing applied migrations, and wraps each migration in a transaction when the database supports transactional DDL.
|
|
352
346
|
|
|
353
347
|
### Example Setup
|
|
354
348
|
|
|
@@ -356,159 +350,187 @@ For adapter authors (including third-party adapters), shared SQL helper utilitie
|
|
|
356
350
|
app/
|
|
357
351
|
db/
|
|
358
352
|
migrations/
|
|
359
|
-
20260228090000_create_users
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
20260228090000_create_users/
|
|
354
|
+
up.sql
|
|
355
|
+
down.sql
|
|
356
|
+
20260301113000_add_user_status/
|
|
357
|
+
up.sql
|
|
358
|
+
down.sql
|
|
359
|
+
db.ts
|
|
362
360
|
```
|
|
363
361
|
|
|
364
|
-
- Keep migration
|
|
365
|
-
-
|
|
366
|
-
-
|
|
362
|
+
- Keep migration directories in one parent directory (for example `app/db/migrations`).
|
|
363
|
+
- Each directory is named `YYYYMMDDHHmmss_<slug>`.
|
|
364
|
+
- `up.sql` is required. `down.sql` is optional (omit for irreversible migrations).
|
|
365
|
+
- Scripts may contain multiple statements. `id` and `name` are inferred from the directory name.
|
|
367
366
|
|
|
368
367
|
### Migration File Example
|
|
369
368
|
|
|
370
|
-
|
|
371
|
-
import { column as c, table } from 'remix/data-table'
|
|
372
|
-
import { createMigration } from 'remix/data-table/migrations'
|
|
369
|
+
`20260228090000_create_users/up.sql`:
|
|
373
370
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
},
|
|
381
|
-
})
|
|
371
|
+
```sql
|
|
372
|
+
create table users (
|
|
373
|
+
id serial primary key,
|
|
374
|
+
email varchar(255) not null unique,
|
|
375
|
+
created_at timestamptz not null default now()
|
|
376
|
+
);
|
|
382
377
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
await schema.createTable(users)
|
|
386
|
-
await schema.createIndex(users, 'email', { unique: true })
|
|
378
|
+
create unique index users_email_idx on users (email);
|
|
379
|
+
```
|
|
387
380
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
await schema.dropTable(users, { ifExists: true })
|
|
394
|
-
},
|
|
395
|
-
})
|
|
381
|
+
`20260228090000_create_users/down.sql`:
|
|
382
|
+
|
|
383
|
+
```sql
|
|
384
|
+
drop index if exists users_email_idx;
|
|
385
|
+
drop table if exists users;
|
|
396
386
|
```
|
|
397
387
|
|
|
398
|
-
###
|
|
388
|
+
### Multi-Statement Driver Configuration
|
|
399
389
|
|
|
400
|
-
|
|
390
|
+
The runner sends each migration to the database as a single multi-statement script. Make sure the underlying driver accepts multiple statements:
|
|
401
391
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'
|
|
406
|
-
import { createMigrationRunner } from 'remix/data-table/migrations'
|
|
407
|
-
import { loadMigrations } from 'remix/data-table/migrations/node'
|
|
392
|
+
- `better-sqlite3`: works out of the box (`db.exec`).
|
|
393
|
+
- `pg`: works out of the box when no parameter array is passed.
|
|
394
|
+
- `mysql2`: requires `multipleStatements: true` on the connection/pool.
|
|
408
395
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
let to = process.argv[3]
|
|
396
|
+
```ts
|
|
397
|
+
import { createMysqlDatabase } from 'remix/data-table/mysql'
|
|
412
398
|
|
|
413
|
-
let
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
399
|
+
let db = createMysqlDatabase({
|
|
400
|
+
uri: process.env.DATABASE_URL,
|
|
401
|
+
multipleStatements: true,
|
|
402
|
+
})
|
|
403
|
+
```
|
|
417
404
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
405
|
+
### Database Command Configuration
|
|
406
|
+
|
|
407
|
+
Configure `remix db` statically in `remix.json`. Connection secrets are read from the named
|
|
408
|
+
environment variable at command runtime, and paths are resolved relative to `remix.json`:
|
|
409
|
+
|
|
410
|
+
```jsonc
|
|
411
|
+
{
|
|
412
|
+
"$schema": "https://remix.run/schemas/remix.json",
|
|
413
|
+
"db": {
|
|
414
|
+
"adapter": {
|
|
415
|
+
"type": "postgres",
|
|
416
|
+
"connectionString": { "env": "DATABASE_URL" },
|
|
417
|
+
},
|
|
418
|
+
"migrations": {
|
|
419
|
+
"directory": "./db/migrations",
|
|
420
|
+
"journalTable": "app_migrations",
|
|
421
|
+
},
|
|
422
|
+
"seed": "./db/seed.sql",
|
|
423
|
+
},
|
|
426
424
|
}
|
|
427
425
|
```
|
|
428
426
|
|
|
429
|
-
|
|
427
|
+
Application runtime setup remains application-owned. It does not need to expose any special
|
|
428
|
+
exports for the CLI.
|
|
430
429
|
|
|
431
|
-
|
|
432
|
-
let runner = createMigrationRunner(adapter, migrations, {
|
|
433
|
-
journalTable: 'app_migrations',
|
|
434
|
-
})
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
Run it with your runtime, for example:
|
|
430
|
+
Run lifecycle commands through the Remix CLI:
|
|
438
431
|
|
|
439
432
|
```sh
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
433
|
+
remix db status
|
|
434
|
+
remix db migrate
|
|
435
|
+
remix db migrate --to 20260301113000_add_user_status
|
|
436
|
+
remix db seed
|
|
437
|
+
remix db reset
|
|
438
|
+
remix db wipe
|
|
444
439
|
```
|
|
445
440
|
|
|
446
|
-
|
|
441
|
+
`--to` accepts a bare migration id (`20260301113000`) or the full directory name (`20260301113000_add_user_status`).
|
|
442
|
+
|
|
443
|
+
`remix db status` reports applied migrations whose files are no longer present as `missing`. If the journal table does not exist, it reports every migration as pending without creating the table. Forward migration runs stop before executing SQL when an applied journal entry is missing from the current migration set. Rollbacks skip those orphaned journal entries so migrations that are still present can be reverted.
|
|
444
|
+
|
|
445
|
+
`wipe` and `reset` are destructive. They require a config-backed database so it can close, recreate, and reconnect to the configured database.
|
|
446
|
+
|
|
447
|
+
### Programmatic Migrations
|
|
448
|
+
|
|
449
|
+
Load migrations and pass the resolved collection directly to the database:
|
|
447
450
|
|
|
448
451
|
```ts
|
|
449
|
-
|
|
450
|
-
await runner.down({ step: 1 })
|
|
451
|
-
```
|
|
452
|
+
import { loadMigrations } from 'remix/data-table/migrations/node'
|
|
452
453
|
|
|
453
|
-
|
|
454
|
+
let migrations = await loadMigrations('./db/migrations')
|
|
455
|
+
await db.migrate(migrations)
|
|
456
|
+
```
|
|
454
457
|
|
|
455
|
-
|
|
458
|
+
`Database.migrate()` supports forward and backward directions, a target or step bound, dry runs,
|
|
459
|
+
and a custom journal table:
|
|
456
460
|
|
|
457
461
|
```ts
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
await db.migrate(migrations)
|
|
463
|
+
await db.migrate(migrations, { to: '20260301113000_add_user_status' })
|
|
464
|
+
await db.migrate(migrations, { step: 1 })
|
|
465
|
+
await db.migrate(migrations, { direction: 'down' })
|
|
466
|
+
await db.migrate(migrations, { direction: 'down', to: '20260301113000' })
|
|
467
|
+
await db.migrate(migrations, { direction: 'down', step: 1 })
|
|
468
|
+
await db.migrate(migrations, { journalTable: 'app_migrations' })
|
|
469
|
+
|
|
470
|
+
let plan = await db.migrate(migrations, { dryRun: true })
|
|
471
|
+
for (let script of plan.sql) console.log(script)
|
|
460
472
|
```
|
|
461
473
|
|
|
462
|
-
|
|
463
|
-
query-builder data operations, and `schema.hasTable(...)` / `schema.hasColumn(...)` all run in the same
|
|
464
|
-
adapter transaction context.
|
|
474
|
+
`to` and `step` are mutually exclusive. Omit `journalTable` to use `data_table_migrations`.
|
|
465
475
|
|
|
466
|
-
|
|
476
|
+
Database drivers with migration locking run the complete migration and journal lifecycle through the
|
|
477
|
+
connection that owns the lock. This keeps advisory locks correctly paired when the driver uses a
|
|
478
|
+
connection pool, including pools configured with a single connection.
|
|
479
|
+
|
|
480
|
+
Read status separately, or rebuild a database with migrations and an optional seed. A seed is a
|
|
481
|
+
function that receives the database; `loadSeed()` builds one from a SQL file:
|
|
467
482
|
|
|
468
483
|
```ts
|
|
469
|
-
import {
|
|
484
|
+
import { loadSeed } from 'remix/data-table/migrations/node'
|
|
485
|
+
|
|
486
|
+
let seed = await loadSeed('./db/seed.sql')
|
|
470
487
|
|
|
471
|
-
await
|
|
488
|
+
let status = await db.migrationStatus(migrations, { journalTable: 'app_migrations' })
|
|
489
|
+
await db.reset({ migrations })
|
|
490
|
+
await db.reset({ migrations, seed })
|
|
491
|
+
await db.reset({ migrations, seed, journalTable: 'app_migrations' })
|
|
472
492
|
```
|
|
473
493
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
accept either a string (`'app.users'`) or a `table(...)` object.
|
|
494
|
+
When a lifecycle command is the last thing a process does, close database-owned connections so
|
|
495
|
+
the process can exit:
|
|
477
496
|
|
|
478
|
-
|
|
479
|
-
|
|
497
|
+
```ts
|
|
498
|
+
await db.close()
|
|
499
|
+
```
|
|
480
500
|
|
|
481
|
-
|
|
501
|
+
### Transaction Modes
|
|
482
502
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
})
|
|
503
|
+
By default each migration is wrapped in a transaction when the database supports transactional DDL. Override per migration with a directive on the first non-blank line of `up.sql`:
|
|
504
|
+
|
|
505
|
+
```sql
|
|
506
|
+
-- data-table/transaction: none
|
|
507
|
+
create index concurrently users_email_active_idx on users (email) where status = 'active';
|
|
489
508
|
```
|
|
490
509
|
|
|
491
|
-
|
|
492
|
-
|
|
510
|
+
Supported modes:
|
|
511
|
+
|
|
512
|
+
- `auto` (default): wrap when the database supports transactional DDL.
|
|
513
|
+
- `required`: wrap; the runner throws if the database cannot support it.
|
|
514
|
+
- `none`: never wrap. Use this for statements like postgres `CREATE INDEX CONCURRENTLY` that cannot run inside a transaction.
|
|
493
515
|
|
|
494
|
-
|
|
516
|
+
You can also set `transaction` directly on a `MigrationDescriptor` when registering migrations programmatically.
|
|
495
517
|
|
|
496
|
-
|
|
497
|
-
- Verify migration ordering and target/step selection
|
|
498
|
-
- Audit dialect-specific SQL differences across adapters
|
|
518
|
+
### Programmatic Registration
|
|
499
519
|
|
|
500
|
-
For non-filesystem runtimes, register migrations
|
|
520
|
+
For non-filesystem runtimes, register migrations directly:
|
|
501
521
|
|
|
502
522
|
```ts
|
|
503
|
-
import { createMigrationRegistry
|
|
504
|
-
import createUsers from './db/migrations/20260228090000_create_users.ts'
|
|
523
|
+
import { createMigrationRegistry } from 'remix/data-table/migrations'
|
|
505
524
|
|
|
506
525
|
let registry = createMigrationRegistry()
|
|
507
|
-
registry.register({
|
|
526
|
+
registry.register({
|
|
527
|
+
id: '20260228090000',
|
|
528
|
+
name: 'create_users',
|
|
529
|
+
up: 'create table users (id serial primary key, email text not null);',
|
|
530
|
+
down: 'drop table users;',
|
|
531
|
+
})
|
|
508
532
|
|
|
509
|
-
|
|
510
|
-
let runner = createMigrationRunner(adapter, registry)
|
|
511
|
-
await runner.up()
|
|
533
|
+
await db.migrate(registry)
|
|
512
534
|
```
|
|
513
535
|
|
|
514
536
|
## Raw SQL Escape Hatch
|
|
@@ -536,14 +558,120 @@ let result = await db.exec(sql`
|
|
|
536
558
|
`)
|
|
537
559
|
```
|
|
538
560
|
|
|
539
|
-
`sql` keeps values parameterized
|
|
561
|
+
`sql` keeps values parameterized for the database dialect, so you can avoid manual string concatenation.
|
|
562
|
+
|
|
563
|
+
## Custom Database Drivers
|
|
564
|
+
|
|
565
|
+
Applications normally use one of the concrete SQLite, PostgreSQL, or MySQL factories. Integration
|
|
566
|
+
packages can add another dialect by implementing `DatabaseDriver` and extending `Database`. This
|
|
567
|
+
complete skeleton assumes the underlying client exposes the operations needed by the driver:
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
import {
|
|
571
|
+
Database,
|
|
572
|
+
type DatabaseDriver,
|
|
573
|
+
type DataManipulationRequest,
|
|
574
|
+
type DataManipulationResult,
|
|
575
|
+
type DatabaseOptions,
|
|
576
|
+
type TableRef,
|
|
577
|
+
type TransactionOptions,
|
|
578
|
+
type TransactionToken,
|
|
579
|
+
} from 'remix/data-table'
|
|
580
|
+
import type { AcmeClient } from 'acme-database'
|
|
581
|
+
|
|
582
|
+
class AcmeDriver implements DatabaseDriver<'acme'> {
|
|
583
|
+
readonly dialect = 'acme'
|
|
584
|
+
readonly capabilities = {
|
|
585
|
+
returning: true,
|
|
586
|
+
savepoints: true,
|
|
587
|
+
upsert: true,
|
|
588
|
+
transactionalDdl: true,
|
|
589
|
+
migrationLock: false,
|
|
590
|
+
} as const
|
|
591
|
+
|
|
592
|
+
#client: AcmeClient
|
|
593
|
+
|
|
594
|
+
constructor(client: AcmeClient) {
|
|
595
|
+
this.#client = client
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
|
|
599
|
+
return this.#client.execute(request)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
executeScript(sql: string, transaction?: TransactionToken): Promise<void> {
|
|
603
|
+
return this.#client.executeScript(sql, transaction)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
|
|
607
|
+
return this.#client.beginTransaction(options)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
commitTransaction(transaction: TransactionToken): Promise<void> {
|
|
611
|
+
return this.#client.commitTransaction(transaction)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
rollbackTransaction(transaction: TransactionToken): Promise<void> {
|
|
615
|
+
return this.#client.rollbackTransaction(transaction)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
|
|
619
|
+
return this.#client.hasTable(table, transaction)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
hasColumn(table: TableRef, column: string, transaction?: TransactionToken): Promise<boolean> {
|
|
623
|
+
return this.#client.hasColumn(table, column, transaction)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
createSavepoint(transaction: TransactionToken, name: string): Promise<void> {
|
|
627
|
+
return this.#client.createSavepoint(transaction, name)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
rollbackToSavepoint(transaction: TransactionToken, name: string): Promise<void> {
|
|
631
|
+
return this.#client.rollbackToSavepoint(transaction, name)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
releaseSavepoint(transaction: TransactionToken, name: string): Promise<void> {
|
|
635
|
+
return this.#client.releaseSavepoint(transaction, name)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
wipe(): Promise<void> {
|
|
639
|
+
return this.#client.wipe()
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
close(): void | Promise<void> {
|
|
643
|
+
return this.#client.close()
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export class AcmeDatabase extends Database<'acme'> {
|
|
648
|
+
constructor(client: AcmeClient, options?: DatabaseOptions) {
|
|
649
|
+
super(new AcmeDriver(client), options)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
`Database` supplies queries, CRUD helpers, relations, transactions, and migrations. The private
|
|
655
|
+
driver owns SQL execution and connection lifecycle. A driver provides:
|
|
656
|
+
|
|
657
|
+
- `dialect` and an immutable `capabilities` object
|
|
658
|
+
- `execute()` and `executeScript()`
|
|
659
|
+
- `hasTable()`, `hasColumn()`, `wipe()`, and idempotent `close()`
|
|
660
|
+
- transaction and savepoint lifecycle methods using opaque `TransactionToken` values
|
|
661
|
+
|
|
662
|
+
Drivers whose capabilities report `migrationLock: true` also implement `withMigrationLock()` and
|
|
663
|
+
run its callback with a `DatabaseDriver` bound to the connection that owns the lock.
|
|
664
|
+
|
|
665
|
+
`Database`, `DatabaseDriver`, and the supporting driver protocol types are all exported directly
|
|
666
|
+
from `remix/data-table`. Transaction callbacks receive a transaction-scoped `Database`, so custom
|
|
667
|
+
subclass methods are intentionally unavailable inside the callback.
|
|
540
668
|
|
|
541
669
|
## Related Packages
|
|
542
670
|
|
|
543
671
|
- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Optional schema parsing you can use inside table-level `validate(...)` hooks
|
|
544
|
-
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL
|
|
545
|
-
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL
|
|
546
|
-
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite
|
|
672
|
+
- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL database integration
|
|
673
|
+
- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL database integration
|
|
674
|
+
- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite database integration
|
|
547
675
|
|
|
548
676
|
## License
|
|
549
677
|
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Database } from './lib/database.ts';
|
|
2
|
+
import type { Migrations, Seed } from './lib/migrations.ts';
|
|
3
|
+
interface DatabaseCommandOptions {
|
|
4
|
+
/** Database instance used by the command. */
|
|
5
|
+
db: Database;
|
|
6
|
+
}
|
|
7
|
+
/** Structured invocation options accepted by {@link runRemixDb}. */
|
|
8
|
+
export type RunRemixDbOptions = (DatabaseCommandOptions & {
|
|
9
|
+
/** Applies pending migrations. */
|
|
10
|
+
command: 'migrate';
|
|
11
|
+
/** Migrations to apply. */
|
|
12
|
+
migrations: Migrations;
|
|
13
|
+
/**
|
|
14
|
+
* Stops after applying this migration. Accepts a bare migration id or
|
|
15
|
+
* the full `id_name` directory form.
|
|
16
|
+
*/
|
|
17
|
+
to?: string;
|
|
18
|
+
/** Migration journal table. */
|
|
19
|
+
journalTable?: string;
|
|
20
|
+
}) | (DatabaseCommandOptions & {
|
|
21
|
+
/** Wipes, migrates, and optionally seeds the database. */
|
|
22
|
+
command: 'reset';
|
|
23
|
+
/** Migrations to apply after wiping the database. */
|
|
24
|
+
migrations: Migrations;
|
|
25
|
+
/** Initializes database data after migrations finish. */
|
|
26
|
+
seed?: Seed;
|
|
27
|
+
/** Migration journal table. */
|
|
28
|
+
journalTable?: string;
|
|
29
|
+
}) | (DatabaseCommandOptions & {
|
|
30
|
+
/** Runs the application's seed function. */
|
|
31
|
+
command: 'seed';
|
|
32
|
+
/** Initializes application database data. */
|
|
33
|
+
seed: Seed;
|
|
34
|
+
}) | (DatabaseCommandOptions & {
|
|
35
|
+
/** Reports the status of known migrations. */
|
|
36
|
+
command: 'status';
|
|
37
|
+
/** Migrations to inspect. */
|
|
38
|
+
migrations: Migrations;
|
|
39
|
+
/** Migration journal table. */
|
|
40
|
+
journalTable?: string;
|
|
41
|
+
}) | (DatabaseCommandOptions & {
|
|
42
|
+
/** Destructively recreates the configured database. */
|
|
43
|
+
command: 'wipe';
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Runs a data-table database command from structured invocation options.
|
|
47
|
+
*
|
|
48
|
+
* @param options Database command and application database values.
|
|
49
|
+
* @returns The exit code the host CLI should use. Always resolves `0`;
|
|
50
|
+
* command failures throw.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runRemixDb(options: RunRemixDbOptions): Promise<number>;
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAE3D,UAAU,sBAAsB;IAC9B,6CAA6C;IAC7C,EAAE,EAAE,QAAQ,CAAA;CACb;AAED,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GACzB,CAAC,sBAAsB,GAAG;IACxB,kCAAkC;IAClC,OAAO,EAAE,SAAS,CAAA;IAClB,2BAA2B;IAC3B,UAAU,EAAE,UAAU,CAAA;IACtB;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAC,GACF,CAAC,sBAAsB,GAAG;IACxB,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAA;IAChB,qDAAqD;IACrD,UAAU,EAAE,UAAU,CAAA;IACtB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAC,GACF,CAAC,sBAAsB,GAAG;IACxB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,IAAI,EAAE,IAAI,CAAA;CACX,CAAC,GACF,CAAC,sBAAsB,GAAG;IACxB,8CAA8C;IAC9C,OAAO,EAAE,QAAQ,CAAA;IACjB,6BAA6B;IAC7B,UAAU,EAAE,UAAU,CAAA;IACtB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAC,GACF,CAAC,sBAAsB,GAAG;IACxB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEN;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4D5E"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a data-table database command from structured invocation options.
|
|
3
|
+
*
|
|
4
|
+
* @param options Database command and application database values.
|
|
5
|
+
* @returns The exit code the host CLI should use. Always resolves `0`;
|
|
6
|
+
* command failures throw.
|
|
7
|
+
*/
|
|
8
|
+
export async function runRemixDb(options) {
|
|
9
|
+
if (options.command === 'migrate') {
|
|
10
|
+
let migrateOptions = options.to === undefined
|
|
11
|
+
? { journalTable: options.journalTable }
|
|
12
|
+
: { to: options.to, journalTable: options.journalTable };
|
|
13
|
+
let result = await options.db.migrate(options.migrations, migrateOptions);
|
|
14
|
+
if (result.applied.length === 0) {
|
|
15
|
+
console.log('no pending migrations');
|
|
16
|
+
}
|
|
17
|
+
for (let entry of result.applied) {
|
|
18
|
+
console.log('applied ' + entry.id + '_' + entry.name);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (options.command === 'reset') {
|
|
23
|
+
await options.db.reset({
|
|
24
|
+
migrations: options.migrations,
|
|
25
|
+
seed: options.seed,
|
|
26
|
+
journalTable: options.journalTable,
|
|
27
|
+
});
|
|
28
|
+
console.log('database reset');
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (options.command === 'seed') {
|
|
32
|
+
await options.seed(options.db);
|
|
33
|
+
console.log('database seeded');
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (options.command === 'status') {
|
|
37
|
+
let entries = await options.db.migrationStatus(options.migrations, {
|
|
38
|
+
journalTable: options.journalTable,
|
|
39
|
+
});
|
|
40
|
+
for (let entry of entries) {
|
|
41
|
+
console.log(entry.id + ' ' + entry.name + ' ' + entry.status);
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
if (options.command === 'wipe') {
|
|
46
|
+
await options.db.wipe();
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
// Guard against unchecked command strings from plain-JS callers so an
|
|
50
|
+
// unknown command can never fall through to a destructive operation.
|
|
51
|
+
let command = options.command;
|
|
52
|
+
throw new Error('Unknown database command: ' + command);
|
|
53
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export {
|
|
1
|
+
export type { DatabaseCapabilities, DatabaseDriver, DataManipulationOperation, DataManipulationRequest, ColumnCheck, ColumnComputed, ColumnDefault, ColumnDefinition, ColumnTypeName, DataManipulationResult, ForeignKeyAction, IdentityOptions, TableRef, TransactionOptions, TransactionToken, } from './lib/driver.ts';
|
|
2
|
+
export { DataTableDatabaseError, DataTableConstraintError, DataTableError, DataTableQueryError, DataTableValidationError, } from './lib/errors.ts';
|
|
3
3
|
export type { AnyRelation, AnyColumn, AnyTable, BelongsToOptions, ColumnReference, ColumnReferenceForQualifiedName, HasManyOptions, HasManyThroughOptions, HasOneOptions, KeySelector, OrderByClause, OrderDirection, PrimaryKeyInput, Relation, RelationCardinality, RelationKind, RelationMapForTable, Table, TableAfterDelete, TableAfterDeleteContext, TableAfterRead, TableAfterReadContext, TableAfterReadResult, TableAfterWrite, TableAfterWriteContext, TableBeforeDelete, TableBeforeDeleteContext, TableBeforeDeleteResult, TableBeforeWrite, TableBeforeWriteContext, TableBeforeWriteResult, TableColumnInput, TableColumnName, TableColumns, TableLifecycleOperation, TableName, TablePrimaryKey, TableReference, TableRow, TableRowWith, TableColumnsDefinition, TableValidate, TableValidationContext, TableWriteOperation, TableValidationOperation, TableValidationResult, TimestampConfig, TimestampOptions, ValidationFailure, ValidationIssue, } from './lib/table.ts';
|
|
4
4
|
export { belongsTo, columnMetadataKey, fail, getTableColumns, getTableColumnDefinitions, getTableBeforeDelete, getTableBeforeWrite, getTableAfterDelete, getTableAfterRead, getTableAfterWrite, getTableName, getTablePrimaryKey, getTableReference, getTableTimestamps, getTableValidator, hasMany, hasManyThrough, hasOne, table, tableMetadataKey, timestamps, } from './lib/table.ts';
|
|
5
5
|
export type { ColumnNamespace } from './lib/column.ts';
|
|
@@ -8,8 +8,9 @@ export type { Predicate, WhereInput, WhereObject } from './lib/operators.ts';
|
|
|
8
8
|
export { and, between, eq, gt, gte, ilike, inList, isNull, like, lt, lte, ne, notInList, notNull, or, } from './lib/operators.ts';
|
|
9
9
|
export type { SqlStatement } from './lib/sql.ts';
|
|
10
10
|
export { rawSql, sql } from './lib/sql.ts';
|
|
11
|
-
export type { CountOptions, CreateManyResultOptions, CreateManyRowsOptions, CreateResultOptions, CreateRowOptions, DeleteManyOptions, FindManyOptions, FindOneOptions, OrderByInput, OrderByTuple, QueryColumnTypesForTable, QueryForTable, QueryTableInput, SingleTableColumn, SingleTableWhere, UpdateManyOptions, UpdateOptions, WriteResult, WriteRowResult, WriteRowsResult, } from './lib/database.ts';
|
|
12
|
-
export {
|
|
11
|
+
export type { CountOptions, CreateManyResultOptions, CreateManyRowsOptions, CreateResultOptions, CreateRowOptions, DeleteManyOptions, DatabaseOptions, FindManyOptions, FindOneOptions, OrderByInput, OrderByTuple, QueryColumnTypesForTable, QueryForTable, QueryTableInput, SingleTableColumn, SingleTableWhere, UpdateManyOptions, UpdateOptions, WriteResult, WriteRowResult, WriteRowsResult, } from './lib/database.ts';
|
|
12
|
+
export { Database } from './lib/database.ts';
|
|
13
|
+
export type { DatabaseMigrateOptions, DatabaseMigrationStatusOptions, DatabaseResetOptions, MigrateResult, MigrationDescriptor, MigrationRegistry, Migrations, MigrationStatus, MigrationStatusEntry, Seed, } from './lib/migrations.ts';
|
|
13
14
|
export type { AnyQuery } from './lib/query.ts';
|
|
14
15
|
export { Query, query } from './lib/query.ts';
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|