@pitlane/data-table-d1 0.1.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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/LICENSE.remix +21 -0
- package/README.md +144 -0
- package/dist/index.d.mts +236 -0
- package/dist/index.mjs +495 -0
- package/dist/migrations.d.mts +55 -0
- package/dist/migrations.mjs +75 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @pitlane/data-table-d1
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `createD1Database(binding, options?)` wraps a Cloudflare D1 binding in a
|
|
8
|
+
`Database` from `remix/data-table`. `D1Database` is the subclass it returns
|
|
9
|
+
and `D1DatabaseDriver` the bare `DatabaseDriver<"sqlite">`, matching the shape
|
|
10
|
+
of `SqliteDatabase` and `PostgresDatabase`.
|
|
11
|
+
- Exists because `@remix-run/data-table-sqlite` drives a synchronous client —
|
|
12
|
+
`prepare(sql).all()` returns rows rather than a promise, as `better-sqlite3`
|
|
13
|
+
and `node:sqlite` do — and D1 is an awaited RPC binding. No adapter bridges
|
|
14
|
+
that, so a D1 app could not use the SQLite driver at all. The SQL is still
|
|
15
|
+
SQLite's, so this pairs that compiler with an async driver.
|
|
16
|
+
- Transactions throw by default, naming `d1.batch()` and the opt-in below. D1
|
|
17
|
+
rejects `BEGIN`, `COMMIT`, and `SAVEPOINT` at the SQL layer, and `batch()`
|
|
18
|
+
wants every statement up front, which cannot express the interleaved
|
|
19
|
+
begin/execute/commit a `Database` transaction drives. Capabilities report
|
|
20
|
+
`savepoints: false` and `transactionalDdl: false` rather than failing
|
|
21
|
+
mid-write.
|
|
22
|
+
- `transactions: "unsafe-nonatomic"` opts out of that refusal, for callers
|
|
23
|
+
shared with a backend that does have transactions where running without
|
|
24
|
+
atomicity beats not running. `transaction()` then runs the callback with each
|
|
25
|
+
statement committing on its own, so a failure part-way leaves the earlier
|
|
26
|
+
writes persisted — asserted against real D1, not just described. Rollback
|
|
27
|
+
stays silent so the callback's own error surfaces instead of an
|
|
28
|
+
`AggregateError` about an impossible rollback, and nesting still fails in
|
|
29
|
+
both modes because `savepoints: false` stops it upstream of the driver.
|
|
30
|
+
- `db.batch(statements)` runs statements atomically through D1's `batch()`,
|
|
31
|
+
which is its one atomic primitive and the reason it cannot back
|
|
32
|
+
`transaction()`. A failing statement rolls the whole batch back, asserted
|
|
33
|
+
against real D1. Inputs are `SqlStatement`s from `remix/data-table`'s `sql`
|
|
34
|
+
tag rather than query-builder calls, because `data-table` exposes no way to
|
|
35
|
+
build an operation without running it — `create` and `updateMany` execute on
|
|
36
|
+
call and `Query` has no `toSql()`. The point is that reaching for atomicity
|
|
37
|
+
no longer means reaching for the raw binding.
|
|
38
|
+
- `wipe()` drops the application's tables, leaving D1's `_cf_*` and SQLite's
|
|
39
|
+
`sqlite_*` bookkeeping in place. The pragma that permits the drops travels in
|
|
40
|
+
the same `batch()` as the drops, because it is per-session and D1 gives each
|
|
41
|
+
statement its own session.
|
|
42
|
+
- `generateD1Migrations()`, from the `@pitlane/data-table-d1/migrations` entry
|
|
43
|
+
point, compiles `data-table` migrations into the flat `.sql` files Wrangler's
|
|
44
|
+
D1 migration runner reads. Production migrations then go through Wrangler's
|
|
45
|
+
own workflow, which is what four apps in the wild had each hand-rolled: two
|
|
46
|
+
byte-identical copies of one generator, and two more that drifted to 74 and
|
|
47
|
+
101 lines from a common ancestor. SQL is copied verbatim rather than split
|
|
48
|
+
into statements, so a semicolon inside a trigger body survives; the output
|
|
49
|
+
directory is pruned to match the source; and files that are not generated
|
|
50
|
+
artifacts are left alone. Node-only, and a separate entry point so it stays
|
|
51
|
+
out of Worker bundles.
|
|
52
|
+
- Raw statements always come back with a rows array. The SQLite driver asks a
|
|
53
|
+
prepared statement whether it returns columns; D1 exposes no equivalent, and
|
|
54
|
+
its `all()` carries both `results` and `meta` regardless.
|
|
55
|
+
- `onStatement` reports what each statement cost —
|
|
56
|
+
`{ kind, table, rowsRead, rowsWritten, durationMs }` — off the `meta` D1
|
|
57
|
+
already returns. D1 bills on rows read and written and reports analytics per
|
|
58
|
+
database, so this is the only per-query attribution available, and it costs
|
|
59
|
+
no extra statement. Observer throws are swallowed, statements that throw are
|
|
60
|
+
not reported, and figures D1 omits come through as `0` rather than estimated.
|
|
61
|
+
The idea is [`@pkg/data-table-d1`](https://github.com/sergiodxa/monorepo/tree/main/packages/data-table-d1)'s.
|
|
62
|
+
- The D1 API is declared structurally, so the package pulls in no Cloudflare
|
|
63
|
+
types and no ambient globals.
|
|
64
|
+
- `src/sql-compiler.ts` is vendored verbatim from
|
|
65
|
+
`@remix-run/data-table-sqlite@0.6.0` (MIT, Copyright (c) 2025 Shopify Inc.),
|
|
66
|
+
with only its import specifiers repointed at the public `remix/*` subpaths.
|
|
67
|
+
Upstream keeps `compileSqliteOperation` internal and publishes no D1 dialect;
|
|
68
|
+
the file goes away if either changes.
|
|
69
|
+
- Tested against `remix@3.0.0-beta.10`, with the query, write, count, schema and
|
|
70
|
+
wipe paths exercised inside real workerd through Miniflare.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Malstrom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LICENSE.remix
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shopify Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @pitlane/data-table-d1
|
|
2
|
+
|
|
3
|
+
A [Cloudflare D1](https://developers.cloudflare.com/d1/) driver for [Remix 3](https://remix.run)'s `data-table`.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createD1Database } from "@pitlane/data-table-d1";
|
|
7
|
+
import { env } from "cloudflare:workers";
|
|
8
|
+
|
|
9
|
+
let db = createD1Database(env.DB);
|
|
10
|
+
|
|
11
|
+
let post = await db.create(Post, { title: "Hello" }, { returnRow: true });
|
|
12
|
+
let recent = await db.query(Post).orderBy({ createdAt: "desc" }).limit(10).all();
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That is a `Database` from `remix/data-table`, so every query, persistence, and migration method behaves exactly as it does on SQLite or Postgres.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install @pitlane/data-table-d1
|
|
21
|
+
# or
|
|
22
|
+
vp add @pitlane/data-table-d1
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires `remix@^3.0.0-beta.10` as a peer. Nothing else: the D1 API is described structurally, so you do not need `@cloudflare/workers-types` unless your own code already does.
|
|
26
|
+
|
|
27
|
+
## Why this package exists
|
|
28
|
+
|
|
29
|
+
D1 is SQLite, so the SQL is SQLite's. The execution model is not.
|
|
30
|
+
|
|
31
|
+
`@remix-run/data-table-sqlite` drives a **synchronous** client — `prepare(sql).all()` returns rows, not a promise, because that is the shape `better-sqlite3` and `node:sqlite` have. D1 is an RPC binding: every call is awaited. No adapter closes that gap, so a D1 app cannot use the SQLite driver at all.
|
|
32
|
+
|
|
33
|
+
What it can reuse is the SQL. This package pairs the SQLite SQL compiler with a driver written against D1's async prepared-statement API.
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `createD1Database(binding, options?)`
|
|
38
|
+
|
|
39
|
+
Wraps a D1 binding in a `Database`. `binding` is `env.DB`; `options` takes everything `Database` takes, plus `onStatement`.
|
|
40
|
+
|
|
41
|
+
### `D1Database`
|
|
42
|
+
|
|
43
|
+
The `Database` subclass, if you would rather construct it yourself. Same shape as `SqliteDatabase` and `PostgresDatabase`.
|
|
44
|
+
|
|
45
|
+
### `D1DatabaseDriver`
|
|
46
|
+
|
|
47
|
+
The bare `DatabaseDriver<"sqlite">`, for handing to `Database` directly or wrapping.
|
|
48
|
+
|
|
49
|
+
### `D1Binding`
|
|
50
|
+
|
|
51
|
+
The slice of D1 this package uses: `prepare`, `batch`, `exec`. A real binding satisfies it, and so does a test double.
|
|
52
|
+
|
|
53
|
+
### `generateD1Migrations(options)`
|
|
54
|
+
|
|
55
|
+
Compiles `data-table` migrations into the flat `.sql` files Wrangler's D1 migration runner reads, so production migrations go through Wrangler's own workflow instead of a hand-rolled script:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { generateD1Migrations } from "@pitlane/data-table-d1/migrations";
|
|
59
|
+
|
|
60
|
+
await generateD1Migrations({ to: "db/d1-migrations" });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Reads `db/migrations` unless told otherwise, writes one `<id>_<name>.sql` per migration, and deletes generated files whose migration is gone. SQL is copied verbatim, so semicolons inside trigger bodies survive.
|
|
64
|
+
|
|
65
|
+
Node-only build tooling, which is why it is a separate entry point. It never enters a Worker bundle.
|
|
66
|
+
|
|
67
|
+
### `onStatement`
|
|
68
|
+
|
|
69
|
+
Called after each executed statement with what it cost:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
let usage = { rowsRead: 0, rowsWritten: 0 };
|
|
73
|
+
|
|
74
|
+
let db = createD1Database(env.DB, {
|
|
75
|
+
onStatement({ rowsRead, rowsWritten }) {
|
|
76
|
+
usage.rowsRead += rowsRead;
|
|
77
|
+
usage.rowsWritten += rowsWritten;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
D1 bills on rows read and written, and its analytics report per database rather than per query, so this is the only way to attribute cost to the query or the request that caused it. The figures ride on responses the driver already reads, so it costs no extra statement and no extra billable operation.
|
|
83
|
+
|
|
84
|
+
The report is `{ kind, table, rowsRead, rowsWritten, durationMs }`. It runs once per statement on the hot path, so keep it cheap. Anything it throws is swallowed rather than failing the statement it was measuring. A statement that throws is not reported, because D1 returns no metadata for one and a zeroed entry would read as free. Figures D1 omits come through as `0`, never estimated.
|
|
85
|
+
|
|
86
|
+
### Reuse the database
|
|
87
|
+
|
|
88
|
+
A binding is stable for the isolate, so build the database once rather than per request:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
let db: D1Database | null = null;
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
fetch(request, env) {
|
|
95
|
+
db ??= createD1Database(env.DB);
|
|
96
|
+
// …
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## What D1 cannot do
|
|
102
|
+
|
|
103
|
+
**Several writes that must commit together** use `db.batch()`, which is D1's one atomic primitive and the reason it cannot back `transaction()`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { sql } from "remix/data-table";
|
|
107
|
+
|
|
108
|
+
await db.batch([
|
|
109
|
+
sql`insert into post (title) values (${title})`,
|
|
110
|
+
sql`update counter set posts = posts + 1`,
|
|
111
|
+
]);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If any statement fails the whole batch rolls back. They are `SqlStatement`s rather than query-builder calls because `data-table` exposes no way to build an operation without running it; `sql` still parameterises the values, so the raw binding stays out of your application code.
|
|
115
|
+
|
|
116
|
+
**Transactions throw by default.** D1 rejects `BEGIN`, `COMMIT`, and `SAVEPOINT` at the SQL layer and offers `d1.batch()` instead, which takes every statement up front. That cannot express the interleaved begin/execute/commit a `Database` transaction drives, so the driver reports `savepoints: false` and `transactionalDdl: false` and throws a message pointing at `batch()`. Failing at the call beats failing halfway through a write that cannot be rolled back.
|
|
117
|
+
|
|
118
|
+
When the caller is shared with a backend that does have transactions, and running without atomicity beats not running at all, opt in:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
let db = createD1Database(env.DB, { transactions: "unsafe-nonatomic" });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`transaction()` then runs the callback and each statement commits on its own. **A failure part-way leaves the earlier writes persisted**, because there is nothing to roll back — that is the whole of what you are accepting, and the package has a test against real D1 asserting exactly that outcome. Rollback stays silent rather than throwing, so the callback's own error is what surfaces instead of an `AggregateError` about an impossible rollback. Nested transactions still fail in both modes, since `savepoints: false` makes `Database` reject them before the driver is reached.
|
|
125
|
+
|
|
126
|
+
**`wipe()` drops tables rather than deleting a file.** There is no file. D1's own `_cf_*` bookkeeping and SQLite's `sqlite_*` tables are left alone; dropping either breaks the binding.
|
|
127
|
+
|
|
128
|
+
Everything else — `returning`, upserts, bulk inserts, migrations, schema inspection — works.
|
|
129
|
+
|
|
130
|
+
## Prior art
|
|
131
|
+
|
|
132
|
+
[`@pkg/data-table-d1`](https://github.com/sergiodxa/monorepo/tree/main/packages/data-table-d1) by Sergio Xalambrí solves the same problem, and `onStatement` is its idea. It also takes the other side of the transaction question, running them non-atomically so shared code keeps working; this package makes that the opt-in above rather than the default, on the grounds that a silent loss of atomicity should be something you asked for. It reports `transactionalDdl: true`, where this one keeps it `false` in both modes, since a non-transaction cannot make DDL transactional.
|
|
133
|
+
|
|
134
|
+
Its sibling [`@pkg/data-table-sqlstorage`](https://github.com/sergiodxa/monorepo/tree/main/packages/data-table-sqlstorage) covers Durable Object SQLite, which is synchronous and does support real transactions.
|
|
135
|
+
|
|
136
|
+
## Provenance
|
|
137
|
+
|
|
138
|
+
`src/sql-compiler.ts` is vendored verbatim from [`@remix-run/data-table-sqlite@0.6.0`](https://www.npmjs.com/package/@remix-run/data-table-sqlite) (MIT, Copyright (c) 2025 Shopify Inc.; the licence is in `LICENSE.remix`). Only its two import specifiers changed, from the private `@remix-run/data-table*` package names to the public `remix/*` subpaths, so a future upstream revision diffs cleanly against it.
|
|
139
|
+
|
|
140
|
+
It is vendored because `@remix-run/data-table-sqlite` exports exactly `createSqliteDatabase` and `SqliteDatabase`. `compileSqliteOperation` is internal, reached by its own driver through a relative import, and there is no `@remix-run/data-table-d1`. If upstream exposes the compiler or ships a D1 dialect, this file goes away.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { DataManipulationRequest, DataManipulationResult, Database, DatabaseDriver, DatabaseOptions, SqlStatement, TableRef, TransactionOptions, TransactionToken } from "remix/data-table";
|
|
2
|
+
//#region src/d1.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The slice of Cloudflare's D1 API this driver uses.
|
|
5
|
+
*
|
|
6
|
+
* Declared structurally rather than imported from `@cloudflare/workers-types`,
|
|
7
|
+
* so the package adds no dependency and no ambient global types to a consumer
|
|
8
|
+
* that does not already have them. A real `D1Database` binding satisfies it;
|
|
9
|
+
* so does a test double.
|
|
10
|
+
*/
|
|
11
|
+
interface D1Binding {
|
|
12
|
+
prepare(query: string): D1PreparedStatement;
|
|
13
|
+
batch(statements: D1PreparedStatement[]): Promise<D1Result[]>;
|
|
14
|
+
exec(query: string): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
interface D1PreparedStatement {
|
|
17
|
+
bind(...values: unknown[]): D1PreparedStatement;
|
|
18
|
+
all(): Promise<D1Result>;
|
|
19
|
+
}
|
|
20
|
+
interface D1Result {
|
|
21
|
+
results: Record<string, unknown>[];
|
|
22
|
+
meta: D1Meta;
|
|
23
|
+
}
|
|
24
|
+
interface D1Meta {
|
|
25
|
+
/** Rows written by the statement. D1 reports 0 for reads. */
|
|
26
|
+
changes: number;
|
|
27
|
+
/** Rowid of the last inserted row, meaningful only after an insert. */
|
|
28
|
+
last_row_id: number;
|
|
29
|
+
/** Rows D1 scanned. Billed, and absent on some responses. */
|
|
30
|
+
rows_read?: number;
|
|
31
|
+
/** Rows D1 persisted. Billed, and absent on some responses. */
|
|
32
|
+
rows_written?: number;
|
|
33
|
+
/** Wall time D1 spent on the statement, in milliseconds. */
|
|
34
|
+
duration?: number;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/observer.d.ts
|
|
38
|
+
/** What one executed statement cost, as D1 reported it. */
|
|
39
|
+
interface D1StatementReport {
|
|
40
|
+
/** The operation that produced it: `select`, `insert`, `update`, and so on. */
|
|
41
|
+
kind: string;
|
|
42
|
+
/** The table it targeted, absent for a raw statement. */
|
|
43
|
+
table: string | undefined;
|
|
44
|
+
/** Rows D1 scanned. `0` when D1 omits the figure, never estimated. */
|
|
45
|
+
rowsRead: number;
|
|
46
|
+
/** Rows D1 persisted. `0` when D1 omits the figure, never estimated. */
|
|
47
|
+
rowsWritten: number;
|
|
48
|
+
/** Wall time D1 spent, in milliseconds. `0` when D1 omits it. */
|
|
49
|
+
durationMs: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Called after each statement the driver executes.
|
|
53
|
+
*
|
|
54
|
+
* D1 bills on rows read and written, and its analytics report per database
|
|
55
|
+
* rather than per query, so these numbers are the only way to attribute cost
|
|
56
|
+
* to the query or the request that caused it. They ride along on responses the
|
|
57
|
+
* driver already reads, so observing them costs no extra statement and no
|
|
58
|
+
* extra billable operation.
|
|
59
|
+
*
|
|
60
|
+
* It runs on the hot path, once per statement, so keep it cheap.
|
|
61
|
+
*/
|
|
62
|
+
type D1StatementObserver = (report: D1StatementReport) => void;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/driver.d.ts
|
|
65
|
+
/** What one statement in a {@link D1DatabaseDriver.batch} produced. */
|
|
66
|
+
interface D1BatchResult {
|
|
67
|
+
/** Rows the statement returned. Empty for a write with no `returning`. */
|
|
68
|
+
rows: Record<string, unknown>[];
|
|
69
|
+
affectedRows: number;
|
|
70
|
+
insertId: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How the driver answers a `transaction()` call.
|
|
74
|
+
*
|
|
75
|
+
* - `throw` refuses, because D1 cannot honour it. The default.
|
|
76
|
+
* - `unsafe-nonatomic` accepts and runs each statement immediately, each
|
|
77
|
+
* committing on its own. A failure part-way leaves the earlier writes
|
|
78
|
+
* persisted, with no rollback. For code shared with a backend that does have
|
|
79
|
+
* transactions, where the alternative is not running at all.
|
|
80
|
+
*/
|
|
81
|
+
type D1TransactionMode = "throw" | "unsafe-nonatomic";
|
|
82
|
+
interface D1DriverOptions {
|
|
83
|
+
onStatement?: D1StatementObserver;
|
|
84
|
+
transactions?: D1TransactionMode;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* D1 speaks SQLite, so the query surface is the SQLite one. What it does not
|
|
88
|
+
* have is transactions: `BEGIN`, `COMMIT`, `SAVEPOINT` and friends are
|
|
89
|
+
* rejected at the SQL layer, and `batch()` is offered instead. `batch()` takes
|
|
90
|
+
* a prepared array up front, which cannot express the interleaved
|
|
91
|
+
* begin/execute/commit a `Database` transaction drives, so the capability is
|
|
92
|
+
* reported as absent rather than faked.
|
|
93
|
+
*
|
|
94
|
+
* `savepoints: false` is what makes `Database` reject a nested transaction on
|
|
95
|
+
* its own, before any savepoint method here can be reached — which holds in
|
|
96
|
+
* `unsafe-nonatomic` too, where nesting would be even less meaningful.
|
|
97
|
+
*/
|
|
98
|
+
declare const CAPABILITIES: Readonly<{
|
|
99
|
+
returning: true;
|
|
100
|
+
savepoints: false;
|
|
101
|
+
upsert: true;
|
|
102
|
+
transactionalDdl: false;
|
|
103
|
+
migrationLock: false;
|
|
104
|
+
}>;
|
|
105
|
+
/**
|
|
106
|
+
* A `DatabaseDriver` backed by a Cloudflare D1 binding.
|
|
107
|
+
*
|
|
108
|
+
* Statements are compiled by the same SQLite compiler `@remix-run/data-table`
|
|
109
|
+
* uses, then executed through D1's async prepared-statement API. Pass it to
|
|
110
|
+
* `Database`, or use {@link createD1Database} to get one already wired.
|
|
111
|
+
*/
|
|
112
|
+
declare class D1DatabaseDriver implements DatabaseDriver<"sqlite"> {
|
|
113
|
+
#private;
|
|
114
|
+
constructor(d1: D1Binding, options?: D1DriverOptions);
|
|
115
|
+
get dialect(): "sqlite";
|
|
116
|
+
get capabilities(): typeof CAPABILITIES;
|
|
117
|
+
execute(request: DataManipulationRequest): Promise<DataManipulationResult>;
|
|
118
|
+
executeScript(sql: string, _transaction?: TransactionToken): Promise<void>;
|
|
119
|
+
hasTable(table: TableRef, _transaction?: TransactionToken): Promise<boolean>;
|
|
120
|
+
hasColumn(table: TableRef, column: string, _transaction?: TransactionToken): Promise<boolean>;
|
|
121
|
+
/**
|
|
122
|
+
* Runs statements together, atomically, through D1's `batch()`.
|
|
123
|
+
*
|
|
124
|
+
* This is the answer to "several writes must land together" on a database
|
|
125
|
+
* with no transactions. `batch()` is D1's only atomic primitive: it takes
|
|
126
|
+
* every statement up front and commits them as a unit, which is why it
|
|
127
|
+
* cannot back `transaction()` but can back this.
|
|
128
|
+
*
|
|
129
|
+
* Statements are `SqlStatement`s, so `sql` from `remix/data-table`
|
|
130
|
+
* parameterises them and there is no reaching for the raw binding:
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* await db.batch([
|
|
134
|
+
* sql`insert into post (title) values (${title})`,
|
|
135
|
+
* sql`update counter set posts = posts + 1`,
|
|
136
|
+
* ]);
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @param statements The statements to run, in order.
|
|
140
|
+
* @returns One result per statement, in the same order.
|
|
141
|
+
*/
|
|
142
|
+
batch(statements: SqlStatement[]): Promise<D1BatchResult[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Drops every table the application owns.
|
|
145
|
+
*
|
|
146
|
+
* D1 keeps its own bookkeeping in `_cf_*` tables and SQLite keeps
|
|
147
|
+
* `sqlite_*`; dropping either breaks the binding, so both are left alone.
|
|
148
|
+
* There is no file to delete the way the SQLite driver deletes one.
|
|
149
|
+
*/
|
|
150
|
+
wipe(): Promise<void>;
|
|
151
|
+
/** A binding is owned by the runtime; there is no connection to release. */
|
|
152
|
+
close(): void;
|
|
153
|
+
/**
|
|
154
|
+
* Opens a transaction, if the caller accepted that it will not be one.
|
|
155
|
+
*
|
|
156
|
+
* No `BEGIN` is sent, because D1 rejects it. The token exists so
|
|
157
|
+
* `Database` has something to carry; statements inside the scope run and
|
|
158
|
+
* commit exactly as they would outside it.
|
|
159
|
+
*/
|
|
160
|
+
beginTransaction(_options?: TransactionOptions): Promise<TransactionToken>;
|
|
161
|
+
/** Nothing to commit: every statement in the scope already did. */
|
|
162
|
+
commitTransaction(_token: TransactionToken): Promise<void>;
|
|
163
|
+
/**
|
|
164
|
+
* Nothing to roll back. This is the cost of `unsafe-nonatomic`, and it is
|
|
165
|
+
* silent by necessity: `Database` calls this while unwinding a failed
|
|
166
|
+
* callback, and throwing here would replace the caller's error with an
|
|
167
|
+
* `AggregateError` about a rollback that was never possible.
|
|
168
|
+
*/
|
|
169
|
+
rollbackTransaction(_token: TransactionToken): Promise<void>;
|
|
170
|
+
createSavepoint(_token: TransactionToken, _name: string): Promise<void>;
|
|
171
|
+
rollbackToSavepoint(_token: TransactionToken, _name: string): Promise<void>;
|
|
172
|
+
releaseSavepoint(_token: TransactionToken, _name: string): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/database.d.ts
|
|
176
|
+
interface D1DatabaseOptions extends DatabaseOptions {
|
|
177
|
+
/**
|
|
178
|
+
* Called after each statement, with the rows read, rows written, and
|
|
179
|
+
* duration D1 reported for it. See {@link D1StatementObserver}.
|
|
180
|
+
*/
|
|
181
|
+
onStatement?: D1StatementObserver;
|
|
182
|
+
/**
|
|
183
|
+
* What `transaction()` does. Defaults to `throw`, because D1 has no
|
|
184
|
+
* transactions; `unsafe-nonatomic` accepts the call and gives up
|
|
185
|
+
* atomicity. See {@link D1TransactionMode}.
|
|
186
|
+
*/
|
|
187
|
+
transactions?: D1TransactionMode;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A `Database` bound to Cloudflare D1.
|
|
191
|
+
*
|
|
192
|
+
* The same shape `SqliteDatabase` and `PostgresDatabase` have: a `Database`
|
|
193
|
+
* subclass that supplies its own driver, so every query, persistence and
|
|
194
|
+
* migration method comes from `remix/data-table` unchanged.
|
|
195
|
+
*/
|
|
196
|
+
declare class D1Database extends Database<"sqlite"> {
|
|
197
|
+
#private;
|
|
198
|
+
constructor(binding: D1Binding, options?: D1DatabaseOptions);
|
|
199
|
+
/**
|
|
200
|
+
* Runs statements together, atomically.
|
|
201
|
+
*
|
|
202
|
+
* D1 has no transactions, so `transaction()` refuses. `batch()` is what it
|
|
203
|
+
* offers instead, and this is it without reaching for the raw binding:
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* import { sql } from "remix/data-table";
|
|
207
|
+
*
|
|
208
|
+
* await db.batch([
|
|
209
|
+
* sql`insert into post (title) values (${title})`,
|
|
210
|
+
* sql`update counter set posts = posts + 1`,
|
|
211
|
+
* ]);
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* The statements are `SqlStatement`s rather than query-builder calls,
|
|
215
|
+
* because `data-table` exposes no way to build an operation without
|
|
216
|
+
* running it. `sql` still parameterises the values.
|
|
217
|
+
*/
|
|
218
|
+
batch(statements: SqlStatement[]): Promise<D1BatchResult[]>;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Wraps a D1 binding in a `Database`.
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* import { createD1Database } from "@pitlane/data-table-d1";
|
|
225
|
+
* import { env } from "cloudflare:workers";
|
|
226
|
+
*
|
|
227
|
+
* let db = createD1Database(env.DB);
|
|
228
|
+
* let posts = await db.query(Post).all();
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @param binding The D1 binding, e.g. `env.DB`.
|
|
232
|
+
* @param options `Database` options, plus `onStatement` and `transactions`.
|
|
233
|
+
*/
|
|
234
|
+
declare function createD1Database(binding: D1Binding, options?: D1DatabaseOptions): D1Database;
|
|
235
|
+
//#endregion
|
|
236
|
+
export { type D1BatchResult, type D1Binding, D1Database, D1DatabaseDriver, type D1DatabaseOptions, type D1DriverOptions, type D1Meta, type D1PreparedStatement, type D1Result, type D1StatementObserver, type D1StatementReport, type D1TransactionMode, createD1Database };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { Database, getTableName, getTablePrimaryKey } from "remix/data-table";
|
|
2
|
+
import { collectColumns, normalizeJoinType, quotePath } from "remix/data-table/sql-helpers";
|
|
3
|
+
//#region src/observer.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reports a statement, absorbing anything the observer throws.
|
|
6
|
+
*
|
|
7
|
+
* Measurement must not be able to fail the thing it measures: a broken
|
|
8
|
+
* observer should cost its own numbers, not the caller's write.
|
|
9
|
+
*/
|
|
10
|
+
function report(observer, kind, table, meta) {
|
|
11
|
+
if (!observer) return;
|
|
12
|
+
try {
|
|
13
|
+
observer({
|
|
14
|
+
kind,
|
|
15
|
+
table,
|
|
16
|
+
rowsRead: meta.rows_read ?? 0,
|
|
17
|
+
rowsWritten: meta.rows_written ?? 0,
|
|
18
|
+
durationMs: meta.duration ?? 0
|
|
19
|
+
});
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/sql-compiler.ts
|
|
24
|
+
function compileSqliteOperation(operation) {
|
|
25
|
+
if (operation.kind === "raw") return {
|
|
26
|
+
text: operation.sql.text,
|
|
27
|
+
values: [...operation.sql.values]
|
|
28
|
+
};
|
|
29
|
+
let context = { values: [] };
|
|
30
|
+
if (operation.kind === "select") {
|
|
31
|
+
let selection = "*";
|
|
32
|
+
if (operation.select !== "*") selection = operation.select.map((field) => quotePath$1(field.column) + " as " + quoteIdentifier$1(field.alias)).join(", ");
|
|
33
|
+
return {
|
|
34
|
+
text: "select " + (operation.distinct ? "distinct " : "") + selection + compileFromClause(operation.table, operation.joins, context) + compileWhereClause(operation.where, context) + compileGroupByClause(operation.groupBy) + compileHavingClause(operation.having, context) + compileOrderByClause(operation.orderBy) + compileLimitClause(operation.limit, context) + compileOffsetClause(operation.offset, context),
|
|
35
|
+
values: context.values
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (operation.kind === "count" || operation.kind === "exists") {
|
|
39
|
+
let inner = "select 1" + compileFromClause(operation.table, operation.joins, context) + compileWhereClause(operation.where, context) + compileGroupByClause(operation.groupBy) + compileHavingClause(operation.having, context);
|
|
40
|
+
return {
|
|
41
|
+
text: "select count(*) as " + quoteIdentifier$1("count") + " from (" + inner + ") as " + quoteIdentifier$1("__dt_count"),
|
|
42
|
+
values: context.values
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (operation.kind === "insert") return compileInsertOperation(operation.table, operation.values, operation.returning, context);
|
|
46
|
+
if (operation.kind === "insertMany") return compileInsertManyOperation(operation.table, operation.values, operation.returning, context);
|
|
47
|
+
if (operation.kind === "update") {
|
|
48
|
+
let columns = Object.keys(operation.changes);
|
|
49
|
+
return {
|
|
50
|
+
text: "update " + quotePath$1(getTableName(operation.table)) + " set " + columns.map((column) => quotePath$1(column) + " = " + pushValue(context, operation.changes[column])).join(", ") + compileWhereClause(operation.where, context) + compileReturningClause(operation.returning),
|
|
51
|
+
values: context.values
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (operation.kind === "delete") return {
|
|
55
|
+
text: "delete from " + quotePath$1(getTableName(operation.table)) + compileWhereClause(operation.where, context) + compileReturningClause(operation.returning),
|
|
56
|
+
values: context.values
|
|
57
|
+
};
|
|
58
|
+
if (operation.kind === "upsert") return compileUpsertOperation(operation, context);
|
|
59
|
+
throw new Error("Unsupported operation kind");
|
|
60
|
+
}
|
|
61
|
+
function compileInsertOperation(table, values, returning, context) {
|
|
62
|
+
let columns = Object.keys(values);
|
|
63
|
+
if (columns.length === 0) return {
|
|
64
|
+
text: "insert into " + quotePath$1(getTableName(table)) + " default values" + compileReturningClause(returning),
|
|
65
|
+
values: context.values
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
text: "insert into " + quotePath$1(getTableName(table)) + " (" + columns.map((column) => quotePath$1(column)).join(", ") + ") values (" + columns.map((column) => pushValue(context, values[column])).join(", ") + ")" + compileReturningClause(returning),
|
|
69
|
+
values: context.values
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function compileInsertManyOperation(table, rows, returning, context) {
|
|
73
|
+
if (rows.length === 0) return {
|
|
74
|
+
text: "select 0 where 1 = 0",
|
|
75
|
+
values: context.values
|
|
76
|
+
};
|
|
77
|
+
let columns = collectColumns$1(rows);
|
|
78
|
+
if (columns.length === 0) return {
|
|
79
|
+
text: "insert into " + quotePath$1(getTableName(table)) + " default values" + compileReturningClause(returning),
|
|
80
|
+
values: context.values
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
text: "insert into " + quotePath$1(getTableName(table)) + " (" + columns.map((column) => quotePath$1(column)).join(", ") + ") values " + rows.map((row) => "(" + columns.map((column) => {
|
|
84
|
+
return pushValue(context, Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null);
|
|
85
|
+
}).join(", ") + ")").join(", ") + compileReturningClause(returning),
|
|
86
|
+
values: context.values
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function compileUpsertOperation(operation, context) {
|
|
90
|
+
let insertColumns = Object.keys(operation.values);
|
|
91
|
+
let conflictTarget = operation.conflictTarget ?? [...getTablePrimaryKey(operation.table)];
|
|
92
|
+
if (insertColumns.length === 0) throw new Error("upsert requires at least one value");
|
|
93
|
+
let updateValues = operation.update ?? operation.values;
|
|
94
|
+
let updateColumns = Object.keys(updateValues);
|
|
95
|
+
let conflictClause = "";
|
|
96
|
+
if (updateColumns.length === 0) conflictClause = " on conflict (" + conflictTarget.map((column) => quotePath$1(column)).join(", ") + ") do nothing";
|
|
97
|
+
else conflictClause = " on conflict (" + conflictTarget.map((column) => quotePath$1(column)).join(", ") + ") do update set " + updateColumns.map((column) => quotePath$1(column) + " = " + pushValue(context, updateValues[column])).join(", ");
|
|
98
|
+
return {
|
|
99
|
+
text: "insert into " + quotePath$1(getTableName(operation.table)) + " (" + insertColumns.map((column) => quotePath$1(column)).join(", ") + ") values (" + insertColumns.map((column) => pushValue(context, operation.values[column])).join(", ") + ")" + conflictClause + compileReturningClause(operation.returning),
|
|
100
|
+
values: context.values
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function compileFromClause(table, joins, context) {
|
|
104
|
+
let output = " from " + quotePath$1(getTableName(table));
|
|
105
|
+
for (let join of joins) output += " " + normalizeJoinType$1(join.type) + " join " + quotePath$1(getTableName(join.table)) + " on " + compilePredicate(join.on, context);
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
function compileWhereClause(predicates, context) {
|
|
109
|
+
if (predicates.length === 0) return "";
|
|
110
|
+
return " where " + predicates.map((predicate) => "(" + compilePredicate(predicate, context) + ")").join(" and ");
|
|
111
|
+
}
|
|
112
|
+
function compileGroupByClause(columns) {
|
|
113
|
+
if (columns.length === 0) return "";
|
|
114
|
+
return " group by " + columns.map((column) => quotePath$1(column)).join(", ");
|
|
115
|
+
}
|
|
116
|
+
function compileHavingClause(predicates, context) {
|
|
117
|
+
if (predicates.length === 0) return "";
|
|
118
|
+
return " having " + predicates.map((predicate) => "(" + compilePredicate(predicate, context) + ")").join(" and ");
|
|
119
|
+
}
|
|
120
|
+
function compileOrderByClause(orderBy) {
|
|
121
|
+
if (orderBy.length === 0) return "";
|
|
122
|
+
return " order by " + orderBy.map((clause) => quotePath$1(clause.column) + " " + clause.direction.toUpperCase()).join(", ");
|
|
123
|
+
}
|
|
124
|
+
function compileLimitClause(limit, context) {
|
|
125
|
+
if (limit === void 0) return "";
|
|
126
|
+
return " limit " + pushValue(context, limit);
|
|
127
|
+
}
|
|
128
|
+
function compileOffsetClause(offset, context) {
|
|
129
|
+
if (offset === void 0) return "";
|
|
130
|
+
return " offset " + pushValue(context, offset);
|
|
131
|
+
}
|
|
132
|
+
function compileReturningClause(returning) {
|
|
133
|
+
if (!returning) return "";
|
|
134
|
+
if (returning === "*") return " returning *";
|
|
135
|
+
return " returning " + returning.map((column) => quotePath$1(column)).join(", ");
|
|
136
|
+
}
|
|
137
|
+
function compilePredicate(predicate, context) {
|
|
138
|
+
if (predicate.type === "comparison") {
|
|
139
|
+
let column = quotePath$1(predicate.column);
|
|
140
|
+
if (predicate.operator === "eq") {
|
|
141
|
+
if (predicate.valueType === "value" && (predicate.value === null || predicate.value === void 0)) return column + " is null";
|
|
142
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
143
|
+
return column + " = " + comparisonValue;
|
|
144
|
+
}
|
|
145
|
+
if (predicate.operator === "ne") {
|
|
146
|
+
if (predicate.valueType === "value" && (predicate.value === null || predicate.value === void 0)) return column + " is not null";
|
|
147
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
148
|
+
return column + " <> " + comparisonValue;
|
|
149
|
+
}
|
|
150
|
+
if (predicate.operator === "gt") {
|
|
151
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
152
|
+
return column + " > " + comparisonValue;
|
|
153
|
+
}
|
|
154
|
+
if (predicate.operator === "gte") {
|
|
155
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
156
|
+
return column + " >= " + comparisonValue;
|
|
157
|
+
}
|
|
158
|
+
if (predicate.operator === "lt") {
|
|
159
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
160
|
+
return column + " < " + comparisonValue;
|
|
161
|
+
}
|
|
162
|
+
if (predicate.operator === "lte") {
|
|
163
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
164
|
+
return column + " <= " + comparisonValue;
|
|
165
|
+
}
|
|
166
|
+
if (predicate.operator === "in" || predicate.operator === "notIn") {
|
|
167
|
+
let values = Array.isArray(predicate.value) ? predicate.value : [];
|
|
168
|
+
if (values.length === 0) return predicate.operator === "in" ? "1 = 0" : "1 = 1";
|
|
169
|
+
let keyword = predicate.operator === "in" ? "in" : "not in";
|
|
170
|
+
return column + " " + keyword + " (" + values.map((value) => pushValue(context, value)).join(", ") + ")";
|
|
171
|
+
}
|
|
172
|
+
if (predicate.operator === "like") {
|
|
173
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
174
|
+
return column + " like " + comparisonValue;
|
|
175
|
+
}
|
|
176
|
+
if (predicate.operator === "ilike") {
|
|
177
|
+
let comparisonValue = compileComparisonValue(predicate, context);
|
|
178
|
+
return "lower(" + column + ") like lower(" + comparisonValue + ")";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (predicate.type === "between") return quotePath$1(predicate.column) + " between " + pushValue(context, predicate.lower) + " and " + pushValue(context, predicate.upper);
|
|
182
|
+
if (predicate.type === "null") return quotePath$1(predicate.column) + (predicate.operator === "isNull" ? " is null" : " is not null");
|
|
183
|
+
if (predicate.type === "logical") {
|
|
184
|
+
if (predicate.predicates.length === 0) return predicate.operator === "and" ? "1 = 1" : "1 = 0";
|
|
185
|
+
let joiner = predicate.operator === "and" ? " and " : " or ";
|
|
186
|
+
return predicate.predicates.map((child) => "(" + compilePredicate(child, context) + ")").join(joiner);
|
|
187
|
+
}
|
|
188
|
+
throw new Error("Unsupported predicate");
|
|
189
|
+
}
|
|
190
|
+
function compileComparisonValue(predicate, context) {
|
|
191
|
+
if (predicate.valueType === "column") return quotePath$1(predicate.value);
|
|
192
|
+
return pushValue(context, predicate.value);
|
|
193
|
+
}
|
|
194
|
+
function normalizeJoinType$1(type) {
|
|
195
|
+
return normalizeJoinType(type);
|
|
196
|
+
}
|
|
197
|
+
function quoteIdentifier$1(value) {
|
|
198
|
+
return "\"" + value.replace(/"/g, "\"\"") + "\"";
|
|
199
|
+
}
|
|
200
|
+
function quotePath$1(path) {
|
|
201
|
+
return quotePath(path, quoteIdentifier$1);
|
|
202
|
+
}
|
|
203
|
+
function pushValue(context, value) {
|
|
204
|
+
context.values.push(normalizeBoundValue(value));
|
|
205
|
+
return "?";
|
|
206
|
+
}
|
|
207
|
+
function normalizeBoundValue(value) {
|
|
208
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function collectColumns$1(rows) {
|
|
212
|
+
return collectColumns(rows);
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/driver.ts
|
|
216
|
+
/**
|
|
217
|
+
* D1 speaks SQLite, so the query surface is the SQLite one. What it does not
|
|
218
|
+
* have is transactions: `BEGIN`, `COMMIT`, `SAVEPOINT` and friends are
|
|
219
|
+
* rejected at the SQL layer, and `batch()` is offered instead. `batch()` takes
|
|
220
|
+
* a prepared array up front, which cannot express the interleaved
|
|
221
|
+
* begin/execute/commit a `Database` transaction drives, so the capability is
|
|
222
|
+
* reported as absent rather than faked.
|
|
223
|
+
*
|
|
224
|
+
* `savepoints: false` is what makes `Database` reject a nested transaction on
|
|
225
|
+
* its own, before any savepoint method here can be reached — which holds in
|
|
226
|
+
* `unsafe-nonatomic` too, where nesting would be even less meaningful.
|
|
227
|
+
*/
|
|
228
|
+
const CAPABILITIES = Object.freeze({
|
|
229
|
+
returning: true,
|
|
230
|
+
savepoints: false,
|
|
231
|
+
upsert: true,
|
|
232
|
+
transactionalDdl: false,
|
|
233
|
+
migrationLock: false
|
|
234
|
+
});
|
|
235
|
+
const NO_TRANSACTIONS = "[@pitlane/data-table-d1] D1 rejects SQL transactions and savepoints. Group writes with `d1.batch()`, which is atomic, or model the operation so it does not need one. If the caller is shared with a backend that does have transactions and running without atomicity beats not running, opt in with `createD1Database(env.DB, { transactions: \"unsafe-nonatomic\" })`.";
|
|
236
|
+
const NO_SAVEPOINTS = "[@pitlane/data-table-d1] D1 rejects savepoints, so nested transactions are unavailable whatever `transactions` is set to.";
|
|
237
|
+
/**
|
|
238
|
+
* A `DatabaseDriver` backed by a Cloudflare D1 binding.
|
|
239
|
+
*
|
|
240
|
+
* Statements are compiled by the same SQLite compiler `@remix-run/data-table`
|
|
241
|
+
* uses, then executed through D1's async prepared-statement API. Pass it to
|
|
242
|
+
* `Database`, or use {@link createD1Database} to get one already wired.
|
|
243
|
+
*/
|
|
244
|
+
var D1DatabaseDriver = class {
|
|
245
|
+
#d1;
|
|
246
|
+
#onStatement;
|
|
247
|
+
#transactions;
|
|
248
|
+
#openedTransactions = 0;
|
|
249
|
+
constructor(d1, options = {}) {
|
|
250
|
+
this.#d1 = d1;
|
|
251
|
+
this.#onStatement = options.onStatement;
|
|
252
|
+
this.#transactions = options.transactions ?? "throw";
|
|
253
|
+
}
|
|
254
|
+
get dialect() {
|
|
255
|
+
return "sqlite";
|
|
256
|
+
}
|
|
257
|
+
get capabilities() {
|
|
258
|
+
return CAPABILITIES;
|
|
259
|
+
}
|
|
260
|
+
async execute(request) {
|
|
261
|
+
let { operation } = request;
|
|
262
|
+
if (operation.kind === "insertMany" && operation.values.length === 0) return {
|
|
263
|
+
affectedRows: 0,
|
|
264
|
+
insertId: void 0,
|
|
265
|
+
rows: operation.returning ? [] : void 0
|
|
266
|
+
};
|
|
267
|
+
let statement = compileSqliteOperation(operation);
|
|
268
|
+
let values = statement.values.map((value) => value === void 0 ? null : value);
|
|
269
|
+
let result = await this.#d1.prepare(statement.text).bind(...values).all();
|
|
270
|
+
report(this.#onStatement, operation.kind, tableName(operation), result.meta);
|
|
271
|
+
return readsRows(operation) ? readerResult(operation, result) : writerResult(operation, result.meta);
|
|
272
|
+
}
|
|
273
|
+
async executeScript(sql, _transaction) {
|
|
274
|
+
await this.#d1.exec(sql);
|
|
275
|
+
}
|
|
276
|
+
async hasTable(table, _transaction) {
|
|
277
|
+
let master = table.schema ? `${quoteIdentifier(table.schema)}.sqlite_master` : "sqlite_master";
|
|
278
|
+
return (await this.#d1.prepare(`select 1 from ${master} where type = ? and name = ? limit 1`).bind("table", table.name).all()).results.length > 0;
|
|
279
|
+
}
|
|
280
|
+
async hasColumn(table, column, _transaction) {
|
|
281
|
+
return (await this.#d1.prepare("select name from pragma_table_info(?)").bind(table.name).all()).results.some((row) => row.name === column);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Runs statements together, atomically, through D1's `batch()`.
|
|
285
|
+
*
|
|
286
|
+
* This is the answer to "several writes must land together" on a database
|
|
287
|
+
* with no transactions. `batch()` is D1's only atomic primitive: it takes
|
|
288
|
+
* every statement up front and commits them as a unit, which is why it
|
|
289
|
+
* cannot back `transaction()` but can back this.
|
|
290
|
+
*
|
|
291
|
+
* Statements are `SqlStatement`s, so `sql` from `remix/data-table`
|
|
292
|
+
* parameterises them and there is no reaching for the raw binding:
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* await db.batch([
|
|
296
|
+
* sql`insert into post (title) values (${title})`,
|
|
297
|
+
* sql`update counter set posts = posts + 1`,
|
|
298
|
+
* ]);
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @param statements The statements to run, in order.
|
|
302
|
+
* @returns One result per statement, in the same order.
|
|
303
|
+
*/
|
|
304
|
+
async batch(statements) {
|
|
305
|
+
if (statements.length === 0) return [];
|
|
306
|
+
return (await this.#d1.batch(statements.map((statement) => this.#d1.prepare(statement.text).bind(...statement.values.map((value) => value === void 0 ? null : value))))).map((result) => {
|
|
307
|
+
report(this.#onStatement, "batch", void 0, result.meta);
|
|
308
|
+
return {
|
|
309
|
+
rows: result.results.map((row) => ({ ...row })),
|
|
310
|
+
affectedRows: Number(result.meta.changes),
|
|
311
|
+
insertId: result.meta.last_row_id
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Drops every table the application owns.
|
|
317
|
+
*
|
|
318
|
+
* D1 keeps its own bookkeeping in `_cf_*` tables and SQLite keeps
|
|
319
|
+
* `sqlite_*`; dropping either breaks the binding, so both are left alone.
|
|
320
|
+
* There is no file to delete the way the SQLite driver deletes one.
|
|
321
|
+
*/
|
|
322
|
+
async wipe() {
|
|
323
|
+
let names = (await this.#d1.prepare("select name from sqlite_master where type = 'table' and name not like 'sqlite\\_%' escape '\\' and name not like '\\_cf\\_%' escape '\\'").all()).results.map((row) => String(row.name));
|
|
324
|
+
if (names.length === 0) return;
|
|
325
|
+
await this.#d1.batch([this.#d1.prepare("pragma defer_foreign_keys = true"), ...names.map((name) => this.#d1.prepare(`drop table if exists ${quoteIdentifier(name)}`))]);
|
|
326
|
+
}
|
|
327
|
+
/** A binding is owned by the runtime; there is no connection to release. */
|
|
328
|
+
close() {}
|
|
329
|
+
/**
|
|
330
|
+
* Opens a transaction, if the caller accepted that it will not be one.
|
|
331
|
+
*
|
|
332
|
+
* No `BEGIN` is sent, because D1 rejects it. The token exists so
|
|
333
|
+
* `Database` has something to carry; statements inside the scope run and
|
|
334
|
+
* commit exactly as they would outside it.
|
|
335
|
+
*/
|
|
336
|
+
async beginTransaction(_options) {
|
|
337
|
+
if (this.#transactions === "throw") throw new Error(NO_TRANSACTIONS);
|
|
338
|
+
return { id: `d1-nonatomic-${++this.#openedTransactions}` };
|
|
339
|
+
}
|
|
340
|
+
/** Nothing to commit: every statement in the scope already did. */
|
|
341
|
+
async commitTransaction(_token) {
|
|
342
|
+
if (this.#transactions === "throw") throw new Error(NO_TRANSACTIONS);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Nothing to roll back. This is the cost of `unsafe-nonatomic`, and it is
|
|
346
|
+
* silent by necessity: `Database` calls this while unwinding a failed
|
|
347
|
+
* callback, and throwing here would replace the caller's error with an
|
|
348
|
+
* `AggregateError` about a rollback that was never possible.
|
|
349
|
+
*/
|
|
350
|
+
async rollbackTransaction(_token) {
|
|
351
|
+
if (this.#transactions === "throw") throw new Error(NO_TRANSACTIONS);
|
|
352
|
+
}
|
|
353
|
+
async createSavepoint(_token, _name) {
|
|
354
|
+
throw new Error(NO_SAVEPOINTS);
|
|
355
|
+
}
|
|
356
|
+
async rollbackToSavepoint(_token, _name) {
|
|
357
|
+
throw new Error(NO_SAVEPOINTS);
|
|
358
|
+
}
|
|
359
|
+
async releaseSavepoint(_token, _name) {
|
|
360
|
+
throw new Error(NO_SAVEPOINTS);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
/** The table an operation targeted. A raw statement names none. */
|
|
364
|
+
function tableName(operation) {
|
|
365
|
+
return operation.kind === "raw" ? void 0 : getTableName(operation.table);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Whether the operation's response carries rows.
|
|
369
|
+
*
|
|
370
|
+
* `raw` is the awkward one: the caller's SQL may or may not select anything,
|
|
371
|
+
* and D1 will not say which. Reporting rows costs nothing when there are none,
|
|
372
|
+
* so raw statements always come back with an array.
|
|
373
|
+
*/
|
|
374
|
+
function readsRows(operation) {
|
|
375
|
+
if (operation.kind === "select" || operation.kind === "count" || operation.kind === "exists") return true;
|
|
376
|
+
if (operation.kind === "raw") return true;
|
|
377
|
+
return operation.returning !== void 0;
|
|
378
|
+
}
|
|
379
|
+
function readerResult(operation, result) {
|
|
380
|
+
let rows = result.results.map((row) => ({ ...row }));
|
|
381
|
+
if (operation.kind === "count" || operation.kind === "exists") rows = rows.map(normalizeCount);
|
|
382
|
+
return {
|
|
383
|
+
rows,
|
|
384
|
+
affectedRows: isWrite(operation.kind) ? rows.length : void 0,
|
|
385
|
+
insertId: lastPrimaryKey(operation, rows)
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function writerResult(operation, meta) {
|
|
389
|
+
return {
|
|
390
|
+
affectedRows: isWrite(operation.kind) ? Number(meta.changes) : void 0,
|
|
391
|
+
insertId: isInsert(operation) && singlePrimaryKey(operation) ? meta.last_row_id : void 0
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* `count(*)` arrives as a string or bigint depending on magnitude and driver;
|
|
396
|
+
* callers expect a number.
|
|
397
|
+
*/
|
|
398
|
+
function normalizeCount(row) {
|
|
399
|
+
let { count } = row;
|
|
400
|
+
if (typeof count === "bigint") return {
|
|
401
|
+
...row,
|
|
402
|
+
count: Number(count)
|
|
403
|
+
};
|
|
404
|
+
if (typeof count === "string") {
|
|
405
|
+
let numeric = Number(count);
|
|
406
|
+
if (!Number.isNaN(numeric)) return {
|
|
407
|
+
...row,
|
|
408
|
+
count: numeric
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return row;
|
|
412
|
+
}
|
|
413
|
+
/** The generated key of the last inserted row, read back out of RETURNING. */
|
|
414
|
+
function lastPrimaryKey(operation, rows) {
|
|
415
|
+
if (!isInsert(operation)) return void 0;
|
|
416
|
+
let key = singlePrimaryKey(operation);
|
|
417
|
+
if (!key) return void 0;
|
|
418
|
+
return rows[rows.length - 1]?.[key];
|
|
419
|
+
}
|
|
420
|
+
/** A composite key has no single insert id to report. */
|
|
421
|
+
function singlePrimaryKey(operation) {
|
|
422
|
+
let primaryKey = getTablePrimaryKey(operation.table);
|
|
423
|
+
return primaryKey.length === 1 ? primaryKey[0] : void 0;
|
|
424
|
+
}
|
|
425
|
+
function isInsert(operation) {
|
|
426
|
+
return operation.kind === "insert" || operation.kind === "insertMany" || operation.kind === "upsert";
|
|
427
|
+
}
|
|
428
|
+
function isWrite(kind) {
|
|
429
|
+
return kind === "insert" || kind === "insertMany" || kind === "update" || kind === "delete" || kind === "upsert";
|
|
430
|
+
}
|
|
431
|
+
function quoteIdentifier(value) {
|
|
432
|
+
return `"${value.replace(/"/g, "\"\"")}"`;
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/database.ts
|
|
436
|
+
/**
|
|
437
|
+
* A `Database` bound to Cloudflare D1.
|
|
438
|
+
*
|
|
439
|
+
* The same shape `SqliteDatabase` and `PostgresDatabase` have: a `Database`
|
|
440
|
+
* subclass that supplies its own driver, so every query, persistence and
|
|
441
|
+
* migration method comes from `remix/data-table` unchanged.
|
|
442
|
+
*/
|
|
443
|
+
var D1Database = class extends Database {
|
|
444
|
+
#driver;
|
|
445
|
+
constructor(binding, options) {
|
|
446
|
+
let { onStatement, transactions, ...databaseOptions } = options ?? {};
|
|
447
|
+
let driver = new D1DatabaseDriver(binding, {
|
|
448
|
+
onStatement,
|
|
449
|
+
transactions
|
|
450
|
+
});
|
|
451
|
+
super(driver, databaseOptions);
|
|
452
|
+
this.#driver = driver;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Runs statements together, atomically.
|
|
456
|
+
*
|
|
457
|
+
* D1 has no transactions, so `transaction()` refuses. `batch()` is what it
|
|
458
|
+
* offers instead, and this is it without reaching for the raw binding:
|
|
459
|
+
*
|
|
460
|
+
* ```ts
|
|
461
|
+
* import { sql } from "remix/data-table";
|
|
462
|
+
*
|
|
463
|
+
* await db.batch([
|
|
464
|
+
* sql`insert into post (title) values (${title})`,
|
|
465
|
+
* sql`update counter set posts = posts + 1`,
|
|
466
|
+
* ]);
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* The statements are `SqlStatement`s rather than query-builder calls,
|
|
470
|
+
* because `data-table` exposes no way to build an operation without
|
|
471
|
+
* running it. `sql` still parameterises the values.
|
|
472
|
+
*/
|
|
473
|
+
batch(statements) {
|
|
474
|
+
return this.#driver.batch(statements);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Wraps a D1 binding in a `Database`.
|
|
479
|
+
*
|
|
480
|
+
* ```ts
|
|
481
|
+
* import { createD1Database } from "@pitlane/data-table-d1";
|
|
482
|
+
* import { env } from "cloudflare:workers";
|
|
483
|
+
*
|
|
484
|
+
* let db = createD1Database(env.DB);
|
|
485
|
+
* let posts = await db.query(Post).all();
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @param binding The D1 binding, e.g. `env.DB`.
|
|
489
|
+
* @param options `Database` options, plus `onStatement` and `transactions`.
|
|
490
|
+
*/
|
|
491
|
+
function createD1Database(binding, options) {
|
|
492
|
+
return new D1Database(binding, options);
|
|
493
|
+
}
|
|
494
|
+
//#endregion
|
|
495
|
+
export { D1Database, D1DatabaseDriver, createD1Database };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/migrations.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What {@link generateD1Migrations} needs to know.
|
|
4
|
+
*/
|
|
5
|
+
interface GenerateD1MigrationsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Directory holding the `data-table` migrations, one `<id>_<name>/`
|
|
8
|
+
* directory per migration.
|
|
9
|
+
*
|
|
10
|
+
* @default "db/migrations"
|
|
11
|
+
*/
|
|
12
|
+
from?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Where the generated `.sql` files land. Point this at the same directory
|
|
15
|
+
* as `migrations_dir` in your Wrangler config.
|
|
16
|
+
*/
|
|
17
|
+
to: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One generated file.
|
|
21
|
+
*/
|
|
22
|
+
interface GeneratedD1Migration {
|
|
23
|
+
/** Migration id, typically a `YYYYMMDDHHmmss` timestamp. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Migration slug. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Path of the written file, relative to the working directory. */
|
|
28
|
+
file: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Writes one `<id>_<name>.sql` per migration into `options.to`, then deletes
|
|
32
|
+
* generated files with no migration behind them, so the output directory is a
|
|
33
|
+
* pure function of the input one.
|
|
34
|
+
*
|
|
35
|
+
* The SQL is copied verbatim. Splitting it into statements is the job of
|
|
36
|
+
* whatever executes the file, and a splitter naive enough to write here would
|
|
37
|
+
* corrupt any migration containing a semicolon inside a string literal or a
|
|
38
|
+
* `begin ... end` trigger body.
|
|
39
|
+
*
|
|
40
|
+
* @param options Where to read migrations from, and where to write SQL to.
|
|
41
|
+
* @returns The generated files, ordered by migration id.
|
|
42
|
+
* @throws If `options.from` holds no migrations, or one of them has an empty
|
|
43
|
+
* `up`. Both mean the output directory is about to be wrong, and a migration
|
|
44
|
+
* runner is the worst place to discover that.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { generateD1Migrations } from "@pitlane/data-table-d1/migrations";
|
|
49
|
+
*
|
|
50
|
+
* await generateD1Migrations({ to: "db/d1-migrations" });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function generateD1Migrations(options: GenerateD1MigrationsOptions): Promise<GeneratedD1Migration[]>;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { GenerateD1MigrationsOptions, GeneratedD1Migration, generateD1Migrations };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { loadMigrations } from "remix/data-table/migrations/node";
|
|
4
|
+
//#region src/migrations.ts
|
|
5
|
+
/**
|
|
6
|
+
* Compiles `data-table` migrations into the flat `.sql` files Wrangler's D1
|
|
7
|
+
* migration runner reads.
|
|
8
|
+
*
|
|
9
|
+
* Node-only build tooling. Nothing here belongs in a Worker bundle, which is
|
|
10
|
+
* why it is a separate entry point from the driver.
|
|
11
|
+
*
|
|
12
|
+
* @module @pitlane/data-table-d1/migrations
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* A generated file, as opposed to a `.gitkeep` or a `README.md` that happens to
|
|
16
|
+
* sit in the same directory. Matches Wrangler's own migration filename shape.
|
|
17
|
+
*/
|
|
18
|
+
const GENERATED = /^\d{4,14}_.+\.sql$/;
|
|
19
|
+
/**
|
|
20
|
+
* Writes one `<id>_<name>.sql` per migration into `options.to`, then deletes
|
|
21
|
+
* generated files with no migration behind them, so the output directory is a
|
|
22
|
+
* pure function of the input one.
|
|
23
|
+
*
|
|
24
|
+
* The SQL is copied verbatim. Splitting it into statements is the job of
|
|
25
|
+
* whatever executes the file, and a splitter naive enough to write here would
|
|
26
|
+
* corrupt any migration containing a semicolon inside a string literal or a
|
|
27
|
+
* `begin ... end` trigger body.
|
|
28
|
+
*
|
|
29
|
+
* @param options Where to read migrations from, and where to write SQL to.
|
|
30
|
+
* @returns The generated files, ordered by migration id.
|
|
31
|
+
* @throws If `options.from` holds no migrations, or one of them has an empty
|
|
32
|
+
* `up`. Both mean the output directory is about to be wrong, and a migration
|
|
33
|
+
* runner is the worst place to discover that.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { generateD1Migrations } from "@pitlane/data-table-d1/migrations";
|
|
38
|
+
*
|
|
39
|
+
* await generateD1Migrations({ to: "db/d1-migrations" });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
async function generateD1Migrations(options) {
|
|
43
|
+
let from = path.resolve(options.from ?? "db/migrations");
|
|
44
|
+
let to = path.resolve(options.to);
|
|
45
|
+
let migrations = await loadMigrations(from);
|
|
46
|
+
if (migrations.length === 0) throw new Error(`[@pitlane/data-table-d1] no migrations found in ${from}`);
|
|
47
|
+
await mkdir(to, { recursive: true });
|
|
48
|
+
let generated = [];
|
|
49
|
+
for (let migration of migrations) {
|
|
50
|
+
if (migration.up.trim().length === 0) throw new Error(`[@pitlane/data-table-d1] migration ${migration.id}_${migration.name} has an empty \`up\``);
|
|
51
|
+
let filename = `${migration.id}_${migration.name}.sql`;
|
|
52
|
+
let file = path.join(to, filename);
|
|
53
|
+
await writeFile(file, header(migration.id, migration.name) + migration.up.trimEnd() + "\n");
|
|
54
|
+
generated.push({
|
|
55
|
+
id: migration.id,
|
|
56
|
+
name: migration.name,
|
|
57
|
+
file: path.relative(process.cwd(), file)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
await pruneStale(to, new Set(generated.map((entry) => path.basename(entry.file))));
|
|
61
|
+
return generated;
|
|
62
|
+
}
|
|
63
|
+
function header(id, name) {
|
|
64
|
+
return `-- Generated by @pitlane/data-table-d1 from ${id}_${name}/up.sql.\n-- Do not edit by hand; re-run the generator instead.
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
async function pruneStale(directory, keep) {
|
|
69
|
+
for (let entry of await readdir(directory)) {
|
|
70
|
+
if (!GENERATED.test(entry) || keep.has(entry)) continue;
|
|
71
|
+
await unlink(path.join(directory, entry));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { generateD1Migrations };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pitlane/data-table-d1",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Cloudflare D1 driver for Remix 3's data-table: SQLite SQL over D1's async prepared-statement binding.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"d1",
|
|
8
|
+
"data-table",
|
|
9
|
+
"pitlane",
|
|
10
|
+
"remix",
|
|
11
|
+
"sqlite",
|
|
12
|
+
"workers"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://pitlane.tools/package/data-table-d1/",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/pitlane-tools/pitlane/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Mark Malstrom <mark@malstrom.me>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/pitlane-tools/pitlane.git",
|
|
23
|
+
"directory": "packages/data-table-d1"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"LICENSE.remix"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./migrations": {
|
|
38
|
+
"types": "./dist/migrations.d.mts",
|
|
39
|
+
"import": "./dist/migrations.mjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prepublishOnly": "vp run build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^25.5.0",
|
|
47
|
+
"miniflare": "^4.20260710.0",
|
|
48
|
+
"remix": "3.0.0-beta.10",
|
|
49
|
+
"typescript": "^7.0.2",
|
|
50
|
+
"vite-plus": "^0.2.6"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"remix": "^3.0.0-beta.10"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
57
|
+
}
|
|
58
|
+
}
|