@gobing-ai/ts-db 0.1.7 → 0.2.1

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/dist/index.js CHANGED
@@ -2,9 +2,11 @@ export { createDbAdapter } from './adapter.js';
2
2
  export { BunSqliteAdapter } from './adapters/bun-sqlite.js';
3
3
  export { D1Adapter } from './adapters/d1.js';
4
4
  export { BaseDao } from './base-dao.js';
5
+ export { defineTable } from './define-table.js';
5
6
  export { embeddedMigrations } from './embedded-migrations.js';
6
- export { EntityDao } from './entity-dao.js';
7
+ export { EntityDao, } from './entity-dao.js';
7
8
  export { applyMigrations } from './migrate.js';
9
+ export { compileOrderBy, compilePredicate, } from './query-spec.js';
8
10
  export { QueueJobDao } from './queue-job-dao.js';
9
11
  export { appendOnlyColumns, buildAppendOnlyColumns, buildStandardColumns, buildStandardColumnsWithSoftDelete, nowTimestamp, standardColumns, standardColumnsWithSoftDelete, } from './schema/common.js';
10
12
  export { queueJobs } from './schema/queue-jobs.js';
@@ -0,0 +1,57 @@
1
+ import { type SQL } from 'drizzle-orm';
2
+ import type { SQLiteColumn } from 'drizzle-orm/sqlite-core';
3
+ /**
4
+ * A column reference for the ts-db predicate spec.
5
+ *
6
+ * Consumers obtain these from their own ts-db table definitions (e.g. `users.email`),
7
+ * never by importing from `drizzle-orm` — keeping the drizzle dependency internal.
8
+ */
9
+ export type ColRef = SQLiteColumn;
10
+ /** Binary comparison operators supported by the predicate spec. */
11
+ export type ComparisonOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'like';
12
+ /**
13
+ * Drizzle-free predicate vocabulary for `where`/`count` filters.
14
+ *
15
+ * Deliberately small (the "lean facade"): enough for the common 90% of filters.
16
+ * Anything beyond (joins, aggregates, window functions) belongs in a named DAO
17
+ * method that uses drizzle privately, never in this public spec.
18
+ */
19
+ export type Predicate = {
20
+ col: ColRef;
21
+ op: ComparisonOp;
22
+ value: unknown;
23
+ } | {
24
+ col: ColRef;
25
+ op: 'in';
26
+ values: readonly unknown[];
27
+ } | {
28
+ col: ColRef;
29
+ op: 'isNull' | 'isNotNull';
30
+ } | {
31
+ and: readonly Predicate[];
32
+ } | {
33
+ or: readonly Predicate[];
34
+ };
35
+ /** A single ordering term: a column and an optional direction (default `asc`). */
36
+ export interface OrderTerm {
37
+ col: ColRef;
38
+ dir?: 'asc' | 'desc';
39
+ }
40
+ /** Options accepted by the structured `list` operation. */
41
+ export interface ListSpec {
42
+ where?: Predicate;
43
+ orderBy?: readonly OrderTerm[];
44
+ limit?: number;
45
+ offset?: number;
46
+ includeDeleted?: boolean;
47
+ }
48
+ /**
49
+ * Compile a ts-db {@link Predicate} into a drizzle `SQL` condition.
50
+ *
51
+ * Internal to ts-db — consumers never see drizzle's `SQL` type. Returns
52
+ * `undefined` for an empty `and`/`or` group so callers can omit the filter.
53
+ */
54
+ export declare function compilePredicate(predicate: Predicate): SQL | undefined;
55
+ /** Compile a list of {@link OrderTerm}s into drizzle order-by `SQL` clauses. */
56
+ export declare function compileOrderBy(orderBy: readonly OrderTerm[]): SQL[];
57
+ //# sourceMappingURL=query-spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-spec.d.ts","sourceRoot":"","sources":["../src/query-spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkF,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AACvH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,YAAY,CAAC;AAElC,mEAAmE;AACnE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GACf;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,GACrD;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,QAAQ,GAAG,WAAW,CAAA;CAAE,GAC3C;IAAE,GAAG,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,CAAC;AAEnC,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB;AAED,2DAA2D;AAC3D,MAAM,WAAW,QAAQ;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAYD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,GAAG,GAAG,GAAG,SAAS,CAmBtE;AAED,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,GAAG,EAAE,CAEnE"}
@@ -0,0 +1,40 @@
1
+ import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, or } from 'drizzle-orm';
2
+ const COMPARISON_BUILDERS = {
3
+ eq: (col, value) => eq(col, value),
4
+ ne: (col, value) => ne(col, value),
5
+ gt: (col, value) => gt(col, value),
6
+ gte: (col, value) => gte(col, value),
7
+ lt: (col, value) => lt(col, value),
8
+ lte: (col, value) => lte(col, value),
9
+ like: (col, value) => like(col, value),
10
+ };
11
+ /**
12
+ * Compile a ts-db {@link Predicate} into a drizzle `SQL` condition.
13
+ *
14
+ * Internal to ts-db — consumers never see drizzle's `SQL` type. Returns
15
+ * `undefined` for an empty `and`/`or` group so callers can omit the filter.
16
+ */
17
+ export function compilePredicate(predicate) {
18
+ if ('and' in predicate) {
19
+ const parts = predicate.and.map(compilePredicate).filter((p) => p !== undefined);
20
+ return parts.length > 0 ? and(...parts) : undefined;
21
+ }
22
+ if ('or' in predicate) {
23
+ const parts = predicate.or.map(compilePredicate).filter((p) => p !== undefined);
24
+ return parts.length > 0 ? or(...parts) : undefined;
25
+ }
26
+ switch (predicate.op) {
27
+ case 'isNull':
28
+ return isNull(predicate.col);
29
+ case 'isNotNull':
30
+ return isNotNull(predicate.col);
31
+ case 'in':
32
+ return inArray(predicate.col, predicate.values);
33
+ default:
34
+ return COMPARISON_BUILDERS[predicate.op](predicate.col, predicate.value);
35
+ }
36
+ }
37
+ /** Compile a list of {@link OrderTerm}s into drizzle order-by `SQL` clauses. */
38
+ export function compileOrderBy(orderBy) {
39
+ return orderBy.map((term) => (term.dir === 'desc' ? desc(term.col) : asc(term.col)));
40
+ }
@@ -1,4 +1,4 @@
1
- import type { DbClient } from './adapter';
1
+ import type { DbAdapter } from './adapter';
2
2
  import { EntityDao } from './entity-dao';
