@gobing-ai/ts-db 0.2.2 → 0.2.4
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 +4 -3
- package/dist/adapters/bun-sqlite.d.ts +1 -1
- package/dist/adapters/bun-sqlite.d.ts.map +1 -1
- package/dist/adapters/bun-sqlite.js +2 -2
- package/dist/adapters/d1.d.ts +1 -1
- package/dist/adapters/d1.d.ts.map +1 -1
- package/dist/adapters/d1.js +1 -1
- package/dist/base-dao.d.ts.map +1 -1
- package/dist/base-dao.js +4 -1
- package/dist/entity-dao.d.ts +1 -0
- package/dist/entity-dao.d.ts.map +1 -1
- package/dist/entity-dao.js +20 -12
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/queue-job-dao.d.ts.map +1 -1
- package/dist/schema/ddl.d.ts +19 -0
- package/dist/schema/ddl.d.ts.map +1 -0
- package/dist/schema/ddl.js +156 -0
- package/dist/{define-table.d.ts → schema/define-table.d.ts} +18 -9
- package/dist/schema/define-table.d.ts.map +1 -0
- package/dist/{define-table.js → schema/define-table.js} +12 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -0
- package/dist/schema/runtime.d.ts +2 -0
- package/dist/schema/runtime.d.ts.map +1 -0
- package/dist/schema/runtime.js +1 -0
- package/package.json +14 -2
- package/src/adapters/bun-sqlite.ts +2 -2
- package/src/adapters/d1.ts +1 -1
- package/src/base-dao.ts +18 -10
- package/src/entity-dao.ts +59 -42
- package/src/index.ts +1 -2
- package/src/queue-job-dao.ts +59 -51
- package/src/schema/ddl.ts +174 -0
- package/src/{define-table.ts → schema/define-table.ts} +26 -9
- package/src/schema/index.ts +3 -0
- package/src/schema/runtime.ts +1 -0
- package/dist/define-table.d.ts.map +0 -1
package/src/base-dao.ts
CHANGED
|
@@ -2,6 +2,21 @@ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
|
2
2
|
import type { DbAdapter, InternalDb } from './adapter';
|
|
3
3
|
import { compileOrderBy, compilePredicate, type ListSpec, type Predicate } from './query-spec';
|
|
4
4
|
|
|
5
|
+
type TransactionalDb = {
|
|
6
|
+
transaction: <T>(cb: (tx: TxHandle) => Promise<T>) => Promise<T>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type SelectQuery<T> = Promise<T[]> & {
|
|
10
|
+
where: (condition: unknown) => SelectQuery<T>;
|
|
11
|
+
orderBy: (...order: unknown[]) => SelectQuery<T>;
|
|
12
|
+
limit: (limit: number) => SelectQuery<T>;
|
|
13
|
+
offset: (offset: number) => SelectQuery<T>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function asSelectQuery<T>(query: object): SelectQuery<T> {
|
|
17
|
+
return query as SelectQuery<T>;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
/**
|
|
6
21
|
* A transaction-scoped handle passed to {@link BaseDao.tx} callbacks.
|
|
7
22
|
*
|
|
@@ -42,9 +57,7 @@ export abstract class BaseDao {
|
|
|
42
57
|
* The callback receives a transaction-scoped db handle.
|
|
43
58
|
*/
|
|
44
59
|
protected async tx<T>(fn: (tx: TxHandle) => Promise<T>): Promise<T> {
|
|
45
|
-
return (this.db as
|
|
46
|
-
fn(tx),
|
|
47
|
-
);
|
|
60
|
+
return (this.db as TransactionalDb).transaction((tx) => fn(tx));
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
/**
|
|
@@ -58,17 +71,12 @@ export abstract class BaseDao {
|
|
|
58
71
|
const order = spec.orderBy ? compileOrderBy(spec.orderBy) : [];
|
|
59
72
|
|
|
60
73
|
// drizzle's fluent builder is internal here; the public input is the spec.
|
|
61
|
-
let q = (this.db
|
|
62
|
-
where: (c: unknown) => typeof q;
|
|
63
|
-
orderBy: (...o: unknown[]) => typeof q;
|
|
64
|
-
limit: (n: number) => typeof q;
|
|
65
|
-
offset: (n: number) => typeof q;
|
|
66
|
-
};
|
|
74
|
+
let q = asSelectQuery<T>(this.db.select().from(table));
|
|
67
75
|
if (condition) q = q.where(condition);
|
|
68
76
|
if (order.length > 0) q = q.orderBy(...order);
|
|
69
77
|
if (spec.limit !== undefined) q = q.limit(spec.limit);
|
|
70
78
|
if (spec.offset !== undefined) q = q.offset(spec.offset);
|
|
71
|
-
return (await
|
|
79
|
+
return (await q) ?? [];
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** Run a SELECT and return the first matching row, or undefined. */
|
package/src/entity-dao.ts
CHANGED
|
@@ -4,6 +4,36 @@ import type { DbAdapter } from './adapter';
|
|
|
4
4
|
import { BaseDao } from './base-dao';
|
|
5
5
|
import { compilePredicate, type OrderTerm, type Predicate } from './query-spec';
|
|
6
6
|
|
|
7
|
+
type ReturningRows = {
|
|
8
|
+
returning: () => Promise<unknown[]>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type InsertBuilder = {
|
|
12
|
+
values: (record: unknown) => ReturningRows & {
|
|
13
|
+
onConflictDoUpdate: (cfg: { target: SQLiteColumn[]; set: unknown }) => ReturningRows;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type UpdateBuilder = {
|
|
18
|
+
set: (data: unknown) => {
|
|
19
|
+
where: (condition: SQL) => ReturningRows;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DeleteBuilder = {
|
|
24
|
+
where: (condition: SQL) => Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type CountQuery = Promise<unknown[]> & {
|
|
28
|
+
where: (condition: SQL) => Promise<unknown[]>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CountDb = {
|
|
32
|
+
select: (projection: unknown) => {
|
|
33
|
+
from: (table: unknown) => CountQuery;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
7
37
|
/**
|
|
8
38
|
* Type for tables compatible with EntityDao.
|
|
9
39
|
* Must have standard columns: createdAt, updatedAt.
|
|
@@ -127,8 +157,9 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
127
157
|
|
|
128
158
|
/** Condition filtering out soft-deleted rows, or undefined when unsupported. */
|
|
129
159
|
protected get activeCondition(): SQL | undefined {
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
const table = this.table;
|
|
161
|
+
if ('inUsed' in table) {
|
|
162
|
+
return eq(table.inUsed as SQLiteColumn, 1);
|
|
132
163
|
}
|
|
133
164
|
return undefined;
|
|
134
165
|
}
|
|
@@ -146,7 +177,7 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
private get insertBuilder() {
|
|
149
|
-
return this.db.insert(this.table as
|
|
180
|
+
return this.db.insert(this.table as Parameters<typeof this.db.insert>[0]) as InsertBuilder;
|
|
150
181
|
}
|
|
151
182
|
|
|
152
183
|
/**
|
|
@@ -159,9 +190,7 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
159
190
|
const now = this.now();
|
|
160
191
|
const record = { createdAt: now, updatedAt: now, ...data };
|
|
161
192
|
this.validate('create', record);
|
|
162
|
-
const rows = (await (
|
|
163
|
-
this.insertBuilder.values(record) as unknown as { returning: () => Promise<unknown[]> }
|
|
164
|
-
).returning()) as TTable['$inferSelect'][];
|
|
193
|
+
const rows = (await this.insertBuilder.values(record).returning()) as TTable['$inferSelect'][];
|
|
165
194
|
return rows[0] as TTable['$inferSelect'];
|
|
166
195
|
}
|
|
167
196
|
|
|
@@ -179,9 +208,7 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
179
208
|
const now = this.now();
|
|
180
209
|
const records = data.map((d) => ({ createdAt: now, updatedAt: now, ...d }));
|
|
181
210
|
for (const record of records) this.validate('createMany', record);
|
|
182
|
-
return (await (
|
|
183
|
-
this.insertBuilder.values(records) as unknown as { returning: () => Promise<unknown[]> }
|
|
184
|
-
).returning()) as TTable['$inferSelect'][];
|
|
211
|
+
return (await this.insertBuilder.values(records).returning()) as TTable['$inferSelect'][];
|
|
185
212
|
}
|
|
186
213
|
|
|
187
214
|
/**
|
|
@@ -202,21 +229,16 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
202
229
|
// their table property key (not DB column name) to handle snake_case columns.
|
|
203
230
|
const identityCols = new Set<SQLiteColumn>([...conflictColumns, ...this.primaryKey]);
|
|
204
231
|
const identityProps = new Set(
|
|
205
|
-
Object.entries(this.table
|
|
206
|
-
.filter(([, col]) => identityCols.has(col))
|
|
232
|
+
Object.entries(this.table)
|
|
233
|
+
.filter(([, col]) => identityCols.has(col as SQLiteColumn))
|
|
207
234
|
.map(([key]) => key),
|
|
208
235
|
);
|
|
209
236
|
const defaultSet = Object.fromEntries(
|
|
210
237
|
Object.entries(data as Record<string, unknown>).filter(([key]) => !identityProps.has(key)),
|
|
211
238
|
);
|
|
212
239
|
const setOnConflict = { ...(updateColumns ?? defaultSet), updatedAt: now };
|
|
213
|
-
const rows = (await
|
|
214
|
-
|
|
215
|
-
onConflictDoUpdate: (cfg: { target: SQLiteColumn[]; set: unknown }) => {
|
|
216
|
-
returning: () => Promise<unknown[]>;
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
)
|
|
240
|
+
const rows = (await this.insertBuilder
|
|
241
|
+
.values(record)
|
|
220
242
|
.onConflictDoUpdate({ target: conflictColumns, set: setOnConflict })
|
|
221
243
|
.returning()) as TTable['$inferSelect'][];
|
|
222
244
|
return rows[0] as TTable['$inferSelect'];
|
|
@@ -261,13 +283,8 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
261
283
|
async update(id: PKValue, data: Partial<TTable['$inferInsert']>): Promise<TTable['$inferSelect'] | undefined> {
|
|
262
284
|
const updateData = { ...data, updatedAt: this.now() };
|
|
263
285
|
this.validate('update', updateData);
|
|
264
|
-
const rows = (await (
|
|
265
|
-
|
|
266
|
-
.update(this.table as unknown as Parameters<typeof this.db.update>[0])
|
|
267
|
-
.set(updateData) as unknown as {
|
|
268
|
-
where: (c: SQL) => { returning: () => Promise<unknown[]> };
|
|
269
|
-
}
|
|
270
|
-
)
|
|
286
|
+
const rows = (await (this.db.update(this.table as Parameters<typeof this.db.update>[0]) as UpdateBuilder)
|
|
287
|
+
.set(updateData)
|
|
271
288
|
.where(this.pkCondition(id))
|
|
272
289
|
.returning()) as TTable['$inferSelect'][];
|
|
273
290
|
return rows[0];
|
|
@@ -279,11 +296,9 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
279
296
|
if (useSoftDelete && this.hasSoftDelete) {
|
|
280
297
|
return this.update(id, { inUsed: 0 } as Partial<TTable['$inferInsert']>);
|
|
281
298
|
}
|
|
282
|
-
await (
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
).where(this.pkCondition(id));
|
|
299
|
+
await (this.db.delete(this.table as Parameters<typeof this.db.delete>[0]) as DeleteBuilder).where(
|
|
300
|
+
this.pkCondition(id),
|
|
301
|
+
);
|
|
287
302
|
return undefined;
|
|
288
303
|
}
|
|
289
304
|
|
|
@@ -315,25 +330,25 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
315
330
|
limit: spec.limit,
|
|
316
331
|
});
|
|
317
332
|
const last = rows[rows.length - 1] as Record<string, unknown> | undefined;
|
|
333
|
+
const cursorResultKey = this.resultKeyForColumn(spec.cursorColumn);
|
|
318
334
|
const nextCursor =
|
|
319
|
-
rows.length === spec.limit && last
|
|
320
|
-
? (last[(spec.cursorColumn as unknown as { name: string }).name] as string | number)
|
|
321
|
-
: undefined;
|
|
335
|
+
rows.length === spec.limit && last ? (last[cursorResultKey] as string | number | undefined) : undefined;
|
|
322
336
|
return nextCursor !== undefined ? { rows, nextCursor } : { rows };
|
|
323
337
|
}
|
|
324
338
|
|
|
339
|
+
private resultKeyForColumn(column: SQLiteColumn): string {
|
|
340
|
+
for (const [key, value] of Object.entries(this.table)) {
|
|
341
|
+
if (value === column) return key;
|
|
342
|
+
}
|
|
343
|
+
return (column as { name: string }).name;
|
|
344
|
+
}
|
|
345
|
+
|
|
325
346
|
/** Count records matching an optional predicate. */
|
|
326
347
|
async count(where?: Predicate, includeDeleted = false): Promise<number> {
|
|
327
348
|
const condition = this.withActive(where, includeDeleted);
|
|
328
349
|
const compiled = condition ? compilePredicate(condition) : undefined;
|
|
329
|
-
const base = (
|
|
330
|
-
|
|
331
|
-
select: (p: unknown) => { from: (t: unknown) => { where: (c: SQL) => Promise<unknown[]> } };
|
|
332
|
-
}
|
|
333
|
-
)
|
|
334
|
-
.select({ value: countFn() })
|
|
335
|
-
.from(this.table);
|
|
336
|
-
const result = (await (compiled ? base.where(compiled) : (base as unknown as Promise<unknown[]>))) as {
|
|
350
|
+
const base = (this.db as CountDb).select({ value: countFn() }).from(this.table);
|
|
351
|
+
const result = (await (compiled ? base.where(compiled) : base)) as {
|
|
337
352
|
value: number;
|
|
338
353
|
}[];
|
|
339
354
|
return result[0]?.value ?? 0;
|
|
@@ -342,7 +357,9 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
|
|
|
342
357
|
/** Combine a caller predicate with the soft-delete active filter. */
|
|
343
358
|
private withActive(where: Predicate | undefined, includeDeleted?: boolean): Predicate | undefined {
|
|
344
359
|
if (includeDeleted || !this.hasSoftDelete) return where;
|
|
345
|
-
const
|
|
360
|
+
const table = this.table;
|
|
361
|
+
if (!('inUsed' in table)) return where;
|
|
362
|
+
const active: Predicate = { col: table.inUsed as SQLiteColumn, op: 'eq', value: 1 };
|
|
346
363
|
if (!where) return active;
|
|
347
364
|
return { and: [where, active] };
|
|
348
365
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type InternalDb } from './adapter';
|
|
2
|
-
export { BunSqliteAdapter, type BunSqliteOptions } from './adapters/bun-sqlite';
|
|
3
2
|
export { D1Adapter } from './adapters/d1';
|
|
4
3
|
export { BaseDao, type TxHandle } from './base-dao';
|
|
5
|
-
|
|
4
|
+
|
|
6
5
|
export { type EmbeddedMigration, embeddedMigrations } from './embedded-migrations';
|
|
7
6
|
export {
|
|
8
7
|
type CursorListSpec,
|
package/src/queue-job-dao.ts
CHANGED
|
@@ -3,6 +3,58 @@ import type { DbAdapter } from './adapter';
|
|
|
3
3
|
import { EntityDao } from './entity-dao';
|
|
4
4
|
import { queueJobs } from './schema/queue-jobs';
|
|
5
5
|
|
|
6
|
+
type SelectGroupByQuery = {
|
|
7
|
+
groupBy: (group: unknown) => Promise<unknown[]>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SelectWhereQuery = {
|
|
11
|
+
where: (where: unknown) => Promise<unknown[]>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SelectReadyQuery = {
|
|
15
|
+
where: (where: unknown) => {
|
|
16
|
+
orderBy: (order: unknown) => { limit: (limit: number) => Promise<unknown[]> };
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type QueueSelectDb = {
|
|
21
|
+
select: (projection: unknown) => {
|
|
22
|
+
from: (table: unknown) => SelectGroupByQuery & SelectWhereQuery;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type QueueReadyDb = {
|
|
27
|
+
select: () => {
|
|
28
|
+
from: (table: unknown) => SelectReadyQuery;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type QueueUpdateReturningDb = {
|
|
33
|
+
update: (table: unknown) => {
|
|
34
|
+
set: (value: unknown) => {
|
|
35
|
+
where: (where: unknown) => {
|
|
36
|
+
returning: () => Promise<unknown[]>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type QueueUpdateVoidDb = {
|
|
43
|
+
update: (table: unknown) => {
|
|
44
|
+
set: (value: unknown) => {
|
|
45
|
+
where: (where: unknown) => Promise<unknown>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type QueueUpdateChangesDb = {
|
|
51
|
+
update: (table: unknown) => {
|
|
52
|
+
set: (value: unknown) => {
|
|
53
|
+
where: (where: unknown) => Promise<{ changes: number }>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
6
58
|
/**
|
|
7
59
|
* Aggregate queue statistics by job status.
|
|
8
60
|
*/
|
|
@@ -101,11 +153,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
101
153
|
* Get aggregate job counts by status.
|
|
102
154
|
*/
|
|
103
155
|
async getStats(): Promise<QueueStats> {
|
|
104
|
-
const result = await (
|
|
105
|
-
this.db as unknown as {
|
|
106
|
-
select: (fn: unknown) => { from: (t: unknown) => { groupBy: (g: unknown) => Promise<unknown[]> } };
|
|
107
|
-
}
|
|
108
|
-
)
|
|
156
|
+
const result = await (this.db as QueueSelectDb)
|
|
109
157
|
.select({
|
|
110
158
|
status: queueJobs.status,
|
|
111
159
|
count: sql`count(*)`,
|
|
@@ -128,11 +176,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
128
176
|
* Count jobs by status.
|
|
129
177
|
*/
|
|
130
178
|
async countByStatus(status: string): Promise<number> {
|
|
131
|
-
const result = await (
|
|
132
|
-
this.db as unknown as {
|
|
133
|
-
select: (fn: unknown) => { from: (t: unknown) => { where: (w: unknown) => Promise<unknown[]> } };
|
|
134
|
-
}
|
|
135
|
-
)
|
|
179
|
+
const result = await (this.db as QueueSelectDb)
|
|
136
180
|
.select({ value: sql`count(*)` })
|
|
137
181
|
.from(queueJobs)
|
|
138
182
|
.where(sql`${queueJobs.status} = ${status}`);
|
|
@@ -146,17 +190,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
146
190
|
async findPending(batchSize: number): Promise<QueueJobRecord[]> {
|
|
147
191
|
const now = this.now();
|
|
148
192
|
|
|
149
|
-
const result = await (
|
|
150
|
-
this.db as unknown as {
|
|
151
|
-
select: () => {
|
|
152
|
-
from: (t: unknown) => {
|
|
153
|
-
where: (w: unknown) => {
|
|
154
|
-
orderBy: (o: unknown) => { limit: (l: number) => Promise<unknown[]> };
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
)
|
|
193
|
+
const result = await (this.db as QueueReadyDb)
|
|
160
194
|
.select()
|
|
161
195
|
.from(queueJobs)
|
|
162
196
|
.where(
|
|
@@ -180,17 +214,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
180
214
|
|
|
181
215
|
const now = this.now();
|
|
182
216
|
|
|
183
|
-
const result = await (
|
|
184
|
-
this.db as unknown as {
|
|
185
|
-
update: (t: unknown) => {
|
|
186
|
-
set: (v: unknown) => {
|
|
187
|
-
where: (w: unknown) => {
|
|
188
|
-
returning: () => Promise<unknown[]>;
|
|
189
|
-
};
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
)
|
|
217
|
+
const result = await (this.db as QueueUpdateReturningDb)
|
|
194
218
|
.update(queueJobs)
|
|
195
219
|
.set({ status: 'processing', processingAt: now, updatedAt: now })
|
|
196
220
|
.where(
|
|
@@ -217,11 +241,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
217
241
|
|
|
218
242
|
const now = this.now();
|
|
219
243
|
|
|
220
|
-
await (
|
|
221
|
-
this.db as unknown as {
|
|
222
|
-
update: (t: unknown) => { set: (v: unknown) => { where: (w: unknown) => Promise<unknown> } };
|
|
223
|
-
}
|
|
224
|
-
)
|
|
244
|
+
await (this.db as QueueUpdateVoidDb)
|
|
225
245
|
.update(queueJobs)
|
|
226
246
|
.set({ status: 'processing', processingAt: now, updatedAt: now })
|
|
227
247
|
.where(and(inArray(queueJobs.id, ids), eq(queueJobs.status, 'pending')));
|
|
@@ -268,13 +288,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
268
288
|
async resetStuckJobs(visibilityTimeout: number): Promise<number> {
|
|
269
289
|
const cutoff = this.now() - visibilityTimeout;
|
|
270
290
|
|
|
271
|
-
const result = await (
|
|
272
|
-
this.db as unknown as {
|
|
273
|
-
update: (t: unknown) => {
|
|
274
|
-
set: (v: unknown) => { where: (w: unknown) => Promise<{ changes: number }> };
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
)
|
|
291
|
+
const result = await (this.db as QueueUpdateChangesDb)
|
|
278
292
|
.update(queueJobs)
|
|
279
293
|
.set({ status: 'pending', processingAt: null, updatedAt: this.now() })
|
|
280
294
|
.where(
|
|
@@ -293,13 +307,7 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
|
|
|
293
307
|
async failExpiredJobs(): Promise<number> {
|
|
294
308
|
const now = this.now();
|
|
295
309
|
|
|
296
|
-
const result = await (
|
|
297
|
-
this.db as unknown as {
|
|
298
|
-
update: (t: unknown) => {
|
|
299
|
-
set: (v: unknown) => { where: (w: unknown) => Promise<{ changes: number }> };
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
)
|
|
310
|
+
const result = await (this.db as QueueUpdateChangesDb)
|
|
303
311
|
.update(queueJobs)
|
|
304
312
|
.set({
|
|
305
313
|
status: 'failed',
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { getTableConfig, type SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Quote an identifier for use in SQL (double-quoted for SQLite compatibility).
|
|
5
|
+
*/
|
|
6
|
+
function quoteIdent(name: string): string {
|
|
7
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract the SQL string from a drizzle-orm SQL expression by walking its
|
|
12
|
+
* internal `queryChunks` — StringChunk values plus Param placeholders.
|
|
13
|
+
*/
|
|
14
|
+
function sqlToString(chunks: Array<{ value?: unknown; input?: unknown }>): string {
|
|
15
|
+
return chunks
|
|
16
|
+
.map((chunk) => {
|
|
17
|
+
// StringChunk — the literal SQL fragment
|
|
18
|
+
if ('value' in chunk && typeof chunk.value === 'string') {
|
|
19
|
+
return chunk.value;
|
|
20
|
+
}
|
|
21
|
+
// Param — use the input value if available
|
|
22
|
+
if ('input' in chunk && chunk.input !== undefined) {
|
|
23
|
+
return String(chunk.input);
|
|
24
|
+
}
|
|
25
|
+
return String(chunk.value ?? '?');
|
|
26
|
+
})
|
|
27
|
+
.join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map a column default value to its SQL literal representation.
|
|
32
|
+
*
|
|
33
|
+
* drizzle-orm defaults can be:
|
|
34
|
+
* - primitives: number, string, boolean, null
|
|
35
|
+
* - SQL expressions (sql\`...\` template results)
|
|
36
|
+
* - undefined (no default, or runtime-only $defaultFn)
|
|
37
|
+
*/
|
|
38
|
+
function defaultToSql(value: unknown): string | undefined {
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return value === null ? 'NULL' : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'boolean') {
|
|
49
|
+
return value ? '1' : '0';
|
|
50
|
+
}
|
|
51
|
+
// drizzle-orm SQL expression (has queryChunks)
|
|
52
|
+
if (typeof value === 'object' && value !== null && 'queryChunks' in value) {
|
|
53
|
+
const chunks = (value as Record<string, unknown>).queryChunks as
|
|
54
|
+
| Array<{ value?: unknown; input?: unknown }>
|
|
55
|
+
| undefined;
|
|
56
|
+
if (chunks) {
|
|
57
|
+
return sqlToString(chunks);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return String(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a drizzle table object to its string name.
|
|
65
|
+
*/
|
|
66
|
+
function getTableName(table: Record<string, unknown>): string {
|
|
67
|
+
// Drizzle tables store name at Symbol.for('drizzle:Name')
|
|
68
|
+
const nameSym = Symbol.for('drizzle:Name');
|
|
69
|
+
return String((table as unknown as Record<symbol, unknown>)[nameSym]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a `CREATE TABLE IF NOT EXISTS` DDL statement from a Drizzle SQLite table.
|
|
74
|
+
*
|
|
75
|
+
* Uses `getTableConfig` (drizzle-orm runtime introspection) to extract columns,
|
|
76
|
+
* types, constraints, and foreign keys — no drizzle-kit CLI required.
|
|
77
|
+
*
|
|
78
|
+
* The output is deterministic: columns are emitted in definition order, identifiers
|
|
79
|
+
* are double-quoted, and table constraints follow column definitions.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const users = sqliteTable('users', { id: text('id').primaryKey() });
|
|
84
|
+
* const ddl = generateCreateTableSql(users);
|
|
85
|
+
* // CREATE TABLE IF NOT EXISTS "users" ("id" text PRIMARY KEY NOT NULL)
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function generateCreateTableSql(table: SQLiteTable): string {
|
|
89
|
+
const config = getTableConfig(table);
|
|
90
|
+
|
|
91
|
+
const columnDefs: string[] = [];
|
|
92
|
+
const tableConstraints: string[] = [];
|
|
93
|
+
|
|
94
|
+
// Track which columns participate in composite unique constraints
|
|
95
|
+
const compositeUniqueCols = new Set<string>();
|
|
96
|
+
for (const uc of config.uniqueConstraints) {
|
|
97
|
+
if (uc.columns.length > 1) {
|
|
98
|
+
for (const col of uc.columns) {
|
|
99
|
+
compositeUniqueCols.add(col.name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Track which columns participate in composite primary keys
|
|
105
|
+
const compositePkCols = new Set<string>();
|
|
106
|
+
for (const pk of config.primaryKeys) {
|
|
107
|
+
if (pk.columns.length > 1) {
|
|
108
|
+
for (const col of pk.columns) {
|
|
109
|
+
compositePkCols.add(col.name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const col of config.columns) {
|
|
115
|
+
const parts: string[] = [quoteIdent(col.name), col.getSQLType()];
|
|
116
|
+
|
|
117
|
+
// Column-level PRIMARY KEY only for single-column PKs
|
|
118
|
+
if (col.primary && !compositePkCols.has(col.name)) {
|
|
119
|
+
parts.push('PRIMARY KEY');
|
|
120
|
+
}
|
|
121
|
+
if (col.notNull) {
|
|
122
|
+
parts.push('NOT NULL');
|
|
123
|
+
}
|
|
124
|
+
// DEFAULT — only for SQL-level defaults (not runtime $defaultFn)
|
|
125
|
+
if (col.hasDefault && col.default !== undefined) {
|
|
126
|
+
const sqlDefault = defaultToSql(col.default);
|
|
127
|
+
if (sqlDefault !== undefined) {
|
|
128
|
+
parts.push(`DEFAULT ${sqlDefault}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// UNIQUE at column level only when it's a single-column unique constraint
|
|
132
|
+
if (col.isUnique && !compositeUniqueCols.has(col.name)) {
|
|
133
|
+
parts.push('UNIQUE');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
columnDefs.push(parts.join(' '));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Composite PRIMARY KEY
|
|
140
|
+
for (const pk of config.primaryKeys) {
|
|
141
|
+
if (pk.columns.length > 1) {
|
|
142
|
+
const pkCols = pk.columns.map((c) => quoteIdent(c.name)).join(', ');
|
|
143
|
+
tableConstraints.push(`PRIMARY KEY (${pkCols})`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Composite UNIQUE constraints
|
|
148
|
+
for (const uc of config.uniqueConstraints) {
|
|
149
|
+
if (uc.columns.length > 1) {
|
|
150
|
+
const cols = uc.columns.map((c) => quoteIdent(c.name)).join(', ');
|
|
151
|
+
tableConstraints.push(`UNIQUE (${cols})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Foreign keys
|
|
156
|
+
for (const fk of config.foreignKeys) {
|
|
157
|
+
const ref = fk.reference();
|
|
158
|
+
const localCols = ref.columns.map((c) => quoteIdent(c.name)).join(', ');
|
|
159
|
+
const foreignCols = ref.foreignColumns.map((c) => quoteIdent(c.name)).join(', ');
|
|
160
|
+
const foreignTableName = getTableName(ref.foreignTable as unknown as Record<string, symbol | unknown>);
|
|
161
|
+
|
|
162
|
+
let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${quoteIdent(foreignTableName)} (${foreignCols})`;
|
|
163
|
+
if (fk.onDelete) {
|
|
164
|
+
constraint += ` ON DELETE ${fk.onDelete}`;
|
|
165
|
+
}
|
|
166
|
+
if (fk.onUpdate) {
|
|
167
|
+
constraint += ` ON UPDATE ${fk.onUpdate}`;
|
|
168
|
+
}
|
|
169
|
+
tableConstraints.push(constraint);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const allDefs = [...columnDefs, ...tableConstraints];
|
|
173
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteIdent(config.name)} (\n ${allDefs.join(',\n ')}\n)`;
|
|
174
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { type SQLiteColumnBuilderBase, sqliteTable } from 'drizzle-orm/sqlite-core';
|
|
2
2
|
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
|
3
3
|
import type { ZodType } from 'zod';
|
|
4
|
+
import { generateCreateTableSql } from './ddl';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* A table definition bundled with its drizzle-zod validation schemas.
|
|
7
|
+
* A table definition bundled with its drizzle-zod validation schemas and DDL.
|
|
7
8
|
*
|
|
8
9
|
* The single source of truth (G2): one table authored once yields the drizzle
|
|
9
|
-
* table (for queries/migrations)
|
|
10
|
-
* validation), with no parallel
|
|
10
|
+
* table (for queries/migrations), insert/select zod schemas (for boundary
|
|
11
|
+
* validation), and CREATE TABLE DDL (for migrations) — with no parallel
|
|
12
|
+
* re-authoring.
|
|
11
13
|
*
|
|
12
|
-
* The zod schemas are derived lazily — only materialised the first
|
|
13
|
-
* read — so a consumer that only needs the table pays nothing
|
|
14
|
+
* The zod schemas and DDL are derived lazily — only materialised the first
|
|
15
|
+
* time they are read — so a consumer that only needs the table pays nothing
|
|
16
|
+
* extra.
|
|
14
17
|
*
|
|
15
|
-
* `defineTable`, `insertSchema`, and `
|
|
16
|
-
* `zod` and `drizzle-zod`. Consumers that never validate
|
|
17
|
-
*
|
|
18
|
+
* `defineTable`, `insertSchema`, `selectSchema`, and `createTableSql` require
|
|
19
|
+
* the optional peers `zod` and `drizzle-zod`. Consumers that never validate and
|
|
20
|
+
* don't need generated DDL can use `createDbAdapter` + raw `sqliteTable` +
|
|
21
|
+
* column helpers without installing those peers. Import from
|
|
22
|
+
* `@gobing-ai/ts-db/schema` to opt in.
|
|
18
23
|
*/
|
|
19
24
|
export interface DefinedTable<TTable> {
|
|
20
25
|
/** The underlying drizzle table — pass to DAOs, migrations, queries. */
|
|
@@ -23,13 +28,17 @@ export interface DefinedTable<TTable> {
|
|
|
23
28
|
readonly insertSchema: ZodType;
|
|
24
29
|
/** Zod schema validating a selected row. */
|
|
25
30
|
readonly selectSchema: ZodType;
|
|
31
|
+
/** `CREATE TABLE IF NOT EXISTS` DDL generated from the table definition (lazy). */
|
|
32
|
+
readonly createTableSql: string;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/**
|
|
29
|
-
* Define a SQLite table and derive its validation schemas in one place (G2).
|
|
36
|
+
* Define a SQLite table and derive its validation schemas and DDL in one place (G2).
|
|
30
37
|
*
|
|
31
38
|
* @example
|
|
32
39
|
* ```ts
|
|
40
|
+
* import { defineTable } from '@gobing-ai/ts-db/schema';
|
|
41
|
+
*
|
|
33
42
|
* export const users = defineTable('users', {
|
|
34
43
|
* id: text('id').primaryKey(),
|
|
35
44
|
* email: text('email').notNull().unique(),
|
|
@@ -37,6 +46,7 @@ export interface DefinedTable<TTable> {
|
|
|
37
46
|
* });
|
|
38
47
|
* users.table // drizzle table for DAOs/migrations
|
|
39
48
|
* users.insertSchema // zod schema derived from the table
|
|
49
|
+
* users.createTableSql // CREATE TABLE IF NOT EXISTS "users" (...)
|
|
40
50
|
* ```
|
|
41
51
|
*/
|
|
42
52
|
export function defineTable<TName extends string, TColumns extends Record<string, SQLiteColumnBuilderBase>>(
|
|
@@ -47,6 +57,7 @@ export function defineTable<TName extends string, TColumns extends Record<string
|
|
|
47
57
|
|
|
48
58
|
let insert: ZodType | undefined;
|
|
49
59
|
let select: ZodType | undefined;
|
|
60
|
+
let ddl: string | undefined;
|
|
50
61
|
|
|
51
62
|
return {
|
|
52
63
|
table,
|
|
@@ -62,5 +73,11 @@ export function defineTable<TName extends string, TColumns extends Record<string
|
|
|
62
73
|
}
|
|
63
74
|
return select;
|
|
64
75
|
},
|
|
76
|
+
get createTableSql(): string {
|
|
77
|
+
if (ddl === undefined) {
|
|
78
|
+
ddl = generateCreateTableSql(table);
|
|
79
|
+
}
|
|
80
|
+
return ddl;
|
|
81
|
+
},
|
|
65
82
|
};
|
|
66
83
|
}
|
package/src/schema/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { queueJobs } from './queue-jobs';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-table.d.ts","sourceRoot":"","sources":["../src/define-table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,uBAAuB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY,CAAC,MAAM;IAChC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,EACtG,IAAI,EAAE,KAAK,EACX,OAAO,EAAE,QAAQ,GAClB,YAAY,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAqB/D"}
|