@lunora/d1 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE.md +6 -0
- package/dist/dialect.d.mts +29 -27
- package/dist/dialect.d.ts +29 -27
- package/dist/dialect.mjs +1 -35
- package/dist/index.d.mts +146 -135
- package/dist/index.d.ts +146 -135
- package/dist/index.mjs +1 -7
- package/dist/packem_shared/D1Client-B9m0fFLr.mjs +1 -0
- package/dist/packem_shared/MigrationRunner-DvnPBDVT.mjs +2 -0
- package/dist/packem_shared/backfillD1SearchIndexes-ERQf6szv.mjs +1 -0
- package/dist/packem_shared/exportGlobalRows-CmcmGZxv.mjs +1 -0
- package/dist/packem_shared/facetGlobalColumn-R-BETSuA.mjs +1 -0
- package/dist/packem_shared/quoteIdentifier-CObIFRhb.mjs +1 -0
- package/dist/packem_shared/sqliteDialect-B43yA0ek.mjs +1 -0
- package/package.json +5 -4
- package/dist/packem_shared/D1Client-DA3flo1o.mjs +0 -143
- package/dist/packem_shared/MigrationRunner-BkEwQ-Ya.mjs +0 -149
- package/dist/packem_shared/createD1CtxDb-BMR8J0dT.mjs +0 -14
- package/dist/packem_shared/exportGlobalRows-BGCPm_nA.mjs +0 -122
- package/dist/packem_shared/facetGlobalColumn-C6u_WMIY.mjs +0 -142
- package/dist/packem_shared/sqliteDialect-DqYnHPuu.mjs +0 -27
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaLike, DatabaseWriterLike } from '@lunora/do';
|
|
2
|
-
import { SqlCtxDbOptions,
|
|
2
|
+
import { SqlCtxDbOptions, SqlCtxExec, createSqlCtxDb, readSqlCdcChanges, SqlDialect } from '@lunora/sql-store';
|
|
3
3
|
export { type SqlCtxExec as D1Exec, type SqlCtxDbOptions, type SqlCtxExec, createSqlCtxDb } from '@lunora/sql-store';
|
|
4
4
|
import { BatchItem, BatchResponse } from 'drizzle-orm/batch';
|
|
5
5
|
import { DrizzleD1Database } from 'drizzle-orm/d1';
|
|
@@ -13,8 +13,10 @@ declare const runD1GlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike)
|
|
|
13
13
|
declare const runD1AggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
|
|
14
14
|
/** Materialize the `__rank_<index>` companion tables for the schema's rank indexes. */
|
|
15
15
|
declare const runD1RankMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
|
|
16
|
-
/** Materialize the `__fts_<index>` fts5 shadow tables for the schema's search indexes
|
|
16
|
+
/** Materialize (and backfill) the `__fts_<index>` fts5 shadow tables for the schema's search indexes. */
|
|
17
17
|
declare const runD1SearchMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
|
|
18
|
+
/** Index existing rows into every search companion, including the `staged: true` ones migrations leave empty. */
|
|
19
|
+
declare const backfillD1SearchIndexes: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
|
|
18
20
|
/** Create the `__cdc_log` table in D1 (idempotent; only run when CDC is enabled). */
|
|
19
21
|
declare const runD1CdcMigration: (exec: SqlCtxExec) => Promise<void>;
|
|
20
22
|
/** Read changelog entries newer than `sinceSeq` in commit order. */
|
|
@@ -42,32 +44,42 @@ interface ImportResult {
|
|
|
42
44
|
inserted: Record<string, number>;
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
|
-
* Return every `.global()` table in the schema, optionally narrowed by an
|
|
46
|
-
* allowlist. Shard-local tables are skipped here — they're handled by the DO
|
|
47
|
-
* helpers — so callers get a clean separation between the two storage planes.
|
|
48
|
-
*/
|
|
47
|
+
* Return every `.global()` table in the schema, optionally narrowed by an
|
|
48
|
+
* allowlist. Shard-local tables are skipped here — they're handled by the DO
|
|
49
|
+
* helpers — so callers get a clean separation between the two storage planes.
|
|
50
|
+
*/
|
|
49
51
|
declare const selectGlobalTables: (schema: SchemaLike, requested?: ReadonlyArray<string>) => string[];
|
|
50
52
|
interface ExportGlobalArgs {
|
|
51
53
|
batchSize?: number;
|
|
52
54
|
tables?: ReadonlyArray<string>;
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
|
-
* Yield rows from every requested `.global()` table in batches.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
57
|
+
* Yield rows from every requested `.global()` table in batches.
|
|
58
|
+
*
|
|
59
|
+
* Keyset-paginates on the physical primary key (`id` — every `.global()` table
|
|
60
|
+
* carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
|
|
61
|
+
* carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
|
|
62
|
+
* SQLite gives no ordering guarantee for an unordered SELECT and each page is a
|
|
63
|
+
* separate query, so pages could overlap or skip rows (and any concurrent
|
|
64
|
+
* insert/delete would shift offsets and silently drop/duplicate rows in the
|
|
65
|
+
* snapshot). Keyset paging is deterministic under concurrent writes and avoids
|
|
66
|
+
* O(n^2) OFFSET scans on large tables.
|
|
67
|
+
*
|
|
68
|
+
* Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
|
|
69
|
+
* tables are created lazily on first write, so a fresh deployment — or any table
|
|
70
|
+
* never written — would otherwise abort the stream with a raw `no such table`
|
|
71
|
+
* instead of exporting it as empty.
|
|
72
|
+
*/
|
|
61
73
|
declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
|
|
62
74
|
interface ImportGlobalArgs {
|
|
63
75
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
* Optional direct exec handle to the same D1 database the writer targets.
|
|
77
|
+
* When supplied, the conflict pre-probe issues a single
|
|
78
|
+
* `SELECT 1 FROM <table> WHERE id = ? LIMIT 1` against the row's declared
|
|
79
|
+
* table instead of falling back to `writer.get(id)`, which scans every
|
|
80
|
+
* global table looking for the id. Strongly recommended for large schemas
|
|
81
|
+
* — the writer-fallback is O(N tables) per row.
|
|
82
|
+
*/
|
|
71
83
|
exec?: D1ExecLike;
|
|
72
84
|
rows: ReadonlyArray<ExportRow>;
|
|
73
85
|
startLine?: number;
|
|
@@ -77,20 +89,19 @@ interface D1ExecLike {
|
|
|
77
89
|
all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
|
|
78
90
|
}
|
|
79
91
|
/**
|
|
80
|
-
* Import rows into `.global()` tables via the schema-aware D1 writer. The
|
|
81
|
-
* writer rejects unknown ids on `insert` (the writer assigns one when `_id` is
|
|
82
|
-
* absent); we pre-probe each row's `_id` so a collision is reported as a
|
|
83
|
-
* conflict instead of bubbled as a UNIQUE error. Schema-failed rows surface in
|
|
84
|
-
* `errors`; the rest land.
|
|
85
|
-
*/
|
|
92
|
+
* Import rows into `.global()` tables via the schema-aware D1 writer. The
|
|
93
|
+
* writer rejects unknown ids on `insert` (the writer assigns one when `_id` is
|
|
94
|
+
* absent); we pre-probe each row's `_id` so a collision is reported as a
|
|
95
|
+
* conflict instead of bubbled as a UNIQUE error. Schema-failed rows surface in
|
|
96
|
+
* `errors`; the rest land.
|
|
97
|
+
*/
|
|
86
98
|
declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike, args: ImportGlobalArgs) => Promise<ImportResult>;
|
|
87
99
|
/**
|
|
88
|
-
* Minimal structural projection of `D1Database` to keep the adapter
|
|
89
|
-
* compatible with the real workers-types value as well as unit-test doubles.
|
|
90
|
-
*/
|
|
100
|
+
* Minimal structural projection of `D1Database` to keep the adapter
|
|
101
|
+
* compatible with the real workers-types value as well as unit-test doubles.
|
|
102
|
+
*/
|
|
91
103
|
interface D1DatabaseLike {
|
|
92
104
|
batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
|
|
93
|
-
exec?: (sql: string) => Promise<unknown>;
|
|
94
105
|
prepare: (sql: string) => D1PreparedStatementLike;
|
|
95
106
|
withSession: (bookmark?: string) => D1SessionLike;
|
|
96
107
|
}
|
|
@@ -131,67 +142,67 @@ declare class D1Session {
|
|
|
131
142
|
}>;
|
|
132
143
|
first<T = unknown>(sql: string, ...binds: unknown[]): Promise<T | null>;
|
|
133
144
|
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
* Returns the most recent bookmark known to the session, or `undefined`
|
|
146
|
+
* when D1 has not issued one yet.
|
|
147
|
+
*/
|
|
137
148
|
getBookmark(): string | undefined;
|
|
138
149
|
}
|
|
139
150
|
declare class D1Client {
|
|
140
151
|
private readonly db;
|
|
141
152
|
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
* SQL string -> prepared statement. Prepared statements are reusable in
|
|
154
|
+
* D1; preparing the same SQL twice forces the worker to round-trip the
|
|
155
|
+
* statement plan. Caching is per-instance so unit-test isolation holds.
|
|
156
|
+
* Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
|
|
157
|
+
*/
|
|
147
158
|
private readonly stmtCache;
|
|
148
159
|
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
* Lazily-built drizzle handle over the bare binding. Memoised so a single
|
|
161
|
+
* `D1Client` reuses the same dialect/session machinery across calls.
|
|
162
|
+
*/
|
|
152
163
|
private drizzleHandle;
|
|
153
164
|
constructor(database: D1DatabaseLike);
|
|
154
165
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
* Open a Sessions-API scoped session. Pass the bookmark forwarded by
|
|
167
|
+
* the client to opt into read-your-writes consistency.
|
|
168
|
+
*
|
|
169
|
+
* With no bookmark this is the first request of a session — there is no
|
|
170
|
+
* prior write to read, so we open with the explicit `"first-unconstrained"`
|
|
171
|
+
* constraint (Cloudflare's lowest-latency default: the first read may serve
|
|
172
|
+
* from any replica). Read-your-writes for sequenced requests still flows
|
|
173
|
+
* through the forwarded bookmark; a caller needing a strongly-consistent
|
|
174
|
+
* very-first read should pass `"first-primary"` as the bookmark instead.
|
|
175
|
+
*/
|
|
165
176
|
withSession(bookmark?: string): D1Session;
|
|
166
177
|
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
* Prepare a statement, reusing a cached one when the SQL text matches.
|
|
179
|
+
* `bind()` on a prepared statement returns a new bound statement and
|
|
180
|
+
* leaves the underlying prepared plan reusable, so cache hits are safe
|
|
181
|
+
* even when the previous caller already called `.bind(...).run()`.
|
|
182
|
+
*/
|
|
172
183
|
prepare(sql: string): D1PreparedStatementLike;
|
|
173
184
|
/**
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
* Drizzle handle over the bare `env.DB` binding. Used for typed queries
|
|
186
|
+
* against generated `sqliteTable` schemas; does **not** participate in the
|
|
187
|
+
* D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
|
|
188
|
+
* {@link drizzleSession} instead.
|
|
189
|
+
*/
|
|
179
190
|
get drizzle(): DrizzleD1Database<Record<string, unknown>>;
|
|
180
191
|
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
* Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
|
|
193
|
+
* supplied, opts into read-your-writes consistency for follow-up reads on
|
|
194
|
+
* the same session.
|
|
195
|
+
*
|
|
196
|
+
* A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
|
|
197
|
+
* drizzle calls into, so a single `unknown` cast lets us treat the session
|
|
198
|
+
* as a `D1Database` for driver-construction purposes.
|
|
199
|
+
*/
|
|
189
200
|
drizzleSession(bookmark?: string): DrizzleD1Database<Record<string, unknown>>;
|
|
190
201
|
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
* Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
|
|
203
|
+
* exactly; exposed on the client so callers don't need to hold a drizzle
|
|
204
|
+
* handle just to run a typed batch.
|
|
205
|
+
*/
|
|
195
206
|
batch<U extends BatchItem<"sqlite">, T extends Readonly<[U, ...U[]]>>(items: T): Promise<BatchResponse<T>>;
|
|
196
207
|
/** Direct access to the underlying binding (advanced use only). */
|
|
197
208
|
get raw(): D1DatabaseLike;
|
|
@@ -205,24 +216,24 @@ interface GlobalTableInfo {
|
|
|
205
216
|
interface GlobalTablePage {
|
|
206
217
|
columns: string[];
|
|
207
218
|
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
* Foreign-key columns (local column → referenced table) for tables that carry
|
|
220
|
+
* real SQL `REFERENCES` constraints — recovered from `PRAGMA foreign_key_list`.
|
|
221
|
+
* Schema `.global()` tables omit this (their refs come from `describeTables`);
|
|
222
|
+
* external tables (e.g. better-auth's `session`/`twoFactor`) expose it so the
|
|
223
|
+
* schema diagram can draw their global→global FK edges.
|
|
224
|
+
*/
|
|
214
225
|
refs?: Record<string, string>;
|
|
215
226
|
rows: Record<string, unknown>[];
|
|
216
227
|
total: number;
|
|
217
228
|
}
|
|
218
229
|
/**
|
|
219
|
-
* One equality constraint a facet-value click adds to the global browser's view:
|
|
220
|
-
* `column = value` (or `column IS NULL` when `value` is nullish). `column` is a
|
|
221
|
-
* displayed column name, validated against the table's columns and mapped to its
|
|
222
|
-
* physical column (`_id` → `id`) before it is quoted; `value` is the **raw stored
|
|
223
|
-
* value** the facet returned (a SQLite scalar), bound as a parameter and never
|
|
224
|
-
* interpolated. AND-combined with the other clauses.
|
|
225
|
-
*/
|
|
230
|
+
* One equality constraint a facet-value click adds to the global browser's view:
|
|
231
|
+
* `column = value` (or `column IS NULL` when `value` is nullish). `column` is a
|
|
232
|
+
* displayed column name, validated against the table's columns and mapped to its
|
|
233
|
+
* physical column (`_id` → `id`) before it is quoted; `value` is the **raw stored
|
|
234
|
+
* value** the facet returned (a SQLite scalar), bound as a parameter and never
|
|
235
|
+
* interpolated. AND-combined with the other clauses.
|
|
236
|
+
*/
|
|
226
237
|
interface GlobalFilterClause {
|
|
227
238
|
column: string;
|
|
228
239
|
value: unknown;
|
|
@@ -234,13 +245,13 @@ interface ReadGlobalTablePageOptions {
|
|
|
234
245
|
table: string;
|
|
235
246
|
}
|
|
236
247
|
/**
|
|
237
|
-
* Options for {@link facetGlobalColumn} — the read-only "what values does this
|
|
238
|
-
* column hold?" summary for the global (D1) browser. `column` is the displayed
|
|
239
|
-
* column to group by (validated and mapped to its physical column, never
|
|
240
|
-
* interpolated); `filters` mirrors {@link ReadGlobalTablePageOptions}'s eq
|
|
241
|
-
* constraints so the facet reflects the **active view** (the same rows the
|
|
242
|
-
* browser is previewing); `limit` caps the distinct values returned (clamped).
|
|
243
|
-
*/
|
|
248
|
+
* Options for {@link facetGlobalColumn} — the read-only "what values does this
|
|
249
|
+
* column hold?" summary for the global (D1) browser. `column` is the displayed
|
|
250
|
+
* column to group by (validated and mapped to its physical column, never
|
|
251
|
+
* interpolated); `filters` mirrors {@link ReadGlobalTablePageOptions}'s eq
|
|
252
|
+
* constraints so the facet reflects the **active view** (the same rows the
|
|
253
|
+
* browser is previewing); `limit` caps the distinct values returned (clamped).
|
|
254
|
+
*/
|
|
244
255
|
interface FacetGlobalColumnOptions {
|
|
245
256
|
column: string;
|
|
246
257
|
filters?: GlobalFilterClause[];
|
|
@@ -253,43 +264,43 @@ interface GlobalFacetValue {
|
|
|
253
264
|
value: unknown;
|
|
254
265
|
}
|
|
255
266
|
/**
|
|
256
|
-
* Payload of a {@link facetGlobalColumn} call: the top-N distinct `values` (each
|
|
257
|
-
* with a `count`) ordered by frequency, plus `truncated` — `true` when more
|
|
258
|
-
* distinct values existed beyond the cap, so the UI can say so rather than imply
|
|
259
|
-
* the list is exhaustive. Mirrors the shard browser's `FacetColumnResult`.
|
|
260
|
-
*/
|
|
267
|
+
* Payload of a {@link facetGlobalColumn} call: the top-N distinct `values` (each
|
|
268
|
+
* with a `count`) ordered by frequency, plus `truncated` — `true` when more
|
|
269
|
+
* distinct values existed beyond the cap, so the UI can say so rather than imply
|
|
270
|
+
* the list is exhaustive. Mirrors the shard browser's `FacetColumnResult`.
|
|
271
|
+
*/
|
|
261
272
|
interface GlobalFacetResult {
|
|
262
273
|
truncated: boolean;
|
|
263
274
|
values: GlobalFacetValue[];
|
|
264
275
|
}
|
|
265
276
|
/**
|
|
266
|
-
* List every browsable D1 table with its row count, ordered by name. Surfaces
|
|
267
|
-
* both the schema's `.global()` tables (provisioned first) and external tables
|
|
268
|
-
* (auth, etc.); internal/companion tables are excluded.
|
|
269
|
-
*/
|
|
277
|
+
* List every browsable D1 table with its row count, ordered by name. Surfaces
|
|
278
|
+
* both the schema's `.global()` tables (provisioned first) and external tables
|
|
279
|
+
* (auth, etc.); internal/companion tables are excluded.
|
|
280
|
+
*/
|
|
270
281
|
declare const listGlobalTables: (exec: SqlCtxExec, schema: SchemaLike) => Promise<GlobalTableInfo[]>;
|
|
271
282
|
/**
|
|
272
|
-
* Read a page of rows from one D1 table. The table is validated against the live
|
|
273
|
-
* browsable-table list before its name is interpolated, so this can't be coerced
|
|
274
|
-
* into reading an internal table or injecting SQL. `limit` is clamped to
|
|
275
|
-
* `[1, 500]`; `offset` floors at `0`. `filters` AND-narrows the page to rows
|
|
276
|
-
* matching each `column = value` eq constraint (a facet-value drill-down), bound
|
|
277
|
-
* through {@link buildEqPredicate} so they never inject SQL.
|
|
278
|
-
*/
|
|
283
|
+
* Read a page of rows from one D1 table. The table is validated against the live
|
|
284
|
+
* browsable-table list before its name is interpolated, so this can't be coerced
|
|
285
|
+
* into reading an internal table or injecting SQL. `limit` is clamped to
|
|
286
|
+
* `[1, 500]`; `offset` floors at `0`. `filters` AND-narrows the page to rows
|
|
287
|
+
* matching each `column = value` eq constraint (a facet-value drill-down), bound
|
|
288
|
+
* through {@link buildEqPredicate} so they never inject SQL.
|
|
289
|
+
*/
|
|
279
290
|
declare const readGlobalTablePage: (exec: SqlCtxExec, schema: SchemaLike, options: ReadGlobalTablePageOptions) => Promise<GlobalTablePage>;
|
|
280
291
|
/**
|
|
281
|
-
* Summarise the distinct values of one displayed column over the **active view**
|
|
282
|
-
* (the same eq `filters` the global browser is previewing) — the D1 twin of the
|
|
283
|
-
* shard browser's `facetColumn`. Read-only: a `SELECT col AS value, COUNT(*) AS
|
|
284
|
-
* count … GROUP BY col ORDER BY count DESC LIMIT N+1`, with the column validated
|
|
285
|
-
* against the table's displayed columns (typed 404 if unknown), mapped to its
|
|
286
|
-
* physical column, and quoted — never interpolated from caller input. The extra
|
|
287
|
-
* over-fetched row is dropped and surfaced as `truncated`. A sensitive column on
|
|
288
|
-
* an external (non-schema) table is never grouped — it collapses to a single
|
|
289
|
-
* redacted `•••` bucket — mirroring the page browser's value redaction so the
|
|
290
|
-
* facet can't leak credentials. The returned `value` is the raw stored scalar, so
|
|
291
|
-
* a click feeds it straight back as an eq filter.
|
|
292
|
-
*/
|
|
292
|
+
* Summarise the distinct values of one displayed column over the **active view**
|
|
293
|
+
* (the same eq `filters` the global browser is previewing) — the D1 twin of the
|
|
294
|
+
* shard browser's `facetColumn`. Read-only: a `SELECT col AS value, COUNT(*) AS
|
|
295
|
+
* count … GROUP BY col ORDER BY count DESC LIMIT N+1`, with the column validated
|
|
296
|
+
* against the table's displayed columns (typed 404 if unknown), mapped to its
|
|
297
|
+
* physical column, and quoted — never interpolated from caller input. The extra
|
|
298
|
+
* over-fetched row is dropped and surfaced as `truncated`. A sensitive column on
|
|
299
|
+
* an external (non-schema) table is never grouped — it collapses to a single
|
|
300
|
+
* redacted `•••` bucket — mirroring the page browser's value redaction so the
|
|
301
|
+
* facet can't leak credentials. The returned `value` is the raw stored scalar, so
|
|
302
|
+
* a click feeds it straight back as an eq filter.
|
|
303
|
+
*/
|
|
293
304
|
declare const facetGlobalColumn: (exec: SqlCtxExec, schema: SchemaLike, options: FacetGlobalColumnOptions) => Promise<GlobalFacetResult>;
|
|
294
305
|
interface Migration {
|
|
295
306
|
/** Human-readable name, e.g. `001_init` (used in logs). */
|
|
@@ -310,20 +321,20 @@ interface MigrationRunnerResult {
|
|
|
310
321
|
}[];
|
|
311
322
|
}
|
|
312
323
|
/**
|
|
313
|
-
* Sequentially applies pending migrations against a D1 database via the
|
|
314
|
-
* drizzle-orm/d1 driver. Each migration is hashed (SHA-256 over its SQL
|
|
315
|
-
* text); the hash is stored in `__drizzle_migrations`, so re-applying the
|
|
316
|
-
* same SQL under a different `version` is rejected and identical migrations
|
|
317
|
-
* are skipped idempotently.
|
|
318
|
-
*/
|
|
324
|
+
* Sequentially applies pending migrations against a D1 database via the
|
|
325
|
+
* drizzle-orm/d1 driver. Each migration is hashed (SHA-256 over its SQL
|
|
326
|
+
* text); the hash is stored in `__drizzle_migrations`, so re-applying the
|
|
327
|
+
* same SQL under a different `version` is rejected and identical migrations
|
|
328
|
+
* are skipped idempotently.
|
|
329
|
+
*/
|
|
319
330
|
declare class MigrationRunner {
|
|
320
331
|
private readonly client;
|
|
321
332
|
private readonly migrations;
|
|
322
333
|
/**
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
* Accepts either a {@link D1Client} (preferred — gets typed batches +
|
|
335
|
+
* drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
|
|
336
|
+
* the caller's behalf so existing `@lunora/cli` callers keep working).
|
|
337
|
+
*/
|
|
327
338
|
constructor(database: D1Client | D1DatabaseLike, migrations: Migration[]);
|
|
328
339
|
run(): Promise<MigrationRunnerResult>;
|
|
329
340
|
private applyOne;
|
|
@@ -331,9 +342,9 @@ declare class MigrationRunner {
|
|
|
331
342
|
private assertUniqueSql;
|
|
332
343
|
}
|
|
333
344
|
/**
|
|
334
|
-
* The canonical SQLite dialect: column affinities, the shared SQLite value
|
|
335
|
-
* codec, `RETURNING` support (both D1 and `node:sqlite`), and `sqlite_master`
|
|
336
|
-
* table probing. The rest of the per-statement shaping is drizzle's.
|
|
337
|
-
*/
|
|
345
|
+
* The canonical SQLite dialect: column affinities, the shared SQLite value
|
|
346
|
+
* codec, `RETURNING` support (both D1 and `node:sqlite`), and `sqlite_master`
|
|
347
|
+
* table probing. The rest of the per-statement shaping is drizzle's.
|
|
348
|
+
*/
|
|
338
349
|
declare const sqliteDialect: SqlDialect;
|
|
339
|
-
export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1DatabaseLike, type D1PreparedStatementLike, D1Session, type D1SessionLike, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, listGlobalTables, readD1CdcChanges, readGlobalTablePage, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges };
|
|
350
|
+
export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1DatabaseLike, type D1PreparedStatementLike, D1Session, type D1SessionLike, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, listGlobalTables, readD1CdcChanges, readGlobalTablePage, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { D1Client, D1Session } from './packem_shared/D1Client-DA3flo1o.mjs';
|
|
3
|
-
export { createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges } from './packem_shared/createD1CtxDb-BMR8J0dT.mjs';
|
|
4
|
-
export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-C6u_WMIY.mjs';
|
|
5
|
-
export { MigrationRunner } from './packem_shared/MigrationRunner-BkEwQ-Ya.mjs';
|
|
6
|
-
export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
|
|
7
|
-
export { createSqlCtxDb } from '@lunora/sql-store';
|
|
1
|
+
import{exportGlobalRows as a,importGlobalRows as o,selectGlobalTables as l}from"./packem_shared/exportGlobalRows-CmcmGZxv.mjs";import{D1Client as i,D1Session as n}from"./packem_shared/D1Client-B9m0fFLr.mjs";import{backfillD1SearchIndexes as b,createD1CtxDb as D,readD1CdcChanges as g,runD1AggregateMigrations as c,runD1CdcMigration as x,runD1GlobalTableMigrations as f,runD1RankMigrations as m,runD1SearchMigrations as p,trimD1CdcChanges as C}from"./packem_shared/backfillD1SearchIndexes-ERQf6szv.mjs";import{facetGlobalColumn as d,listGlobalTables as G,readGlobalTablePage as M}from"./packem_shared/facetGlobalColumn-R-BETSuA.mjs";import{MigrationRunner as R}from"./packem_shared/MigrationRunner-DvnPBDVT.mjs";import{default as T}from"./packem_shared/sqliteDialect-B43yA0ek.mjs";import{createSqlCtxDb as q}from"@lunora/sql-store";export{i as D1Client,n as D1Session,R as MigrationRunner,b as backfillD1SearchIndexes,D as createD1CtxDb,q as createSqlCtxDb,a as exportGlobalRows,d as facetGlobalColumn,o as importGlobalRows,G as listGlobalTables,g as readD1CdcChanges,M as readGlobalTablePage,c as runD1AggregateMigrations,x as runD1CdcMigration,f as runD1GlobalTableMigrations,m as runD1RankMigrations,p as runD1SearchMigrations,l as selectGlobalTables,T as sqliteDialect,C as trimD1CdcChanges};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{drizzle as n}from"drizzle-orm/d1";const d=(t,e)=>{if(t.size<e)return;const s=t.keys().next().value;s!==void 0&&t.delete(s)},l=256,o=(t,e,s)=>{const r=t.get(s);if(r)return t.delete(s),t.set(s,r),r;const i=e(s);return d(t,l),t.set(s,i),i},a="first-unconstrained";class h{session;stmtCache=new Map;constructor(e){this.session=e}prepare(e){return o(this.stmtCache,s=>this.session.prepare(s),e)}async run(e,...s){return this.prepare(e).bind(...s).run()}async all(e,...s){return this.prepare(e).bind(...s).all()}async first(e,...s){return this.prepare(e).bind(...s).first()}getBookmark(){return this.session.getBookmark()??void 0}}class u{db;stmtCache=new Map;drizzleHandle;constructor(e){this.db=e}withSession(e){const s=this.db.withSession(e??a);return new h(s)}prepare(e){return o(this.stmtCache,s=>this.db.prepare(s),e)}get drizzle(){return this.drizzleHandle?this.drizzleHandle:(this.drizzleHandle=n(this.db,{logger:!1}),this.drizzleHandle)}drizzleSession(e){const s=this.db.withSession(e??a);return n(s,{logger:!1})}async batch(e){return this.drizzle.batch(e)}get raw(){return this.db}}export{u as D1Client,h as D1Session};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{sql as h}from"drizzle-orm";import{D1Client as m}from"./D1Client-B9m0fFLr.mjs";const f="__drizzle_migrations",w=`CREATE TABLE IF NOT EXISTS ${f} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`,d=/\s/u,g=/^[0-9a-f]{64}$/u,p=new RegExp(String.raw`UNIQUE constraint failed:\s*${f}\.hash`,"iu"),E=c=>{const n=c.sql;let i=!1,e=!1,r=!1,o=!1,a;for(let t=0;t<n.length;t+=1){const s=n[t],l=n[t+1];if(r){s===`
|
|
2
|
+
`&&(r=!1);continue}if(o){s==="*"&&l==="/"&&(o=!1,t+=1);continue}if(i){s==="'"&&(l==="'"?t+=1:i=!1);continue}if(e){s==='"'&&(l==='"'?t+=1:e=!1);continue}if(s==="-"&&l==="-"){r=!0,t+=1;continue}if(s==="/"&&l==="*"){o=!0,t+=1;continue}if(a!==void 0){if(s!==void 0&&!d.test(s))throw new u("INTERNAL",`Migration "${c.name}" (v${String(c.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`);continue}if(s==="'"){i=!0;continue}if(s==='"'){e=!0;continue}s===";"&&(a=t)}return a},S=async c=>{const n=new TextEncoder().encode(c),i=await crypto.subtle.digest("SHA-256",n);return[...new Uint8Array(i)].map(e=>e.toString(16).padStart(2,"0")).join("")};class I{client;migrations;constructor(n,i){this.client=n instanceof m?n:new m(n),this.migrations=[...i].toSorted((e,r)=>e.version-r.version),this.assertUniqueVersions(),this.assertUniqueSql()}async run(){await this.client.drizzle.run(h.raw(w));const n=await this.client.drizzle.all(h.raw(`SELECT hash FROM ${f}`)),i=new Set(n.map(a=>a.hash)),e=[],r=[],o=await Promise.all(this.migrations.map(async a=>S(a.sql)));for(const[a,t]of this.migrations.entries()){const s=o[a];if(i.has(s)){r.push({name:t.name,version:t.version});continue}await this.applyOne(t,s)?e.push({name:t.name,version:t.version}):r.push({name:t.name,version:t.version})}return{applied:e,skipped:r}}async applyOne(n,i){const e=E(n),r=(e===void 0?n.sql:n.sql.slice(0,e)).trim();if(!g.test(i))throw new u("INTERNAL",`migration "${n.name}" produced a non-hex hash; refusing to inline into SQL`);const o=`INSERT INTO ${f} (hash, created_at) VALUES ('${i}', ${String(Date.now())})`,a=[this.client.drizzle.run(h.raw(r)),this.client.drizzle.run(h.raw(o))];try{await this.client.batch(a)}catch(t){if(p.test(t instanceof Error?t.message:String(t)))return!1;throw t}return!0}assertUniqueVersions(){const n=new Set;for(const i of this.migrations){if(n.has(i.version))throw new u("INTERNAL",`Duplicate migration version ${String(i.version)}`);n.add(i.version)}}assertUniqueSql(){const n=new Map;for(const i of this.migrations){const e=n.get(i.sql);if(e!==void 0)throw new u("INTERNAL",`Migrations ${String(e)} and ${String(i.version)} have identical SQL — bump the content, not just the version.`);n.set(i.sql,i.version)}}}export{I as MigrationRunner};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{backfillSqlSearchIndexes as n,createSqlCtxDb as i,readSqlCdcChanges as t,runSqlAggregateMigrations as s,runSqlCdcMigration as l,runSqlGlobalTableMigrations as o,runSqlRankMigrations as g,runSqlSearchMigrations as c,trimSqlCdcChanges as d}from"@lunora/sql-store";import{createSqlCtxDb as G,decodeGlobalRow as R}from"@lunora/sql-store";import e from"./sqliteDialect-B43yA0ek.mjs";const D=a=>i({...a,dialect:e}),b=(a,r)=>o(a,r,e),q=(a,r)=>s(a,r,e),u=(a,r)=>g(a,r,e),M=(a,r)=>c(a,r,e),h=(a,r)=>n(a,r,e),m=a=>l(a,e),x=(a,r={})=>t(a,r,e),f=(a,r)=>d(a,r,e);export{h as backfillD1SearchIndexes,D as createD1CtxDb,G as createSqlCtxDb,R as decodeGlobalRow,x as readD1CdcChanges,q as runD1AggregateMigrations,m as runD1CdcMigration,b as runD1GlobalTableMigrations,u as runD1RankMigrations,M as runD1SearchMigrations,f as trimD1CdcChanges};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toErrorBody as u}from"@lunora/errors";import{runD1GlobalTableMigrations as m}from"./backfillD1SearchIndexes-ERQf6szv.mjs";import{quoteIdentifier as f}from"./quoteIdentifier-CObIFRhb.mjs";import{decodeGlobalRow as E}from"@lunora/sql-store";const R=200,g=(o,t)=>{const r=e=>o.tables[e]?.shardMode?.kind==="global";return t&&t.length>0?t.filter(e=>r(e)):Object.keys(o.tables).filter(e=>r(e))},k=(o,t,r)=>{const e=o.tables[t];return e?E(e,r):{_creationTime:r._creationTime,_id:r.id}},O=async function*(o,t,r){const e=g(t,r.tables),a=r.batchSize??R;await m(o,t);for(const i of e){const n=f(i);let c,s=!0;for(;s;){const l=c===void 0?await o.all(`SELECT * FROM ${n} ORDER BY "id" LIMIT ?`,[a]):await o.all(`SELECT * FROM ${n} WHERE "id" > ? ORDER BY "id" LIMIT ?`,[c,a]);for(const b of l)yield{doc:k(t,i,b),table:i};const d=l.at(-1);d!==void 0&&(c=String(d.id)),s=l.length===a}}},w=(o,t,r)=>{const e=o.tables[t];if(!e)return`unknown table: ${t}`;for(const[a,i]of Object.entries(e.shape)){const n=r[a],c=i.kind==="optional";if(n===void 0&&c)continue;const s=i.parse;if(typeof s=="function")try{s(n)}catch(l){const d=l instanceof Error?l.message:String(l);return`field "${a}": ${d}`}}},y=async(o,t,r,e)=>{try{return t?(await t.all(`SELECT 1 AS hit FROM ${f(r)} WHERE "id" = ? LIMIT 1`,[e])).length>0:await o.get(e)!==null}catch{return!1}},p=async(o,t,r,e,a)=>{const{doc:i,table:n}=e;if(t.tables[n]?.shardMode?.kind!=="global")return{kind:"skip"};if(!i||typeof i!="object"||Array.isArray(i))return{error:{code:"BAD_ROW",line:a,message:"row is missing or malformed `doc`",table:n},kind:"error"};const c=w(t,n,i);if(c!==void 0)return{error:{code:"VALIDATION_ERROR",line:a,message:c,table:n},kind:"error"};const s=typeof i._id=="string"?i._id:void 0;if(s!==void 0&&await y(o,r.exec,n,s))return{kind:"conflict"};try{return await o.insert(n,i,{allowExplicitId:!0}),{inserted:n,kind:"inserted"}}catch(l){const{body:d}=u(l,{fallbackCode:"INSERT_FAILED"});return{error:{code:d.code,line:a,message:d.message,table:n},kind:"error"}}},M=async(o,t,r)=>{const e=[],a={};let i=0,n=(r.startLine??1)-1;for(const c of r.rows){n+=1;const s=await p(o,t,r,c,n);switch(s.kind){case"conflict":{i+=1;break}case"error":{e.push(s.error);break}case"inserted":{a[s.inserted]=(a[s.inserted]??0)+1;break}}}return{conflicts:i,errors:e,inserted:a}};export{O as exportGlobalRows,M as importGlobalRows,g as selectGlobalTables};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as _}from"@lunora/errors";import{runD1GlobalTableMigrations as T}from"./backfillD1SearchIndexes-ERQf6szv.mjs";import{quoteIdentifier as i}from"./quoteIdentifier-CObIFRhb.mjs";import{decodeGlobalRow as S}from"@lunora/sql-store";const E=(t,a)=>T(t,a),L=50,g=500,y=30,C=200,O=(t,a,o)=>Math.min(Math.max(t,a),o),A=/^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u,U=t=>A.test(t),N=/password|secret|token|hash|salt|credential/iu,p=async t=>(await t.all("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",[])).map(a=>String(a.name)).filter(a=>!U(a)),$=async(t,a,o="",n=[])=>{const s=await t.all(`SELECT COUNT(*) AS c FROM ${a}${o}`,n);return Number(s[0]?.c??0)},v=(t,a,o)=>t.tables[a]!==void 0&&o==="_id"?"id":o,M=(t,a,o,n)=>{if(n===void 0||n.length===0)return;const s=[],l=[];for(const e of n){if(!o.includes(e.column))throw new _("UNKNOWN_COLUMN",`unknown column: ${e.column}`,{status:404});if(t.tables[a]===void 0&&N.test(e.column))throw new _("FORBIDDEN",`cannot filter on a redacted column: ${e.column}`,{status:403});const r=i(v(t,a,e.column));e.value===null||e.value===void 0?s.push(`${r} IS NULL`):(s.push(`${r} = ?`),l.push(e.value))}return{params:l,where:s.join(" AND ")}},G=(t,a,o)=>{const n=t.tables[a];if(n)return S(n,o);const s={};for(const[l,e]of Object.entries(o))s[l]=e!=null&&N.test(l)?"•••":e;return s},R=async(t,a,o)=>{const n=a.tables[o];return n?["_id","_creationTime",...Object.keys(n.shape)]:(await t.all(`PRAGMA table_info(${i(o)})`,[])).map(s=>String(s.name))},k=async(t,a,o)=>{if(a.tables[o])return;const n=await t.all(`PRAGMA foreign_key_list(${i(o)})`,[]);if(n.length===0)return;const s={};for(const l of n){const e=String(l.from),r=String(l.table);s[e]??=r}return s},B=async(t,a)=>{await E(t,a);const o=await p(t);return Promise.all(o.map(async n=>({name:n,rowCount:await $(t,i(n))})))},P=async(t,a,o)=>{const{table:n}=o;if(await E(t,a),!(await p(t)).includes(n))throw new _("UNKNOWN_TABLE",`unknown table: ${n}`,{status:404});const s=O(Math.trunc(o.limit??L),1,g),l=Math.max(0,Math.trunc(o.offset??0)),e=i(n),r=await R(t,a,n),u=M(a,n,r,o.filters),w=u===void 0?"":` WHERE ${u.where}`,c=u?.params??[],f=await $(t,e,w,c),m=(await t.all(`SELECT * FROM ${e}${w} LIMIT ? OFFSET ?`,[...c,s,l])).map(h=>G(a,n,h)),d=await k(t,a,n);return d===void 0?{columns:r,rows:m,total:f}:{columns:r,refs:d,rows:m,total:f}},K=async(t,a,o)=>{const{column:n,table:s}=o;if(await E(t,a),!(await p(t)).includes(s))throw new _("UNKNOWN_TABLE",`unknown table: ${s}`,{status:404});const l=await R(t,a,s);if(!l.includes(n))throw new _("UNKNOWN_COLUMN",`unknown column: ${n}`,{status:404});const e=i(s),r=M(a,s,l,o.filters),u=r===void 0?"":` WHERE ${r.where}`,w=r?.params??[];if(a.tables[s]===void 0&&N.test(n)){const b=await $(t,e,u,w);return{truncated:!1,values:b===0?[]:[{count:b,value:"•••"}]}}const c=O(Math.trunc(o.limit??y),1,C),f=i(v(a,s,n)),m=await t.all(`SELECT ${f} AS value, COUNT(*) AS count FROM ${e}${u} GROUP BY ${f} ORDER BY count DESC LIMIT ?`,[...w,c+1]),d=m.length>c,h=d?m.slice(0,c):m;return{truncated:d,values:h.map(b=>({count:Number(b.count),value:b.value}))}};export{K as facetGlobalColumn,B as listGlobalTables,P as readGlobalTablePage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=e=>`"${e.replaceAll('"','""')}"`;export{t as quoteIdentifier};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{sqliteEncode as i,sqliteDecode as n}from"@lunora/sql-store";import{sql as o}from"drizzle-orm";import{sqlAffinityForKind as r}from"../dialect.mjs";const s=/unique constraint failed/iu,T={companionTypes:{autoincrementPrimaryKey:"INTEGER PRIMARY KEY AUTOINCREMENT",integer:"INTEGER",key:"TEXT",real:"REAL",text:"TEXT"},columnType:e=>r(e),decode:(e,t)=>n(e,t),encode:e=>i(e),frameworkColumns:()=>[{name:"id",type:"TEXT PRIMARY KEY"},{name:"_creationTime",type:"REAL NOT NULL"}],isUniqueViolation:e=>e instanceof Error&&s.test(e.message),name:"sqlite",supportsFts5:!0,supportsReturning:!0,tableExists:e=>o`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${e}`};export{T as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/d1",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.50",
|
|
4
4
|
"description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/d1"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist",
|
|
28
|
+
"./dist",
|
|
29
29
|
"README.md",
|
|
30
30
|
"LICENSE.md",
|
|
31
31
|
"__assets__"
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/do": "1.0.0-alpha.
|
|
54
|
-
"@lunora/
|
|
53
|
+
"@lunora/do": "1.0.0-alpha.54",
|
|
54
|
+
"@lunora/errors": "1.0.0-alpha.9",
|
|
55
|
+
"@lunora/sql-store": "1.0.0-alpha.51",
|
|
55
56
|
"drizzle-orm": "^0.45.2"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/d1';
|
|
2
|
-
|
|
3
|
-
const STMT_CACHE_CAPACITY = 256;
|
|
4
|
-
const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
|
|
5
|
-
class D1Session {
|
|
6
|
-
session;
|
|
7
|
-
/** See {@link D1Client.stmtCache}. Scoped per session. */
|
|
8
|
-
stmtCache = /* @__PURE__ */ new Map();
|
|
9
|
-
constructor(session) {
|
|
10
|
-
this.session = session;
|
|
11
|
-
}
|
|
12
|
-
prepare(sql) {
|
|
13
|
-
const cached = this.stmtCache.get(sql);
|
|
14
|
-
if (cached) {
|
|
15
|
-
this.stmtCache.delete(sql);
|
|
16
|
-
this.stmtCache.set(sql, cached);
|
|
17
|
-
return cached;
|
|
18
|
-
}
|
|
19
|
-
const stmt = this.session.prepare(sql);
|
|
20
|
-
if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
|
|
21
|
-
const oldest = this.stmtCache.keys().next().value;
|
|
22
|
-
if (oldest !== void 0) {
|
|
23
|
-
this.stmtCache.delete(oldest);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
this.stmtCache.set(sql, stmt);
|
|
27
|
-
return stmt;
|
|
28
|
-
}
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
|
|
30
|
-
async run(sql, ...binds) {
|
|
31
|
-
return this.prepare(sql).bind(...binds).run();
|
|
32
|
-
}
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
|
|
34
|
-
async all(sql, ...binds) {
|
|
35
|
-
return this.prepare(sql).bind(...binds).all();
|
|
36
|
-
}
|
|
37
|
-
async first(sql, ...binds) {
|
|
38
|
-
return this.prepare(sql).bind(...binds).first();
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Returns the most recent bookmark known to the session, or `undefined`
|
|
42
|
-
* when D1 has not issued one yet.
|
|
43
|
-
*/
|
|
44
|
-
getBookmark() {
|
|
45
|
-
return this.session.getBookmark() ?? void 0;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
class D1Client {
|
|
49
|
-
db;
|
|
50
|
-
/**
|
|
51
|
-
* SQL string -> prepared statement. Prepared statements are reusable in
|
|
52
|
-
* D1; preparing the same SQL twice forces the worker to round-trip the
|
|
53
|
-
* statement plan. Caching is per-instance so unit-test isolation holds.
|
|
54
|
-
* Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
|
|
55
|
-
*/
|
|
56
|
-
stmtCache = /* @__PURE__ */ new Map();
|
|
57
|
-
/**
|
|
58
|
-
* Lazily-built drizzle handle over the bare binding. Memoised so a single
|
|
59
|
-
* `D1Client` reuses the same dialect/session machinery across calls.
|
|
60
|
-
*/
|
|
61
|
-
drizzleHandle;
|
|
62
|
-
constructor(database) {
|
|
63
|
-
this.db = database;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Open a Sessions-API scoped session. Pass the bookmark forwarded by
|
|
67
|
-
* the client to opt into read-your-writes consistency.
|
|
68
|
-
*
|
|
69
|
-
* With no bookmark this is the first request of a session — there is no
|
|
70
|
-
* prior write to read, so we open with the explicit `"first-unconstrained"`
|
|
71
|
-
* constraint (Cloudflare's lowest-latency default: the first read may serve
|
|
72
|
-
* from any replica). Read-your-writes for sequenced requests still flows
|
|
73
|
-
* through the forwarded bookmark; a caller needing a strongly-consistent
|
|
74
|
-
* very-first read should pass `"first-primary"` as the bookmark instead.
|
|
75
|
-
*/
|
|
76
|
-
withSession(bookmark) {
|
|
77
|
-
const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
|
|
78
|
-
return new D1Session(session);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Prepare a statement, reusing a cached one when the SQL text matches.
|
|
82
|
-
* `bind()` on a prepared statement returns a new bound statement and
|
|
83
|
-
* leaves the underlying prepared plan reusable, so cache hits are safe
|
|
84
|
-
* even when the previous caller already called `.bind(...).run()`.
|
|
85
|
-
*/
|
|
86
|
-
prepare(sql) {
|
|
87
|
-
const cached = this.stmtCache.get(sql);
|
|
88
|
-
if (cached) {
|
|
89
|
-
this.stmtCache.delete(sql);
|
|
90
|
-
this.stmtCache.set(sql, cached);
|
|
91
|
-
return cached;
|
|
92
|
-
}
|
|
93
|
-
const stmt = this.db.prepare(sql);
|
|
94
|
-
if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
|
|
95
|
-
const oldest = this.stmtCache.keys().next().value;
|
|
96
|
-
if (oldest !== void 0) {
|
|
97
|
-
this.stmtCache.delete(oldest);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.stmtCache.set(sql, stmt);
|
|
101
|
-
return stmt;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Drizzle handle over the bare `env.DB` binding. Used for typed queries
|
|
105
|
-
* against generated `sqliteTable` schemas; does **not** participate in the
|
|
106
|
-
* D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
|
|
107
|
-
* {@link drizzleSession} instead.
|
|
108
|
-
*/
|
|
109
|
-
get drizzle() {
|
|
110
|
-
if (this.drizzleHandle) {
|
|
111
|
-
return this.drizzleHandle;
|
|
112
|
-
}
|
|
113
|
-
this.drizzleHandle = drizzle(this.db, { logger: false });
|
|
114
|
-
return this.drizzleHandle;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
|
|
118
|
-
* supplied, opts into read-your-writes consistency for follow-up reads on
|
|
119
|
-
* the same session.
|
|
120
|
-
*
|
|
121
|
-
* A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
|
|
122
|
-
* drizzle calls into, so a single `unknown` cast lets us treat the session
|
|
123
|
-
* as a `D1Database` for driver-construction purposes.
|
|
124
|
-
*/
|
|
125
|
-
drizzleSession(bookmark) {
|
|
126
|
-
const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
|
|
127
|
-
return drizzle(session, { logger: false });
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
|
|
131
|
-
* exactly; exposed on the client so callers don't need to hold a drizzle
|
|
132
|
-
* handle just to run a typed batch.
|
|
133
|
-
*/
|
|
134
|
-
async batch(items) {
|
|
135
|
-
return this.drizzle.batch(items);
|
|
136
|
-
}
|
|
137
|
-
/** Direct access to the underlying binding (advanced use only). */
|
|
138
|
-
get raw() {
|
|
139
|
-
return this.db;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export { D1Client, D1Session };
|