3
3
  import { queueJobs } from './schema/queue-jobs';
4
4
  /**
@@ -21,7 +21,7 @@ export type QueueJobRecord = typeof queueJobs.$inferSelect;
21
21
  * methods for job lifecycle management (enqueue, process, retry, fail).
22
22
  */
23
23
  export declare class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id> {
24
- constructor(db: DbClient);
24
+ constructor(adapter: DbAdapter);
25
25
  /**
26
26
  * Enqueue a new job.
27
27
  */
@@ -1 +1 @@
1
- {"version":3,"file":"queue-job-dao.d.ts","sourceRoot":"","sources":["../src/queue-job-dao.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AAE3D;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,SAAS,CAAC,OAAO,SAAS,EAAE,OAAO,SAAS,CAAC,EAAE,CAAC;gBACjE,EAAE,EAAE,QAAQ;IAIxB;;OAEG;IACG,OAAO,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAClE,OAAO,CAAC,MAAM,CAAC;IAkBlB;;OAEG;IACG,YAAY,CACd,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAC1G,OAAO,CAAC,MAAM,EAAE,CAAC;IA+BpB;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI9D;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAwBrC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAapD;;OAEG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyB/D;;;;;OAKG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAmC9D;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAelD;;OAEG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5E;;OAEG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU1G;;OAEG;IACG,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBhE;;;;;OAKG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;CAwB3C"}
1
+ {"version":3,"file":"queue-job-dao.d.ts","sourceRoot":"","sources":["../src/queue-job-dao.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AAE3D;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,SAAS,CAAC,OAAO,SAAS,EAAE,OAAO,SAAS,CAAC,EAAE,CAAC;gBACjE,OAAO,EAAE,SAAS;IAI9B;;OAEG;IACG,OAAO,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAClE,OAAO,CAAC,MAAM,CAAC;IAkBlB;;OAEG;IACG,YAAY,CACd,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAC1G,OAAO,CAAC,MAAM,EAAE,CAAC;IA+BpB;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI9D;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAwBrC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAapD;;OAEG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyB/D;;;;;OAKG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAmC9D;;OAEG;IACG,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAelD;;OAEG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5E;;OAEG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU1G;;OAEG;IACG,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBhE;;;;;OAKG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;CAwB3C"}
@@ -8,8 +8,8 @@ import { queueJobs } from './schema/queue-jobs.js';
8
8
  * methods for job lifecycle management (enqueue, process, retry, fail).
9
9
  */
10
10
  export class QueueJobDao extends EntityDao {
11
- constructor(db) {
12
- super(db, queueJobs, queueJobs.id, 'queue_jobs');
11
+ constructor(adapter) {
12
+ super(adapter, queueJobs, [queueJobs.id], 'queue_jobs');
13
13
  }
14
14
  /**
15
15
  * Enqueue a new job.
@@ -50,7 +50,7 @@ export class QueueJobDao extends EntityDao {
50
50
  };
51
51
  });
52
52
  if (rows.length > 0) {
53
- await this.withTransaction(async (tx) => {
53
+ await this.tx(async (tx) => {
54
54
  for (const row of rows) {
55
55
  await tx.insert(queueJobs).values(row);
56
56
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-db",
3
- "version": "0.1.7",
4
- "description": "@gobing-ai/ts-db — Database abstraction layer with Drizzle ORM adapters, generic DAOs, and migration tooling.",
3
+ "version": "0.2.1",
4
+ "description": "@gobing-ai/ts-db — a drizzle-free database facade: typed DAOs over Bun SQLite / Cloudflare D1, a small predicate query spec, single-source-of-truth tables, and migrations. Drizzle stays an internal detail.",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "database",
@@ -50,15 +50,27 @@
50
50
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-db-v<version> && git push --tags' && exit 1"
51
51
  },
52
52
  "dependencies": {
53
- "@gobing-ai/ts-runtime": "^0.1.0"
53
+ "@gobing-ai/ts-runtime": "^0.2.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "drizzle-orm": ">=0.38.0"
56
+ "drizzle-orm": ">=0.38.0",
57
+ "drizzle-zod": ">=0.5.0",
58
+ "zod": ">=3.23.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "drizzle-zod": {
62
+ "optional": true
63
+ },
64
+ "zod": {
65
+ "optional": true
66
+ }
57
67
  },
58
68
  "devDependencies": {
59
69
  "@types/bun": "1.3.14",
60
70
  "drizzle-kit": "^0.30.0",
61
- "drizzle-orm": "^0.38.0"
71
+ "drizzle-orm": "^0.38.0",
72
+ "drizzle-zod": "^0.5.1",
73
+ "zod": "^4.1.0"
62
74
  },
63
75
  "publishConfig": {
64
76
  "access": "public"
package/src/adapter.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
2
+ import type { DrizzleD1Database } from 'drizzle-orm/d1';
3
+
1
4
  /**
2
5
  * Minimal D1 binding interface — avoids depending on @cloudflare/workers-types.
3
6
  */
@@ -7,70 +10,40 @@ interface D1Binding {
7
10
  }
8
11
 
9
12
  /**
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.
13
+ * Internal typed drizzle database handle.
14
+ *
15
+ * This is the REAL drizzle database (bun:sqlite or D1 flavour), fully typed.
16
+ * It is `@internal` — ts-db's DAO base classes use it to build queries, but it
17
+ * is never part of the public API. Consumers depend only on the ts-db facade
18
+ * (DAOs, the predicate spec), never on drizzle types directly. (G1)
19
+ *
20
+ * @internal
51
21
  */
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
- }
22
+ export type InternalDb = BunSQLiteDatabase<Record<string, unknown>> | DrizzleD1Database<Record<string, unknown>>;
63
23
 
64
24
  /**
65
- * Database adapter providing a unified client, raw SQL exec, and lifecycle management.
25
+ * Database adapter: construction, lifecycle, the internal typed drizzle db, and a
26
+ * raw string-SQL escape for DDL / dynamic identifiers.
27
+ *
28
+ * The internal drizzle db (`db`) is exposed only to ts-db's own DAO layer; the
29
+ * string-SQL methods are the sole raw escape, intended for DDL and dynamic
30
+ * identifiers and gated to DAO files by a consumer-side lint rule.
66
31
  */
67
32
  export interface DbAdapter {
68
- getDb(): DbClient;
33
+ /**
34
+ * The internal typed drizzle database. ts-db DAO layer only — not public API.
35
+ * @internal
36
+ */
37
+ readonly db: InternalDb;
38
+ /** Run a raw SQL statement with no parameters (DDL). */
69
39
  exec(sql: string): Promise<void>;
70
40
  /** Parameterized write (INSERT/UPDATE/DELETE) that returns no rows. */
71
41
  run(sql: string, ...params: unknown[]): Promise<void>;
42
+ /** Parameterized read returning the first row, or undefined. */
72
43
  queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
44
+ /** Parameterized read returning all rows. */
73
45
  queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
46
+ /** Close the underlying connection. */
74
47
  close(): void;
75
48
  }
76
49
 
@@ -1,7 +1,7 @@
1
1
  import { Database } from 'bun:sqlite';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { type BunSQLiteDatabase, drizzle } from 'drizzle-orm/bun-sqlite';
4
- import type { DbAdapter, DbClient } from '../adapter';
4
+ import type { DbAdapter, InternalDb } from '../adapter';
5
5
  import * as schema from '../schema/index';
6
6
 
7
7
  type SqliteStatementLike = {
@@ -74,8 +74,9 @@ export class BunSqliteAdapter implements DbAdapter {
74
74
  this.drizzleDb = drizzle({ client: this.sqlite, schema });
75
75
  }
76
76
 
77
- getDb(): DbClient {
78
- return this.drizzleDb as unknown as DbClient;
77
+ /** The internal typed drizzle database (ts-db DAO layer + migrations only). */
78
+ get db(): InternalDb {
79
+ return this.drizzleDb as unknown as InternalDb;
79
80
  }
80
81
 
81
82
  /** Returns the underlying drizzle instance for migration operations. */
@@ -1,5 +1,5 @@
1
1
  import { type DrizzleD1Database, drizzle } from 'drizzle-orm/d1';
2
- import type { DbAdapter, DbClient } from '../adapter';
2
+ import type { DbAdapter, InternalDb } from '../adapter';
3
3
  import * as schema from '../schema/index';
4
4
 
5
5
  /**
@@ -36,8 +36,14 @@ export class D1Adapter implements DbAdapter {
36
36
  this.drizzleDb = drizzle(this.binding, { schema });
37
37
  }
38
38
 
39
- getDb(): DbClient {
40
- return this.drizzleDb as unknown as DbClient;
39
+ /** The internal typed drizzle database (ts-db DAO layer only). */
40
+ get db(): InternalDb {
41
+ return this.drizzleDb as unknown as InternalDb;
42
+ }
43
+
44
+ /** Returns the underlying drizzle instance for migration operations. */
45
+ getDrizzleDb(): DrizzleD1Database<typeof schema> {
46
+ return this.drizzleDb;
41
47
  }
42
48
 
43
49
  /** Returns the non-mutating binding for advanced direct D1 calls. */
package/src/base-dao.ts CHANGED
@@ -1,18 +1,36 @@
1
- import type { DbClient } from './adapter';
1
+ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
2
+ import type { DbAdapter, InternalDb } from './adapter';
3
+ import { compileOrderBy, compilePredicate, type ListSpec, type Predicate } from './query-spec';
2
4
 
3
5
  /**
4
- * Abstract base DAO providing transaction and timestamp utilities to all entity DAOs.
6
+ * A transaction-scoped handle passed to {@link BaseDao.tx} callbacks.
7
+ *
8
+ * Drizzle-free by design — exposes the same internal db shape DAOs use, so a
9
+ * transactional block can run the same structured/raw operations. (G3)
10
+ *
11
+ * @internal
12
+ */
13
+ export type TxHandle = InternalDb;
14
+
15
+ /**
16
+ * Abstract base DAO — the RAW tier of the ts-db facade.
17
+ *
18
+ * Owns the adapter and provides generic, table-agnostic data access:
19
+ * transactions and parameterized queries expressed through the ts-db predicate
20
+ * spec (never drizzle's `sql` tag). ETL / analytics / reporting DAOs extend this
21
+ * directly; {@link EntityDao} extends it to add typed CRUD over a single table.
22
+ *
23
+ * All signatures are ts-db's own vocabulary — drizzle never leaks to consumers. (G1)
5
24
  */
6
25
  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) {}
26
+ protected constructor(protected readonly adapter: DbAdapter) {}
27
+
28
+ /** The internal typed drizzle db. Subclasses use it to build queries. @internal */
29
+ protected get db(): InternalDb {
30
+ return this.adapter.db;
31
+ }
15
32
 
33
+ /** Current timestamp in milliseconds (audit columns, etc.). */
16
34
  protected now(): number {
17
35
  return Date.now();
18
36
  }
@@ -20,18 +38,42 @@ export abstract class BaseDao {
20
38
  /**
21
39
  * Execute a function within a database transaction.
22
40
  *
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`.
41
+ * Works uniformly on bun:sqlite (sync, wrapped in a promise) and D1 (async).
42
+ * The callback receives a transaction-scoped db handle.
28
43
  */
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) =>
44
+ protected async tx<T>(fn: (tx: TxHandle) => Promise<T>): Promise<T> {
45
+ return (this.db as { transaction: (cb: (tx: TxHandle) => Promise<T>) => Promise<T> }).transaction((tx) =>
34
46
  fn(tx),
35
47
  );
36
48
  }
49
+
50
+ /**
51
+ * Run a SELECT against a table, filtered/ordered/paged by a {@link ListSpec}.
52
+ *
53
+ * The raw-tier read primitive: drizzle-free input, typed rows out. Subclasses
54
+ * pass their table; `EntityDao` builds on this for `list`.
55
+ */
56
+ protected async query<T>(table: SQLiteTable, spec: ListSpec = {}): Promise<T[]> {
57
+ const condition = spec.where ? compilePredicate(spec.where) : undefined;
58
+ const order = spec.orderBy ? compileOrderBy(spec.orderBy) : [];
59
+
60
+ // drizzle's fluent builder is internal here; the public input is the spec.
61
+ let q = (this.db as InternalDb).select().from(table) as unknown as {
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
+ };
67
+ if (condition) q = q.where(condition);
68
+ if (order.length > 0) q = q.orderBy(...order);
69
+ if (spec.limit !== undefined) q = q.limit(spec.limit);
70
+ if (spec.offset !== undefined) q = q.offset(spec.offset);
71
+ return (await (q as unknown as Promise<T[]>)) ?? [];
72
+ }
73
+
74
+ /** Run a SELECT and return the first matching row, or undefined. */
75
+ protected async one<T>(table: SQLiteTable, where: Predicate): Promise<T | undefined> {
76
+ const rows = await this.query<T>(table, { where, limit: 1 });
77
+ return rows[0];
78
+ }
37
79
  }
@@ -0,0 +1,66 @@
1
+ import { type SQLiteColumnBuilderBase, sqliteTable } from 'drizzle-orm/sqlite-core';
2
+ import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
3
+ import type { ZodType } from 'zod';
4
+
5
+ /**
6
+ * A table definition bundled with its drizzle-zod validation schemas.
7
+ *
8
+ * The single source of truth (G2): one table authored once yields the drizzle
9
+ * table (for queries/migrations) plus insert/select zod schemas (for boundary
10
+ * validation), with no parallel re-authoring.
11
+ *
12
+ * The zod schemas are derived lazily — only materialised the first time they are
13
+ * read — so a consumer that only needs the table pays nothing extra.
14
+ *
15
+ * `defineTable`, `insertSchema`, and `selectSchema` require the optional peers
16
+ * `zod` and `drizzle-zod`. Consumers that never validate need not install them
17
+ * and simply use `createDbAdapter` + raw `sqliteTable` + the column helpers.
18
+ */
19
+ export interface DefinedTable<TTable> {
20
+ /** The underlying drizzle table — pass to DAOs, migrations, queries. */
21
+ readonly table: TTable;
22
+ /** Zod schema validating a row for insertion. */
23
+ readonly insertSchema: ZodType;
24
+ /** Zod schema validating a selected row. */
25
+ readonly selectSchema: ZodType;
26
+ }
27
+
28
+ /**
29
+ * Define a SQLite table and derive its validation schemas in one place (G2).
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * export const users = defineTable('users', {
34
+ * id: text('id').primaryKey(),
35
+ * email: text('email').notNull().unique(),
36
+ * ...standardColumns,
37
+ * });
38
+ * users.table // drizzle table for DAOs/migrations
39
+ * users.insertSchema // zod schema derived from the table
40
+ * ```
41
+ */
42
+ export function defineTable<TName extends string, TColumns extends Record<string, SQLiteColumnBuilderBase>>(
43
+ name: TName,
44
+ columns: TColumns,
45
+ ): DefinedTable<ReturnType<typeof sqliteTable<TName, TColumns>>> {
46
+ const table = sqliteTable(name, columns);
47
+
48
+ let insert: ZodType | undefined;
49
+ let select: ZodType | undefined;
50
+
51
+ return {
52
+ table,
53
+ get insertSchema(): ZodType {
54
+ if (insert === undefined) {
55
+ insert = createInsertSchema(table) as unknown as ZodType;
56
+ }
57
+ return insert;
58
+ },
59
+ get selectSchema(): ZodType {
60
+ if (select === undefined) {
61
+ select = createSelectSchema(table) as unknown as ZodType;
62
+ }
63
+ return select;
64
+ },
65
+ };
66
+ }