@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.
- package/README.md +326 -0
- package/dist/adapter.d.ts +86 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +18 -0
- package/dist/adapters/bun-sqlite.d.ts +40 -0
- package/dist/adapters/bun-sqlite.d.ts.map +1 -0
- package/dist/adapters/bun-sqlite.js +70 -0
- package/dist/adapters/d1.d.ts +48 -0
- package/dist/adapters/d1.d.ts.map +1 -0
- package/dist/adapters/d1.js +45 -0
- package/dist/base-dao.d.ts +27 -0
- package/dist/base-dao.d.ts.map +1 -0
- package/dist/base-dao.js +34 -0
- package/dist/embedded-migrations.d.ts +15 -0
- package/dist/embedded-migrations.d.ts.map +1 -0
- package/dist/embedded-migrations.js +25 -0
- package/dist/entity-dao.d.ts +143 -0
- package/dist/entity-dao.d.ts.map +1 -0
- package/dist/entity-dao.js +218 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +9 -0
- package/dist/migrate.d.ts +38 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +131 -0
- package/dist/queue-job-dao.d.ts +95 -0
- package/dist/queue-job-dao.d.ts.map +1 -0
- package/dist/queue-job-dao.js +211 -0
- package/dist/schema/common.d.ts +87 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +76 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/queue-jobs.d.ts +225 -0
- package/dist/schema/queue-jobs.d.ts.map +1 -0
- package/dist/schema/queue-jobs.js +18 -0
- package/dist/span-context.d.ts +2 -0
- package/dist/span-context.d.ts.map +1 -0
- package/dist/span-context.js +0 -0
- package/package.json +47 -0
- package/src/adapter.ts +109 -0
- package/src/adapters/bun-sqlite.ts +108 -0
- package/src/adapters/d1.ts +76 -0
- package/src/base-dao.ts +37 -0
- package/src/embedded-migrations.ts +32 -0
- package/src/entity-dao.ts +290 -0
- package/src/index.ts +19 -0
- package/src/migrate.ts +163 -0
- package/src/queue-job-dao.ts +317 -0
- package/src/schema/common.ts +94 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/queue-jobs.ts +23 -0
- package/src/span-context.ts +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema definition for the queue_jobs table.
|
|
3
|
+
*/
|
|
4
|
+
export declare const queueJobs: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
5
|
+
name: "queue_jobs";
|
|
6
|
+
schema: undefined;
|
|
7
|
+
columns: {
|
|
8
|
+
nextRetryAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
9
|
+
name: "next_retry_at";
|
|
10
|
+
tableName: "queue_jobs";
|
|
11
|
+
dataType: "number";
|
|
12
|
+
columnType: "SQLiteInteger";
|
|
13
|
+
data: number;
|
|
14
|
+
driverParam: number;
|
|
15
|
+
notNull: false;
|
|
16
|
+
hasDefault: false;
|
|
17
|
+
isPrimaryKey: false;
|
|
18
|
+
isAutoincrement: false;
|
|
19
|
+
hasRuntimeDefault: false;
|
|
20
|
+
enumValues: undefined;
|
|
21
|
+
baseColumn: never;
|
|
22
|
+
identity: undefined;
|
|
23
|
+
generated: undefined;
|
|
24
|
+
}, {}, {}>;
|
|
25
|
+
lastError: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
26
|
+
name: "last_error";
|
|
27
|
+
tableName: "queue_jobs";
|
|
28
|
+
dataType: "string";
|
|
29
|
+
columnType: "SQLiteText";
|
|
30
|
+
data: string;
|
|
31
|
+
driverParam: string;
|
|
32
|
+
notNull: false;
|
|
33
|
+
hasDefault: false;
|
|
34
|
+
isPrimaryKey: false;
|
|
35
|
+
isAutoincrement: false;
|
|
36
|
+
hasRuntimeDefault: false;
|
|
37
|
+
enumValues: [string, ...string[]];
|
|
38
|
+
baseColumn: never;
|
|
39
|
+
identity: undefined;
|
|
40
|
+
generated: undefined;
|
|
41
|
+
}, {}, {
|
|
42
|
+
length: number | undefined;
|
|
43
|
+
}>;
|
|
44
|
+
processingAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
45
|
+
name: "processing_at";
|
|
46
|
+
tableName: "queue_jobs";
|
|
47
|
+
dataType: "number";
|
|
48
|
+
columnType: "SQLiteInteger";
|
|
49
|
+
data: number;
|
|
50
|
+
driverParam: number;
|
|
51
|
+
notNull: false;
|
|
52
|
+
hasDefault: false;
|
|
53
|
+
isPrimaryKey: false;
|
|
54
|
+
isAutoincrement: false;
|
|
55
|
+
hasRuntimeDefault: false;
|
|
56
|
+
enumValues: undefined;
|
|
57
|
+
baseColumn: never;
|
|
58
|
+
identity: undefined;
|
|
59
|
+
generated: undefined;
|
|
60
|
+
}, {}, {}>;
|
|
61
|
+
expiresAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
62
|
+
name: "expires_at";
|
|
63
|
+
tableName: "queue_jobs";
|
|
64
|
+
dataType: "number";
|
|
65
|
+
columnType: "SQLiteInteger";
|
|
66
|
+
data: number;
|
|
67
|
+
driverParam: number;
|
|
68
|
+
notNull: false;
|
|
69
|
+
hasDefault: false;
|
|
70
|
+
isPrimaryKey: false;
|
|
71
|
+
isAutoincrement: false;
|
|
72
|
+
hasRuntimeDefault: false;
|
|
73
|
+
enumValues: undefined;
|
|
74
|
+
baseColumn: never;
|
|
75
|
+
identity: undefined;
|
|
76
|
+
generated: undefined;
|
|
77
|
+
}, {}, {}>;
|
|
78
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
79
|
+
name: "created_at";
|
|
80
|
+
tableName: "queue_jobs";
|
|
81
|
+
dataType: "number";
|
|
82
|
+
columnType: "SQLiteInteger";
|
|
83
|
+
data: number;
|
|
84
|
+
driverParam: number;
|
|
85
|
+
notNull: true;
|
|
86
|
+
hasDefault: true;
|
|
87
|
+
isPrimaryKey: false;
|
|
88
|
+
isAutoincrement: false;
|
|
89
|
+
hasRuntimeDefault: true;
|
|
90
|
+
enumValues: undefined;
|
|
91
|
+
baseColumn: never;
|
|
92
|
+
identity: undefined;
|
|
93
|
+
generated: undefined;
|
|
94
|
+
}, {}, {}>;
|
|
95
|
+
updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
96
|
+
name: "updated_at";
|
|
97
|
+
tableName: "queue_jobs";
|
|
98
|
+
dataType: "number";
|
|
99
|
+
columnType: "SQLiteInteger";
|
|
100
|
+
data: number;
|
|
101
|
+
driverParam: number;
|
|
102
|
+
notNull: true;
|
|
103
|
+
hasDefault: true;
|
|
104
|
+
isPrimaryKey: false;
|
|
105
|
+
isAutoincrement: false;
|
|
106
|
+
hasRuntimeDefault: true;
|
|
107
|
+
enumValues: undefined;
|
|
108
|
+
baseColumn: never;
|
|
109
|
+
identity: undefined;
|
|
110
|
+
generated: undefined;
|
|
111
|
+
}, {}, {}>;
|
|
112
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
113
|
+
name: "id";
|
|
114
|
+
tableName: "queue_jobs";
|
|
115
|
+
dataType: "string";
|
|
116
|
+
columnType: "SQLiteText";
|
|
117
|
+
data: string;
|
|
118
|
+
driverParam: string;
|
|
119
|
+
notNull: true;
|
|
120
|
+
hasDefault: false;
|
|
121
|
+
isPrimaryKey: true;
|
|
122
|
+
isAutoincrement: false;
|
|
123
|
+
hasRuntimeDefault: false;
|
|
124
|
+
enumValues: [string, ...string[]];
|
|
125
|
+
baseColumn: never;
|
|
126
|
+
identity: undefined;
|
|
127
|
+
generated: undefined;
|
|
128
|
+
}, {}, {
|
|
129
|
+
length: number | undefined;
|
|
130
|
+
}>;
|
|
131
|
+
type: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
132
|
+
name: "type";
|
|
133
|
+
tableName: "queue_jobs";
|
|
134
|
+
dataType: "string";
|
|
135
|
+
columnType: "SQLiteText";
|
|
136
|
+
data: string;
|
|
137
|
+
driverParam: string;
|
|
138
|
+
notNull: true;
|
|
139
|
+
hasDefault: false;
|
|
140
|
+
isPrimaryKey: false;
|
|
141
|
+
isAutoincrement: false;
|
|
142
|
+
hasRuntimeDefault: false;
|
|
143
|
+
enumValues: [string, ...string[]];
|
|
144
|
+
baseColumn: never;
|
|
145
|
+
identity: undefined;
|
|
146
|
+
generated: undefined;
|
|
147
|
+
}, {}, {
|
|
148
|
+
length: number | undefined;
|
|
149
|
+
}>;
|
|
150
|
+
payload: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
151
|
+
name: "payload";
|
|
152
|
+
tableName: "queue_jobs";
|
|
153
|
+
dataType: "string";
|
|
154
|
+
columnType: "SQLiteText";
|
|
155
|
+
data: string;
|
|
156
|
+
driverParam: string;
|
|
157
|
+
notNull: true;
|
|
158
|
+
hasDefault: false;
|
|
159
|
+
isPrimaryKey: false;
|
|
160
|
+
isAutoincrement: false;
|
|
161
|
+
hasRuntimeDefault: false;
|
|
162
|
+
enumValues: [string, ...string[]];
|
|
163
|
+
baseColumn: never;
|
|
164
|
+
identity: undefined;
|
|
165
|
+
generated: undefined;
|
|
166
|
+
}, {}, {
|
|
167
|
+
length: number | undefined;
|
|
168
|
+
}>;
|
|
169
|
+
status: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
170
|
+
name: "status";
|
|
171
|
+
tableName: "queue_jobs";
|
|
172
|
+
dataType: "string";
|
|
173
|
+
columnType: "SQLiteText";
|
|
174
|
+
data: string;
|
|
175
|
+
driverParam: string;
|
|
176
|
+
notNull: true;
|
|
177
|
+
hasDefault: true;
|
|
178
|
+
isPrimaryKey: false;
|
|
179
|
+
isAutoincrement: false;
|
|
180
|
+
hasRuntimeDefault: false;
|
|
181
|
+
enumValues: [string, ...string[]];
|
|
182
|
+
baseColumn: never;
|
|
183
|
+
identity: undefined;
|
|
184
|
+
generated: undefined;
|
|
185
|
+
}, {}, {
|
|
186
|
+
length: number | undefined;
|
|
187
|
+
}>;
|
|
188
|
+
attempts: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
189
|
+
name: "attempts";
|
|
190
|
+
tableName: "queue_jobs";
|
|
191
|
+
dataType: "number";
|
|
192
|
+
columnType: "SQLiteInteger";
|
|
193
|
+
data: number;
|
|
194
|
+
driverParam: number;
|
|
195
|
+
notNull: true;
|
|
196
|
+
hasDefault: true;
|
|
197
|
+
isPrimaryKey: false;
|
|
198
|
+
isAutoincrement: false;
|
|
199
|
+
hasRuntimeDefault: false;
|
|
200
|
+
enumValues: undefined;
|
|
201
|
+
baseColumn: never;
|
|
202
|
+
identity: undefined;
|
|
203
|
+
generated: undefined;
|
|
204
|
+
}, {}, {}>;
|
|
205
|
+
maxRetries: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
206
|
+
name: "max_retries";
|
|
207
|
+
tableName: "queue_jobs";
|
|
208
|
+
dataType: "number";
|
|
209
|
+
columnType: "SQLiteInteger";
|
|
210
|
+
data: number;
|
|
211
|
+
driverParam: number;
|
|
212
|
+
notNull: true;
|
|
213
|
+
hasDefault: true;
|
|
214
|
+
isPrimaryKey: false;
|
|
215
|
+
isAutoincrement: false;
|
|
216
|
+
hasRuntimeDefault: false;
|
|
217
|
+
enumValues: undefined;
|
|
218
|
+
baseColumn: never;
|
|
219
|
+
identity: undefined;
|
|
220
|
+
generated: undefined;
|
|
221
|
+
}, {}, {}>;
|
|
222
|
+
};
|
|
223
|
+
dialect: "sqlite";
|
|
224
|
+
}>;
|
|
225
|
+
//# sourceMappingURL=queue-jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-jobs.d.ts","sourceRoot":"","sources":["../../src/schema/queue-jobs.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBrB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import { standardColumns } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Drizzle schema definition for the queue_jobs table.
|
|
5
|
+
*/
|
|
6
|
+
export const queueJobs = sqliteTable('queue_jobs', {
|
|
7
|
+
id: text('id').primaryKey(),
|
|
8
|
+
type: text('type').notNull(),
|
|
9
|
+
payload: text('payload').notNull(),
|
|
10
|
+
status: text('status').notNull().default('pending'),
|
|
11
|
+
attempts: integer('attempts').notNull().default(0),
|
|
12
|
+
maxRetries: integer('max_retries').notNull().default(3),
|
|
13
|
+
...standardColumns,
|
|
14
|
+
nextRetryAt: integer('next_retry_at'),
|
|
15
|
+
lastError: text('last_error'),
|
|
16
|
+
processingAt: integer('processing_at'),
|
|
17
|
+
expiresAt: integer('expires_at'),
|
|
18
|
+
}, (table) => [index('queue_jobs_ready_idx').on(table.status, table.nextRetryAt, table.createdAt)]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span-context.d.ts","sourceRoot":"","sources":["../src/span-context.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/ts-db",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "@gobing-ai/ts-db — Database abstraction layer with Drizzle ORM adapters, generic DAOs, and migration tooling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json && bun ../../scripts/fix-dist-esm-extensions.ts dist",
|
|
24
|
+
"test": "NODE_ENV=test bun test --coverage --coverage-dir=.coverage --reporter=dots",
|
|
25
|
+
"test:full": "NODE_ENV=test bun test --update-snapshots --coverage --coverage-dir=.coverage",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome check . && bun run typecheck",
|
|
28
|
+
"format": "biome check . --write",
|
|
29
|
+
"check": "bun run lint && bun run test",
|
|
30
|
+
"prepublishOnly": "bun run build",
|
|
31
|
+
"release": "npm publish --access public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@gobing-ai/ts-runtime": "^0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"drizzle-orm": ">=0.38.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "1.3.14",
|
|
41
|
+
"drizzle-kit": "^0.30.0",
|
|
42
|
+
"drizzle-orm": "^0.38.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Generic database table descriptor carrying select and insert type info.
|
|
11
|
+
*/
|
|
12
|
+
export interface DbTable<TSelect, TInsert = TSelect> {
|
|
13
|
+
readonly $inferSelect: TSelect;
|
|
14
|
+
readonly $inferInsert: TInsert;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type DbInsertBuilder<TTable extends DbTable<unknown, unknown>> = {
|
|
18
|
+
values(values: TTable['$inferInsert'] | TTable['$inferInsert'][]): PromiseLike<unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface DbSelectWhereResult<TTable extends DbTable<unknown, unknown>> extends PromiseLike<TTable['$inferSelect'][]> {
|
|
22
|
+
limit(value: number): DbSelectWhereResult<TTable>;
|
|
23
|
+
offset(value: number): DbSelectWhereResult<TTable>;
|
|
24
|
+
orderBy(column: unknown): DbSelectWhereResult<TTable>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type DbSelectFromResult<TTable extends DbTable<unknown, unknown>> = DbSelectWhereResult<TTable> & {
|
|
28
|
+
where(condition: unknown): DbSelectWhereResult<TTable>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type DbSelectBuilder = {
|
|
32
|
+
from<TTable extends DbTable<unknown, unknown>>(table: TTable): DbSelectFromResult<TTable>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DbProjectionSelectBuilder<TProjection> = {
|
|
36
|
+
from(table: DbTable<unknown, unknown>): PromiseLike<TProjection[]> & {
|
|
37
|
+
where(condition: unknown): PromiseLike<TProjection[]>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface DbUpdateResult {
|
|
42
|
+
changes: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DbUpdateBuilder<TTable extends DbTable<unknown, unknown>> {
|
|
46
|
+
set(values: Partial<TTable['$inferInsert']>): { where(condition: unknown): PromiseLike<DbUpdateResult> };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Abstract database client with insert/select/update/delete query builders.
|
|
51
|
+
*/
|
|
52
|
+
export interface DbClient {
|
|
53
|
+
insert<TTable extends DbTable<unknown, unknown>>(table: TTable): DbInsertBuilder<TTable>;
|
|
54
|
+
select(): DbSelectBuilder;
|
|
55
|
+
select<TProjection>(projection: Record<string, unknown>): DbProjectionSelectBuilder<TProjection>;
|
|
56
|
+
update<TTable extends DbTable<unknown, unknown>>(table: TTable): DbUpdateBuilder<TTable>;
|
|
57
|
+
delete<TTable extends DbTable<unknown, unknown>>(
|
|
58
|
+
table: TTable,
|
|
59
|
+
): {
|
|
60
|
+
where(condition: unknown): PromiseLike<DbUpdateResult>;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Database adapter providing a unified client, raw SQL exec, and lifecycle management.
|
|
66
|
+
*/
|
|
67
|
+
export interface DbAdapter {
|
|
68
|
+
getDb(): DbClient;
|
|
69
|
+
exec(sql: string): Promise<void>;
|
|
70
|
+
/** Parameterized write (INSERT/UPDATE/DELETE) that returns no rows. */
|
|
71
|
+
run(sql: string, ...params: unknown[]): Promise<void>;
|
|
72
|
+
queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
|
|
73
|
+
queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
|
|
74
|
+
close(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Discriminated-union config for creating a database adapter (bun-sqlite or D1).
|
|
79
|
+
*/
|
|
80
|
+
export type DbAdapterConfig =
|
|
81
|
+
| {
|
|
82
|
+
driver: 'bun-sqlite';
|
|
83
|
+
url?: string;
|
|
84
|
+
pragmas?: {
|
|
85
|
+
journalMode?: string;
|
|
86
|
+
synchronous?: string;
|
|
87
|
+
foreignKeys?: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
| { driver: 'd1'; binding: D1Binding };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Factory: creates the correct {@link DbAdapter} implementation based on driver config.
|
|
94
|
+
*/
|
|
95
|
+
export async function createDbAdapter(config: DbAdapterConfig): Promise<DbAdapter> {
|
|
96
|
+
switch (config.driver) {
|
|
97
|
+
case 'bun-sqlite': {
|
|
98
|
+
const { BunSqliteAdapter } = await import('./adapters/bun-sqlite');
|
|
99
|
+
return new BunSqliteAdapter({
|
|
100
|
+
...(config.url ? { databaseUrl: config.url } : {}),
|
|
101
|
+
...(config.pragmas ? { pragmas: config.pragmas } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
case 'd1': {
|
|
105
|
+
const { D1Adapter } = await import('./adapters/d1');
|
|
106
|
+
return new D1Adapter(config.binding as import('./adapters/d1').D1Binding);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { type BunSQLiteDatabase, drizzle } from 'drizzle-orm/bun-sqlite';
|
|
4
|
+
import type { DbAdapter, DbClient } from '../adapter';
|
|
5
|
+
import * as schema from '../schema/index';
|
|
6
|
+
|
|
7
|
+
type SqliteStatementLike = {
|
|
8
|
+
all: (...params: unknown[]) => unknown;
|
|
9
|
+
get: (...params: unknown[]) => unknown;
|
|
10
|
+
run: (...params: unknown[]) => unknown;
|
|
11
|
+
values?: (...params: unknown[]) => unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration options for the bun:sqlite adapter (path, pragmas).
|
|
16
|
+
*/
|
|
17
|
+
export interface BunSqliteOptions {
|
|
18
|
+
/** Database path or ":memory:". Default: ".spur/spur.db" */
|
|
19
|
+
databaseUrl?: string;
|
|
20
|
+
/** SQLite pragmas. All have sensible defaults. */
|
|
21
|
+
pragmas?: {
|
|
22
|
+
journalMode?: string;
|
|
23
|
+
synchronous?: string;
|
|
24
|
+
foreignKeys?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PRAGMAS = {
|
|
29
|
+
journalMode: 'PRAGMA journal_mode = WAL',
|
|
30
|
+
synchronous: 'PRAGMA synchronous = NORMAL',
|
|
31
|
+
foreignKeys: 'PRAGMA foreign_keys = ON',
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const DEFAULT_DB_PATH = '.spur/spur.db';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bun SQLite database adapter backed by `bun:sqlite`.
|
|
38
|
+
*/
|
|
39
|
+
export class BunSqliteAdapter implements DbAdapter {
|
|
40
|
+
private sqlite: Database;
|
|
41
|
+
private drizzleDb: BunSQLiteDatabase<typeof schema>;
|
|
42
|
+
/**
|
|
43
|
+
* Compiled-statement cache keyed by SQL text. `bun:sqlite` statements are
|
|
44
|
+
* reusable across calls with different params, so caching collapses the
|
|
45
|
+
* per-call `prepare()` recompile that dominated bulk write loops.
|
|
46
|
+
*/
|
|
47
|
+
private readonly stmtCache = new Map<string, SqliteStatementLike>();
|
|
48
|
+
|
|
49
|
+
private getStatement(sql: string): SqliteStatementLike {
|
|
50
|
+
const cached = this.stmtCache.get(sql);
|
|
51
|
+
if (cached !== undefined) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
const stmt = this.sqlite.prepare(sql) as unknown as SqliteStatementLike;
|
|
55
|
+
this.stmtCache.set(sql, stmt);
|
|
56
|
+
return stmt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
constructor(options?: BunSqliteOptions) {
|
|
60
|
+
let dbPath = options?.databaseUrl ?? DEFAULT_DB_PATH;
|
|
61
|
+
const pragmas = { ...DEFAULT_PRAGMAS, ...options?.pragmas };
|
|
62
|
+
|
|
63
|
+
// Resolve relative paths
|
|
64
|
+
if (dbPath !== ':memory:' && !isAbsolute(dbPath)) {
|
|
65
|
+
dbPath = resolve(dbPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.sqlite = new Database(dbPath, { create: true });
|
|
69
|
+
|
|
70
|
+
this.sqlite.run(pragmas.journalMode);
|
|
71
|
+
this.sqlite.run(pragmas.synchronous);
|
|
72
|
+
this.sqlite.run(pragmas.foreignKeys);
|
|
73
|
+
|
|
74
|
+
this.drizzleDb = drizzle({ client: this.sqlite, schema });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getDb(): DbClient {
|
|
78
|
+
return this.drizzleDb as unknown as DbClient;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Returns the underlying drizzle instance for migration operations. */
|
|
82
|
+
getDrizzleDb(): BunSQLiteDatabase<typeof schema> {
|
|
83
|
+
return this.drizzleDb;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async exec(sql: string): Promise<void> {
|
|
87
|
+
this.sqlite.prepare(sql).run();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async run(sql: string, ...params: unknown[]): Promise<void> {
|
|
91
|
+
const stmt = this.getStatement(sql);
|
|
92
|
+
(stmt as unknown as { run: (...p: unknown[]) => unknown }).run(...params);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined> {
|
|
96
|
+
const stmt = this.getStatement(sql);
|
|
97
|
+
return (stmt as unknown as { get: (...p: unknown[]) => T | undefined }).get(...params) as T | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]> {
|
|
101
|
+
const stmt = this.getStatement(sql);
|
|
102
|
+
return ((stmt as unknown as { all: (...p: unknown[]) => T[] }).all(...params) as T[]) ?? [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
close(): void {
|
|
106
|
+
this.sqlite.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type DrizzleD1Database, drizzle } from 'drizzle-orm/d1';
|
|
2
|
+
import type { DbAdapter, DbClient } from '../adapter';
|
|
3
|
+
import * as schema from '../schema/index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal D1 binding interface — avoids depending on @cloudflare/workers-types.
|
|
7
|
+
*/
|
|
8
|
+
export interface D1Binding {
|
|
9
|
+
prepare(sql: string): {
|
|
10
|
+
bind(...params: unknown[]): D1BoundStatement;
|
|
11
|
+
first?<T>(): Promise<T | null>;
|
|
12
|
+
run?(): Promise<{ results: unknown[]; success: boolean }>;
|
|
13
|
+
};
|
|
14
|
+
exec(sql: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface D1BoundStatement {
|
|
18
|
+
all<T>(): Promise<{ results: T[]; success: boolean }>;
|
|
19
|
+
run(): Promise<{ results: unknown[]; success: boolean }>;
|
|
20
|
+
raw<T>(): Promise<T[]>;
|
|
21
|
+
first?<T>(): Promise<T | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cloudflare D1 database adapter.
|
|
26
|
+
*
|
|
27
|
+
* Accepts a D1 binding object matching the Cloudflare Workers D1Database
|
|
28
|
+
* interface shape. No ambient @cloudflare/workers-types dependency required.
|
|
29
|
+
*/
|
|
30
|
+
export class D1Adapter implements DbAdapter {
|
|
31
|
+
private binding: D1Binding;
|
|
32
|
+
private drizzleDb: DrizzleD1Database<typeof schema>;
|
|
33
|
+
|
|
34
|
+
constructor(binding: D1Binding) {
|
|
35
|
+
this.binding = binding;
|
|
36
|
+
this.drizzleDb = drizzle(this.binding, { schema });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getDb(): DbClient {
|
|
40
|
+
return this.drizzleDb as unknown as DbClient;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Returns the non-mutating binding for advanced direct D1 calls. */
|
|
44
|
+
getBinding(): D1Binding {
|
|
45
|
+
return this.binding;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exec(sql: string): Promise<void> {
|
|
49
|
+
await this.binding.exec(sql);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async run(sql: string, ...params: unknown[]): Promise<void> {
|
|
53
|
+
const stmt = this.binding.prepare(sql);
|
|
54
|
+
const bound = params.length > 0 ? stmt.bind(...params) : stmt;
|
|
55
|
+
await (bound as unknown as { run: () => Promise<unknown> }).run();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined> {
|
|
59
|
+
const stmt = this.binding.prepare(sql);
|
|
60
|
+
const bound = params.length > 0 ? stmt.bind(...params) : stmt;
|
|
61
|
+
return ((await (bound as unknown as { first: <T>() => Promise<T | null> }).first<T>()) ?? undefined) as
|
|
62
|
+
| T
|
|
63
|
+
| undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]> {
|
|
67
|
+
const stmt = this.binding.prepare(sql);
|
|
68
|
+
const bound = stmt.bind(...params);
|
|
69
|
+
const result = await (bound as unknown as { all: <T>() => Promise<{ results: T[] }> }).all<T>();
|
|
70
|
+
return result.results ?? [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
close(): void {
|
|
74
|
+
// D1 bindings are managed by the Workers runtime -- no-op
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/base-dao.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DbClient } from './adapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base DAO providing transaction and timestamp utilities to all entity DAOs.
|
|
5
|
+
*/
|
|
6
|
+
export abstract class BaseDao {
|
|
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(protected readonly db: DbClient) {}
|
|
15
|
+
|
|
16
|
+
protected now(): number {
|
|
17
|
+
return Date.now();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute a function within a database transaction.
|
|
22
|
+
*
|
|
23
|
+
* Works uniformly on both D1 (async) and bun:sqlite (sync wrapped in promise).
|
|
24
|
+
* The callback receives a transaction-scoped DbClient.
|
|
25
|
+
*
|
|
26
|
+
* @param fn - Function to execute within the transaction.
|
|
27
|
+
* @returns The return value of `fn`.
|
|
28
|
+
*/
|
|
29
|
+
protected async withTransaction<T>(fn: (tx: DbClient) => Promise<T>): Promise<T> {
|
|
30
|
+
// Drizzle's .transaction() works on both backends:
|
|
31
|
+
// - bun:sqlite: sync wrapped in a promise
|
|
32
|
+
// - D1: native async
|
|
33
|
+
return (this.db as unknown as { transaction: (fn: unknown) => Promise<T> }).transaction(async (tx: DbClient) =>
|
|
34
|
+
fn(tx),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded migration SQL — auto-generated from drizzle/ folder.
|
|
3
|
+
*
|
|
4
|
+
* This file bundles all migration SQL as inline strings so the compiled
|
|
5
|
+
* binary can apply migrations without needing the drizzle/ folder on disk.
|
|
6
|
+
*
|
|
7
|
+
* DO NOT EDIT MANUALLY. Regenerate with: bun run scripts/embed-migrations.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface EmbeddedMigration {
|
|
11
|
+
tag: string;
|
|
12
|
+
sql: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const embeddedMigrations: EmbeddedMigration[] = [
|
|
17
|
+
{
|
|
18
|
+
tag: '0000_init',
|
|
19
|
+
sql: "CREATE TABLE `queue_jobs` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`type` text NOT NULL,\n\t`payload` text NOT NULL,\n\t`status` text DEFAULT 'pending' NOT NULL,\n\t`attempts` integer DEFAULT 0 NOT NULL,\n\t`max_retries` integer DEFAULT 3 NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`next_retry_at` integer,\n\t`last_error` text,\n\t`processing_at` integer\n);\n",
|
|
20
|
+
hash: '558dea3834348925f79b4d30ca79d0afd0d990b2883341377d369444d50ce76e',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
tag: '0001_salty_red_ghost',
|
|
24
|
+
sql: 'CREATE INDEX `queue_jobs_ready_idx` ON `queue_jobs` (`status`,`next_retry_at`,`created_at`);',
|
|
25
|
+
hash: 'f842da3f49edeec8a17bcab399669410db09996ab258dc4fa781357d0400ddbf',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
tag: '0002_nasty_namora',
|
|
29
|
+
sql: 'ALTER TABLE `queue_jobs` ADD `expires_at` integer;',
|
|
30
|
+
hash: '7380f8c162352a61b15205af5a87e0e7313a499203dae98fe62151a1dc7fec0e',
|
|
31
|
+
},
|
|
32
|
+
];
|