@gobing-ai/ts-db 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.
Files changed (55) hide show
  1. package/README.md +326 -0
  2. package/dist/adapter.d.ts +86 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +18 -0
  5. package/dist/adapters/bun-sqlite.d.ts +40 -0
  6. package/dist/adapters/bun-sqlite.d.ts.map +1 -0
  7. package/dist/adapters/bun-sqlite.js +70 -0
  8. package/dist/adapters/d1.d.ts +48 -0
  9. package/dist/adapters/d1.d.ts.map +1 -0
  10. package/dist/adapters/d1.js +45 -0
  11. package/dist/base-dao.d.ts +27 -0
  12. package/dist/base-dao.d.ts.map +1 -0
  13. package/dist/base-dao.js +34 -0
  14. package/dist/embedded-migrations.d.ts +15 -0
  15. package/dist/embedded-migrations.d.ts.map +1 -0
  16. package/dist/embedded-migrations.js +25 -0
  17. package/dist/entity-dao.d.ts +143 -0
  18. package/dist/entity-dao.d.ts.map +1 -0
  19. package/dist/entity-dao.js +218 -0
  20. package/dist/index.d.ts +12 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +10 -0
  23. package/dist/index.js.map +9 -0
  24. package/dist/migrate.d.ts +38 -0
  25. package/dist/migrate.d.ts.map +1 -0
  26. package/dist/migrate.js +131 -0
  27. package/dist/queue-job-dao.d.ts +95 -0
  28. package/dist/queue-job-dao.d.ts.map +1 -0
  29. package/dist/queue-job-dao.js +211 -0
  30. package/dist/schema/common.d.ts +87 -0
  31. package/dist/schema/common.d.ts.map +1 -0
  32. package/dist/schema/common.js +76 -0
  33. package/dist/schema/index.d.ts +3 -0
  34. package/dist/schema/index.d.ts.map +1 -0
  35. package/dist/schema/index.js +2 -0
  36. package/dist/schema/queue-jobs.d.ts +225 -0
  37. package/dist/schema/queue-jobs.d.ts.map +1 -0
  38. package/dist/schema/queue-jobs.js +18 -0
  39. package/dist/span-context.d.ts +2 -0
  40. package/dist/span-context.d.ts.map +1 -0
  41. package/dist/span-context.js +0 -0
  42. package/package.json +47 -0
  43. package/src/adapter.ts +109 -0
  44. package/src/adapters/bun-sqlite.ts +108 -0
  45. package/src/adapters/d1.ts +76 -0
  46. package/src/base-dao.ts +37 -0
  47. package/src/embedded-migrations.ts +32 -0
  48. package/src/entity-dao.ts +290 -0
  49. package/src/index.ts +19 -0
  50. package/src/migrate.ts +163 -0
  51. package/src/queue-job-dao.ts +317 -0
  52. package/src/schema/common.ts +94 -0
  53. package/src/schema/index.ts +2 -0
  54. package/src/schema/queue-jobs.ts +23 -0
  55. package/src/span-context.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # @gobing-ai/ts-db
2
+
3
+ Database abstraction layer — adapter pattern with Drizzle ORM, generic CRUD DAOs, job queue persistence, and migration tooling. Supports Bun SQLite (in-memory and file-based) and Cloudflare D1.
4
+
5
+ ## Overview
6
+
7
+ `ts-db` provides a typed database layer decoupled from any specific storage engine. The `DbAdapter` interface abstracts Bun SQLite and Cloudflare D1 behind a common API, while `BaseDao`, `EntityDao`, and `QueueJobDao` provide progressively richer data access patterns.
8
+
9
+ | Component | Purpose |
10
+ |-----------|---------|
11
+ | `DbAdapter` | Unified interface across Bun SQLite and D1 |
12
+ | `BunSqliteAdapter` | Bun SQLite implementation with statement caching and WAL pragmas |
13
+ | `D1Adapter` | Cloudflare D1 implementation (no `@cloudflare/workers-types` dependency) |
14
+ | `BaseDao` | Transaction + timestamp utilities for all DAOs |
15
+ | `EntityDao` | Generic CRUD with soft delete, pagination, and `count()` |
16
+ | `QueueJobDao` | Job queue persistence — `enqueue`, `claimReady`, `markCompleted`, `failExpiredJobs` |
17
+ | `applyMigrations` | Drizzle migration runner (file-based + embedded fallback) |
18
+ | `schema` | Reusable Drizzle column helpers + `queue_jobs` table definition |
19
+ | `SpanContext` | Re-exported from `@gobing-ai/ts-runtime` for telemetry |
20
+
21
+ ## Architecture
22
+
23
+ ```mermaid
24
+ classDiagram
25
+ class DbAdapter {
26
+ <<interface>>
27
+ +getDb() DbClient
28
+ +exec(sql) void
29
+ +run(sql, ...params) void
30
+ +queryFirst(sql, ...params) T?
31
+ +queryAll(sql, ...params) T[]
32
+ +close() void
33
+ }
34
+
35
+ class BunSqliteAdapter {
36
+ -Database sqlite
37
+ -drizzleDb
38
+ -stmtCache
39
+ +getDrizzleDb()
40
+ }
41
+
42
+ class D1Adapter {
43
+ -binding
44
+ -drizzleDb
45
+ +getBinding()
46
+ }
47
+
48
+ class BaseDao {
49
+ <<abstract>>
50
+ #db
51
+ +now() number
52
+ +withTransaction(fn) T
53
+ }
54
+
55
+ class EntityDao {
56
+ +create(data) TSelect
57
+ +findById(id) TSelect?
58
+ +findAll() TSelect[]
59
+ +update(id, data) TSelect?
60
+ +delete(id, soft?) TSelect?
61
+ +findBy(column, value) TSelect?
62
+ +findAllBy(column, value) TSelect[]
63
+ +list(opts) TSelect[]
64
+ +count(where?) number
65
+ #hasSoftDelete boolean
66
+ #activeCondition SQL?
67
+ }
68
+
69
+ class QueueJobDao {
70
+ +enqueue(type, payload, opts?) string
71
+ +enqueueBatch(jobs) string[]
72
+ +claimReady(batchSize) QueueJobRecord[]
73
+ +markProcessing(ids) void
74
+ +markCompleted(id) void
75
+ +markFailed(id, attempts, error) void
76
+ +markForRetry(id, attempts, error, nextRetryAt) void
77
+ +resetStuckJobs(timeout) number
78
+ +failExpiredJobs() number
79
+ +getStats() QueueStats
80
+ }
81
+
82
+ class ColumnHelpers {
83
+ +standardColumns
84
+ +standardColumnsWithSoftDelete
85
+ +appendOnlyColumns
86
+ }
87
+
88
+ class QueueJobsTable {
89
+ +queueJobs
90
+ }
91
+
92
+ class MigrationRunner {
93
+ +applyMigrations(adapter, opts?) void
94
+ }
95
+
96
+ class EmbeddedMigrations {
97
+ +embeddedMigrations EmbeddedMigration[]
98
+ }
99
+
100
+ DbAdapter <|.. BunSqliteAdapter : implements
101
+ DbAdapter <|.. D1Adapter : implements
102
+ BaseDao <|-- EntityDao : extends
103
+ EntityDao <|-- QueueJobDao : extends
104
+ QueueJobDao --> QueueJobsTable : "uses"
105
+ MigrationRunner --> EmbeddedMigrations : "uses"
106
+ MigrationRunner --> BunSqliteAdapter : "requires"
107
+ ```
108
+
109
+ ## How It Works
110
+
111
+ ### Adapter pattern
112
+
113
+ `createDbAdapter()` selects the correct implementation based on driver config:
114
+
115
+ ```ts
116
+ import { createDbAdapter } from '@gobing-ai/ts-db';
117
+
118
+ // Bun SQLite (in-memory)
119
+ const adapter = await createDbAdapter({ driver: 'bun-sqlite', url: ':memory:' });
120
+
121
+ // Bun SQLite (file-based with pragmas)
122
+ const adapter = await createDbAdapter({
123
+ driver: 'bun-sqlite',
124
+ url: './data/app.db',
125
+ pragmas: { journalMode: 'PRAGMA journal_mode = WAL' },
126
+ });
127
+
128
+ // Cloudflare D1
129
+ const adapter = await createDbAdapter({ driver: 'd1', binding: env.DB });
130
+ ```
131
+
132
+ All adapters implement the same `DbAdapter` interface:
133
+
134
+ ```ts
135
+ await adapter.exec('CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)');
136
+ await adapter.run('INSERT INTO users VALUES (?, ?)', 'u1', 'Alice');
137
+ const user = await adapter.queryFirst<{ name: string }>('SELECT name FROM users WHERE id = ?', 'u1');
138
+ const all = await adapter.queryAll<{ name: string }>('SELECT name FROM users');
139
+ ```
140
+
141
+ ### EntityDao — CRUD with soft delete
142
+
143
+ Define a Drizzle table, extend `EntityDao`, get full CRUD for free:
144
+
145
+ ```ts
146
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
147
+ import { EntityDao, standardColumns } from '@gobing-ai/ts-db';
148
+
149
+ const users = sqliteTable('users', {
150
+ id: text('id').primaryKey(),
151
+ name: text('name').notNull(),
152
+ email: text('email').notNull(),
153
+ ...standardColumns,
154
+ });
155
+
156
+ class UsersDao extends EntityDao<typeof users, typeof users.id> {
157
+ constructor(db: DbClient) {
158
+ super(db, users, users.id, 'users');
159
+ }
160
+
161
+ async findByEmail(email: string) {
162
+ return this.findBy(users.email, email);
163
+ }
164
+ }
165
+
166
+ // Usage
167
+ const dao = new UsersDao(adapter.getDb());
168
+ const user = await dao.create({ id: 'u1', name: 'Alice', email: 'a@test.com' });
169
+ const found = await dao.findById('u1');
170
+ const updated = await dao.update('u1', { name: 'Alice Updated' });
171
+ const page = await dao.list({ limit: 20, offset: 0 });
172
+ const total = await dao.count();
173
+ await dao.delete('u1'); // soft delete if table has `inUsed` column
174
+ ```
175
+
176
+ **Soft delete** is automatic for tables with an `inUsed` column (from `standardColumnsWithSoftDelete`). Call `findById(id, true)` to include soft-deleted records.
177
+
178
+ ### QueueJobDao — job queue persistence
179
+
180
+ ```ts
181
+ import { QueueJobDao } from '@gobing-ai/ts-db';
182
+
183
+ const queue = new QueueJobDao(adapter.getDb());
184
+
185
+ // Enqueue
186
+ const jobId = await queue.enqueue('send-email', { to: 'user@test.com' }, { maxRetries: 5 });
187
+
188
+ // Consumer: claim ready jobs atomically
189
+ const jobs = await queue.claimReady(10);
190
+
191
+ for (const job of jobs) {
192
+ try {
193
+ await processJob(job);
194
+ await queue.markCompleted(job.id);
195
+ } catch (error) {
196
+ if (job.attempts >= job.maxRetries) {
197
+ await queue.markFailed(job.id, job.attempts + 1, String(error));
198
+ } else {
199
+ const retryAt = Date.now() + Math.pow(2, job.attempts) * 1000;
200
+ await queue.markForRetry(job.id, job.attempts + 1, String(error), retryAt);
201
+ }
202
+ }
203
+ }
204
+
205
+ // Maintenance
206
+ await queue.resetStuckJobs(30_000); // reset stuck after 30s
207
+ await queue.failExpiredJobs(); // fail expired TTL jobs
208
+
209
+ const stats = await queue.getStats();
210
+ // → { pending: 5, processing: 2, completed: 100, failed: 3 }
211
+ ```
212
+
213
+ ### Migrations
214
+
215
+ ```ts
216
+ import { BunSqliteAdapter, applyMigrations } from '@gobing-ai/ts-db';
217
+
218
+ const adapter = new BunSqliteAdapter({ databaseUrl: './data/app.db' });
219
+
220
+ // Applies pending migrations from drizzle/ folder (file-based)
221
+ // Falls back to embedded SQL if no folder exists (compiled binaries, CF Workers)
222
+ await applyMigrations(adapter);
223
+
224
+ // Safe to call on every startup — already-applied migrations are skipped
225
+ ```
226
+
227
+ ### Schema helpers
228
+
229
+ ```ts
230
+ import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
231
+ import { standardColumns, standardColumnsWithSoftDelete, queueJobs } from '@gobing-ai/ts-db';
232
+
233
+ // Standard columns (createdAt, updatedAt)
234
+ const docs = sqliteTable('docs', {
235
+ id: text('id').primaryKey(),
236
+ title: text('title').notNull(),
237
+ ...standardColumns,
238
+ });
239
+
240
+ // With soft delete (adds inUsed column)
241
+ const projects = sqliteTable('projects', {
242
+ id: text('id').primaryKey(),
243
+ name: text('name').notNull(),
244
+ ...standardColumnsWithSoftDelete,
245
+ });
246
+
247
+ // queue_jobs table is pre-built for use with QueueJobDao
248
+ ```
249
+
250
+ ## Usage
251
+
252
+ ### Install
253
+
254
+ ```bash
255
+ bun add @gobing-ai/ts-db drizzle-orm
256
+ bun add -D drizzle-kit
257
+ ```
258
+
259
+ ### Define your schema
260
+
261
+ ```ts
262
+ // src/schema.ts
263
+ import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
264
+ import { standardColumns } from '@gobing-ai/ts-db';
265
+
266
+ export const todos = sqliteTable('todos', {
267
+ id: text('id').primaryKey(),
268
+ title: text('title').notNull(),
269
+ done: text('done').notNull().default('0'),
270
+ ...standardColumns,
271
+ });
272
+ ```
273
+
274
+ ### Create a DAO
275
+
276
+ ```ts
277
+ // src/todos-dao.ts
278
+ import type { DbClient } from '@gobing-ai/ts-db';
279
+ import { EntityDao } from '@gobing-ai/ts-db';
280
+ import { todos } from './schema';
281
+
282
+ export class TodosDao extends EntityDao<typeof todos, typeof todos.id> {
283
+ constructor(db: DbClient) {
284
+ super(db, todos, todos.id, 'todos');
285
+ }
286
+
287
+ async findPending() {
288
+ return this.findAllBy(todos.done, '0');
289
+ }
290
+
291
+ async markDone(id: string) {
292
+ return this.update(id, { done: '1' });
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### Wire it up
298
+
299
+ ```ts
300
+ // src/index.ts
301
+ import { createDbAdapter, applyMigrations } from '@gobing-ai/ts-db';
302
+ import { TodosDao } from './todos-dao';
303
+
304
+ const adapter = await createDbAdapter({ driver: 'bun-sqlite', url: ':memory:' });
305
+ await applyMigrations(adapter);
306
+
307
+ const todos = new TodosDao(adapter.getDb());
308
+
309
+ await todos.create({ id: '1', title: 'Learn ts-db' });
310
+ await todos.create({ id: '2', title: 'Build something' });
311
+
312
+ const pending = await todos.findPending();
313
+ // → [{ id: '1', ... }, { id: '2', ... }]
314
+
315
+ await todos.markDone('1');
316
+ ```
317
+
318
+ ### Running with Bun
319
+
320
+ ```bash
321
+ # Generate migrations
322
+ bun drizzle-kit generate
323
+
324
+ # Apply at startup
325
+ bun run src/index.ts
326
+ ```
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Minimal D1 binding interface — avoids depending on @cloudflare/workers-types.
3
+ */
4
+ interface D1Binding {
5
+ prepare(sql: string): unknown;
6
+ exec(sql: string): Promise<void>;
7
+ }
8
+ /**
9
+ * Generic database table descriptor carrying select and insert type info.
10
+ */
11
+ export interface DbTable<TSelect, TInsert = TSelect> {
12
+ readonly $inferSelect: TSelect;
13
+ readonly $inferInsert: TInsert;
14
+ }
15
+ type DbInsertBuilder<TTable extends DbTable<unknown, unknown>> = {
16
+ values(values: TTable['$inferInsert'] | TTable['$inferInsert'][]): PromiseLike<unknown>;
17
+ };
18
+ interface DbSelectWhereResult<TTable extends DbTable<unknown, unknown>> extends PromiseLike<TTable['$inferSelect'][]> {
19
+ limit(value: number): DbSelectWhereResult<TTable>;
20
+ offset(value: number): DbSelectWhereResult<TTable>;
21
+ orderBy(column: unknown): DbSelectWhereResult<TTable>;
22
+ }
23
+ type DbSelectFromResult<TTable extends DbTable<unknown, unknown>> = DbSelectWhereResult<TTable> & {
24
+ where(condition: unknown): DbSelectWhereResult<TTable>;
25
+ };
26
+ type DbSelectBuilder = {
27
+ from<TTable extends DbTable<unknown, unknown>>(table: TTable): DbSelectFromResult<TTable>;
28
+ };
29
+ type DbProjectionSelectBuilder<TProjection> = {
30
+ from(table: DbTable<unknown, unknown>): PromiseLike<TProjection[]> & {
31
+ where(condition: unknown): PromiseLike<TProjection[]>;
32
+ };
33
+ };
34
+ interface DbUpdateResult {
35
+ changes: number;
36
+ }
37
+ interface DbUpdateBuilder<TTable extends DbTable<unknown, unknown>> {
38
+ set(values: Partial<TTable['$inferInsert']>): {
39
+ where(condition: unknown): PromiseLike<DbUpdateResult>;
40
+ };
41
+ }
42
+ /**
43
+ * Abstract database client with insert/select/update/delete query builders.
44
+ */
45
+ export interface DbClient {
46
+ insert<TTable extends DbTable<unknown, unknown>>(table: TTable): DbInsertBuilder<TTable>;
47
+ select(): DbSelectBuilder;
48
+ select<TProjection>(projection: Record<string, unknown>): DbProjectionSelectBuilder<TProjection>;
49
+ update<TTable extends DbTable<unknown, unknown>>(table: TTable): DbUpdateBuilder<TTable>;
50
+ delete<TTable extends DbTable<unknown, unknown>>(table: TTable): {
51
+ where(condition: unknown): PromiseLike<DbUpdateResult>;
52
+ };
53
+ }
54
+ /**
55
+ * Database adapter providing a unified client, raw SQL exec, and lifecycle management.
56
+ */
57
+ export interface DbAdapter {
58
+ getDb(): DbClient;
59
+ exec(sql: string): Promise<void>;
60
+ /** Parameterized write (INSERT/UPDATE/DELETE) that returns no rows. */
61
+ run(sql: string, ...params: unknown[]): Promise<void>;
62
+ queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
63
+ queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
64
+ close(): void;
65
+ }
66
+ /**
67
+ * Discriminated-union config for creating a database adapter (bun-sqlite or D1).
68
+ */
69
+ export type DbAdapterConfig = {
70
+ driver: 'bun-sqlite';
71
+ url?: string;
72
+ pragmas?: {
73
+ journalMode?: string;
74
+ synchronous?: string;
75
+ foreignKeys?: string;
76
+ };
77
+ } | {
78
+ driver: 'd1';
79
+ binding: D1Binding;
80
+ };
81
+ /**
82
+ * Factory: creates the correct {@link DbAdapter} implementation based on driver config.
83
+ */
84
+ export declare function createDbAdapter(config: DbAdapterConfig): Promise<DbAdapter>;
85
+ export {};
86
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Factory: creates the correct {@link DbAdapter} implementation based on driver config.
3
+ */
4
+ export async function createDbAdapter(config) {
5
+ switch (config.driver) {
6
+ case 'bun-sqlite': {
7
+ const { BunSqliteAdapter } = await import('./adapters/bun-sqlite.js');
8
+ return new BunSqliteAdapter({
9
+ ...(config.url ? { databaseUrl: config.url } : {}),
10
+ ...(config.pragmas ? { pragmas: config.pragmas } : {}),
11
+ });
12
+ }
13
+ case 'd1': {
14
+ const { D1Adapter } = await import('./adapters/d1.js');
15
+ return new D1Adapter(config.binding);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,40 @@
1
+ import { type BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
2
+ import type { DbAdapter, DbClient } from '../adapter';
3
+ import * as schema from '../schema/index';
4
+ /**
5
+ * Configuration options for the bun:sqlite adapter (path, pragmas).
6
+ */
7
+ export interface BunSqliteOptions {
8
+ /** Database path or ":memory:". Default: ".spur/spur.db" */
9
+ databaseUrl?: string;
10
+ /** SQLite pragmas. All have sensible defaults. */
11
+ pragmas?: {
12
+ journalMode?: string;
13
+ synchronous?: string;
14
+ foreignKeys?: string;
15
+ };
16
+ }
17
+ /**
18
+ * Bun SQLite database adapter backed by `bun:sqlite`.
19
+ */
20
+ export declare class BunSqliteAdapter implements DbAdapter {
21
+ private sqlite;
22
+ private drizzleDb;
23
+ /**
24
+ * Compiled-statement cache keyed by SQL text. `bun:sqlite` statements are
25
+ * reusable across calls with different params, so caching collapses the
26
+ * per-call `prepare()` recompile that dominated bulk write loops.
27
+ */
28
+ private readonly stmtCache;
29
+ private getStatement;
30
+ constructor(options?: BunSqliteOptions);
31
+ getDb(): DbClient;
32
+ /** Returns the underlying drizzle instance for migration operations. */
33
+ getDrizzleDb(): BunSQLiteDatabase<typeof schema>;
34
+ exec(sql: string): Promise<void>;
35
+ run(sql: string, ...params: unknown[]): Promise<void>;
36
+ queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
37
+ queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
38
+ close(): void;
39
+ }
40
+ //# sourceMappingURL=bun-sqlite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bun-sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/bun-sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAW,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAS1C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AAUD;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAC9C,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAmC;IACpD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,OAAO,CAAC,YAAY;gBAUR,OAAO,CAAC,EAAE,gBAAgB;IAkBtC,KAAK,IAAI,QAAQ;IAIjB,wEAAwE;IACxE,YAAY,IAAI,iBAAiB,CAAC,OAAO,MAAM,CAAC;IAI1C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAKxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAKlE,KAAK,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,70 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { drizzle } from 'drizzle-orm/bun-sqlite';
4
+ import * as schema from '../schema/index.js';
5
+ const DEFAULT_PRAGMAS = {
6
+ journalMode: 'PRAGMA journal_mode = WAL',
7
+ synchronous: 'PRAGMA synchronous = NORMAL',
8
+ foreignKeys: 'PRAGMA foreign_keys = ON',
9
+ };
10
+ const DEFAULT_DB_PATH = '.spur/spur.db';
11
+ /**
12
+ * Bun SQLite database adapter backed by `bun:sqlite`.
13
+ */
14
+ export class BunSqliteAdapter {
15
+ sqlite;
16
+ drizzleDb;
17
+ /**
18
+ * Compiled-statement cache keyed by SQL text. `bun:sqlite` statements are
19
+ * reusable across calls with different params, so caching collapses the
20
+ * per-call `prepare()` recompile that dominated bulk write loops.
21
+ */
22
+ stmtCache = new Map();
23
+ getStatement(sql) {
24
+ const cached = this.stmtCache.get(sql);
25
+ if (cached !== undefined) {
26
+ return cached;
27
+ }
28
+ const stmt = this.sqlite.prepare(sql);
29
+ this.stmtCache.set(sql, stmt);
30
+ return stmt;
31
+ }
32
+ constructor(options) {
33
+ let dbPath = options?.databaseUrl ?? DEFAULT_DB_PATH;
34
+ const pragmas = { ...DEFAULT_PRAGMAS, ...options?.pragmas };
35
+ // Resolve relative paths
36
+ if (dbPath !== ':memory:' && !isAbsolute(dbPath)) {
37
+ dbPath = resolve(dbPath);
38
+ }
39
+ this.sqlite = new Database(dbPath, { create: true });
40
+ this.sqlite.run(pragmas.journalMode);
41
+ this.sqlite.run(pragmas.synchronous);
42
+ this.sqlite.run(pragmas.foreignKeys);
43
+ this.drizzleDb = drizzle({ client: this.sqlite, schema });
44
+ }
45
+ getDb() {
46
+ return this.drizzleDb;
47
+ }
48
+ /** Returns the underlying drizzle instance for migration operations. */
49
+ getDrizzleDb() {
50
+ return this.drizzleDb;
51
+ }
52
+ async exec(sql) {
53
+ this.sqlite.prepare(sql).run();
54
+ }
55
+ async run(sql, ...params) {
56
+ const stmt = this.getStatement(sql);
57
+ stmt.run(...params);
58
+ }
59
+ async queryFirst(sql, ...params) {
60
+ const stmt = this.getStatement(sql);
61
+ return stmt.get(...params);
62
+ }
63
+ async queryAll(sql, ...params) {
64
+ const stmt = this.getStatement(sql);
65
+ return stmt.all(...params) ?? [];
66
+ }
67
+ close() {
68
+ this.sqlite.close();
69
+ }
70
+ }
@@ -0,0 +1,48 @@
1
+ import type { DbAdapter, DbClient } from '../adapter';
2
+ /**
3
+ * Minimal D1 binding interface — avoids depending on @cloudflare/workers-types.
4
+ */
5
+ export interface D1Binding {
6
+ prepare(sql: string): {
7
+ bind(...params: unknown[]): D1BoundStatement;
8
+ first?<T>(): Promise<T | null>;
9
+ run?(): Promise<{
10
+ results: unknown[];
11
+ success: boolean;
12
+ }>;
13
+ };
14
+ exec(sql: string): Promise<void>;
15
+ }
16
+ interface D1BoundStatement {
17
+ all<T>(): Promise<{
18
+ results: T[];
19
+ success: boolean;
20
+ }>;
21
+ run(): Promise<{
22
+ results: unknown[];
23
+ success: boolean;
24
+ }>;
25
+ raw<T>(): Promise<T[]>;
26
+ first?<T>(): Promise<T | null>;
27
+ }
28
+ /**
29
+ * Cloudflare D1 database adapter.
30
+ *
31
+ * Accepts a D1 binding object matching the Cloudflare Workers D1Database
32
+ * interface shape. No ambient @cloudflare/workers-types dependency required.
33
+ */
34
+ export declare class D1Adapter implements DbAdapter {
35
+ private binding;
36
+ private drizzleDb;
37
+ constructor(binding: D1Binding);
38
+ getDb(): DbClient;
39
+ /** Returns the non-mutating binding for advanced direct D1 calls. */
40
+ getBinding(): D1Binding;
41
+ exec(sql: string): Promise<void>;
42
+ run(sql: string, ...params: unknown[]): Promise<void>;
43
+ queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
44
+ queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
45
+ close(): void;
46
+ }
47
+ export {};
48
+ //# sourceMappingURL=d1.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1.d.ts","sourceRoot":"","sources":["../../src/adapters/d1.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtD;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAClB,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;QAC7C,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/B,GAAG,CAAC,IAAI,OAAO,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAC7D,CAAC;IACF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,UAAU,gBAAgB;IACtB,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtD,GAAG,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzD,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACvB,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,SAAS;IACvC,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,SAAS,CAAmC;gBAExC,OAAO,EAAE,SAAS;IAK9B,KAAK,IAAI,QAAQ;IAIjB,qEAAqE;IACrE,UAAU,IAAI,SAAS;IAIjB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAQxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAOlE,KAAK,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,45 @@
1
+ import { drizzle } from 'drizzle-orm/d1';
2
+ import * as schema from '../schema/index.js';
3
+ /**
4
+ * Cloudflare D1 database adapter.
5
+ *
6
+ * Accepts a D1 binding object matching the Cloudflare Workers D1Database
7
+ * interface shape. No ambient @cloudflare/workers-types dependency required.
8
+ */
9
+ export class D1Adapter {
10
+ binding;
11
+ drizzleDb;
12
+ constructor(binding) {
13
+ this.binding = binding;
14
+ this.drizzleDb = drizzle(this.binding, { schema });
15
+ }
16
+ getDb() {
17
+ return this.drizzleDb;
18
+ }
19
+ /** Returns the non-mutating binding for advanced direct D1 calls. */
20
+ getBinding() {
21
+ return this.binding;
22
+ }
23
+ async exec(sql) {
24
+ await this.binding.exec(sql);
25
+ }
26
+ async run(sql, ...params) {
27
+ const stmt = this.binding.prepare(sql);
28
+ const bound = params.length > 0 ? stmt.bind(...params) : stmt;
29
+ await bound.run();
30
+ }
31
+ async queryFirst(sql, ...params) {
32
+ const stmt = this.binding.prepare(sql);
33
+ const bound = params.length > 0 ? stmt.bind(...params) : stmt;
34
+ return ((await bound.first()) ?? undefined);
35
+ }
36
+ async queryAll(sql, ...params) {
37
+ const stmt = this.binding.prepare(sql);
38
+ const bound = stmt.bind(...params);
39
+ const result = await bound.all();
40
+ return result.results ?? [];
41
+ }
42
+ close() {
43
+ // D1 bindings are managed by the Workers runtime -- no-op
44
+ }
45
+ }
@@ -0,0 +1,27 @@
1
+ import type { DbClient } from './adapter';
2
+ /**
3
+ * Abstract base DAO providing transaction and timestamp utilities to all entity DAOs.
4
+ */
5
+ export declare abstract class BaseDao {
6
+ protected readonly db: DbClient;
7
+ /**
8
+ * DB transaction utility for subclasses.
9
+ *
10
+ * Constructor is `protected` — instantiate through concrete DAO subclasses,
11
+ * not BaseDao directly. Tests must declare an explicit public constructor
12
+ * that calls `super(db)` to expose the protected constructor publicly.
13
+ */
14
+ protected constructor(db: DbClient);
15
+ protected now(): number;
16
+ /**
17
+ * Execute a function within a database transaction.
18
+ *
19
+ * Works uniformly on both D1 (async) and bun:sqlite (sync wrapped in promise).
20
+ * The callback receives a transaction-scoped DbClient.
21
+ *
22
+ * @param fn - Function to execute within the transaction.
23
+ * @returns The return value of `fn`.
24
+ */
25
+ protected withTransaction<T>(fn: (tx: DbClient) => Promise<T>): Promise<T>;
26
+ }
27
+ //# sourceMappingURL=base-dao.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-dao.d.ts","sourceRoot":"","sources":["../src/base-dao.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;GAEG;AACH,8BAAsB,OAAO;IAQH,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ;IAPrD;;;;;;OAMG;IACH,SAAS,aAAgC,EAAE,EAAE,QAAQ;IAErD,SAAS,CAAC,GAAG,IAAI,MAAM;IAIvB;;;;;;;;OAQG;cACa,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAQnF"}