@gobing-ai/ts-db 0.2.7 → 0.2.9

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 (48) hide show
  1. package/README.md +53 -2
  2. package/dist/drizzle-builders.d.ts +96 -0
  3. package/dist/drizzle-builders.d.ts.map +1 -0
  4. package/dist/drizzle-builders.js +17 -0
  5. package/dist/embedded-migrations.d.ts.map +1 -1
  6. package/dist/embedded-migrations.js +10 -0
  7. package/dist/entity-dao.d.ts.map +1 -1
  8. package/dist/inbox-message-dao.d.ts +19 -0
  9. package/dist/inbox-message-dao.d.ts.map +1 -0
  10. package/dist/inbox-message-dao.js +75 -0
  11. package/dist/inbox.d.ts +3 -0
  12. package/dist/inbox.d.ts.map +1 -0
  13. package/dist/inbox.js +2 -0
  14. package/dist/index.d.ts +3 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/migrate.d.ts +12 -5
  18. package/dist/migrate.d.ts.map +1 -1
  19. package/dist/migrate.js +19 -25
  20. package/dist/queue-job-dao.d.ts.map +1 -1
  21. package/dist/schema/ddl.d.ts.map +1 -1
  22. package/dist/schema/ddl.js +4 -36
  23. package/dist/schema/drizzle-internals.d.ts +25 -0
  24. package/dist/schema/drizzle-internals.d.ts.map +1 -0
  25. package/dist/schema/drizzle-internals.js +47 -0
  26. package/dist/schema/inbox-messages.d.ts +212 -0
  27. package/dist/schema/inbox-messages.d.ts.map +1 -0
  28. package/dist/schema/inbox-messages.js +17 -0
  29. package/dist/schema/index.d.ts +1 -0
  30. package/dist/schema/index.d.ts.map +1 -1
  31. package/dist/schema/index.js +1 -0
  32. package/dist/schema/runtime.d.ts +1 -0
  33. package/dist/schema/runtime.d.ts.map +1 -1
  34. package/dist/schema/runtime.js +1 -0
  35. package/package.json +7 -2
  36. package/src/drizzle-builders.ts +98 -0
  37. package/src/embedded-migrations.ts +10 -0
  38. package/src/entity-dao.ts +2 -21
  39. package/src/inbox-message-dao.ts +91 -0
  40. package/src/inbox.ts +2 -0
  41. package/src/index.ts +3 -1
  42. package/src/migrate.ts +34 -22
  43. package/src/queue-job-dao.ts +14 -59
  44. package/src/schema/ddl.ts +4 -40
  45. package/src/schema/drizzle-internals.ts +55 -0
  46. package/src/schema/inbox-messages.ts +22 -0
  47. package/src/schema/index.ts +1 -0
  48. package/src/schema/runtime.ts +1 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Quarantine for the *unversioned* drizzle-orm internal shapes ts-db reaches into
3
+ * for DDL generation.
4
+ *
5
+ * Everything else in ts-db consumes drizzle's public API (`getTableConfig`,
6
+ * column accessors). These two helpers are the only places that touch private
7
+ * structure — the SQL-expression `queryChunks` array and the
8
+ * `Symbol.for('drizzle:Name')` table-name slot — neither of which is covered by
9
+ * drizzle's semver contract. Keeping both here means a drizzle bump that changes
10
+ * an internal shape breaks in ONE file with ONE focused test, instead of silently
11
+ * mis-generating DDL across `ddl.ts`. If this module starts failing after an
12
+ * upgrade, the fix lives here.
13
+ */
14
+ /**
15
+ * Render a drizzle `sql\`...\`` expression to its literal SQL text by walking its
16
+ * internal `queryChunks`. Used to emit SQL-level column defaults
17
+ * (e.g. `DEFAULT (unixepoch())`) into generated DDL.
18
+ *
19
+ * Returns `undefined` when `value` is not a drizzle SQL expression with chunks,
20
+ * so callers can fall through to their primitive/`String()` handling.
21
+ */
22
+ export declare function sqlExpressionToText(value: unknown): string | undefined;
23
+ /** Resolve a drizzle table object to its declared name (stored at a private symbol). */
24
+ export declare function getDrizzleTableName(table: object): string;
25
+ //# sourceMappingURL=drizzle-internals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle-internals.d.ts","sourceRoot":"","sources":["../../src/schema/drizzle-internals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAoBtE;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGzD"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Quarantine for the *unversioned* drizzle-orm internal shapes ts-db reaches into
3
+ * for DDL generation.
4
+ *
5
+ * Everything else in ts-db consumes drizzle's public API (`getTableConfig`,
6
+ * column accessors). These two helpers are the only places that touch private
7
+ * structure — the SQL-expression `queryChunks` array and the
8
+ * `Symbol.for('drizzle:Name')` table-name slot — neither of which is covered by
9
+ * drizzle's semver contract. Keeping both here means a drizzle bump that changes
10
+ * an internal shape breaks in ONE file with ONE focused test, instead of silently
11
+ * mis-generating DDL across `ddl.ts`. If this module starts failing after an
12
+ * upgrade, the fix lives here.
13
+ */
14
+ /**
15
+ * Render a drizzle `sql\`...\`` expression to its literal SQL text by walking its
16
+ * internal `queryChunks`. Used to emit SQL-level column defaults
17
+ * (e.g. `DEFAULT (unixepoch())`) into generated DDL.
18
+ *
19
+ * Returns `undefined` when `value` is not a drizzle SQL expression with chunks,
20
+ * so callers can fall through to their primitive/`String()` handling.
21
+ */
22
+ export function sqlExpressionToText(value) {
23
+ if (typeof value !== 'object' || value === null || !('queryChunks' in value)) {
24
+ return undefined;
25
+ }
26
+ const chunks = value.queryChunks;
27
+ if (!chunks)
28
+ return undefined;
29
+ return chunks
30
+ .map((chunk) => {
31
+ // StringChunk — the literal SQL fragment
32
+ if ('value' in chunk && typeof chunk.value === 'string') {
33
+ return chunk.value;
34
+ }
35
+ // Param — use the input value if available
36
+ if ('input' in chunk && chunk.input !== undefined) {
37
+ return String(chunk.input);
38
+ }
39
+ return String(chunk.value ?? '?');
40
+ })
41
+ .join('');
42
+ }
43
+ /** Resolve a drizzle table object to its declared name (stored at a private symbol). */
44
+ export function getDrizzleTableName(table) {
45
+ const nameSym = Symbol.for('drizzle:Name');
46
+ return String(table[nameSym]);
47
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Drizzle schema definition for durable inter-agent inbox messages.
3
+ */
4
+ export declare const inboxMessages: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
5
+ name: "inbox_messages";
6
+ schema: undefined;
7
+ columns: {
8
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
9
+ name: "id";
10
+ tableName: "inbox_messages";
11
+ dataType: "string";
12
+ columnType: "SQLiteText";
13
+ data: string;
14
+ driverParam: string;
15
+ notNull: true;
16
+ hasDefault: false;
17
+ isPrimaryKey: true;
18
+ isAutoincrement: false;
19
+ hasRuntimeDefault: false;
20
+ enumValues: [string, ...string[]];
21
+ baseColumn: never;
22
+ identity: undefined;
23
+ generated: undefined;
24
+ }, {}, {
25
+ length: number | undefined;
26
+ }>;
27
+ fromId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
28
+ name: "from_id";
29
+ tableName: "inbox_messages";
30
+ dataType: "string";
31
+ columnType: "SQLiteText";
32
+ data: string;
33
+ driverParam: string;
34
+ notNull: false;
35
+ hasDefault: false;
36
+ isPrimaryKey: false;
37
+ isAutoincrement: false;
38
+ hasRuntimeDefault: false;
39
+ enumValues: [string, ...string[]];
40
+ baseColumn: never;
41
+ identity: undefined;
42
+ generated: undefined;
43
+ }, {}, {
44
+ length: number | undefined;
45
+ }>;
46
+ toId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
47
+ name: "to_id";
48
+ tableName: "inbox_messages";
49
+ dataType: "string";
50
+ columnType: "SQLiteText";
51
+ data: string;
52
+ driverParam: string;
53
+ notNull: true;
54
+ hasDefault: false;
55
+ isPrimaryKey: false;
56
+ isAutoincrement: false;
57
+ hasRuntimeDefault: false;
58
+ enumValues: [string, ...string[]];
59
+ baseColumn: never;
60
+ identity: undefined;
61
+ generated: undefined;
62
+ }, {}, {
63
+ length: number | undefined;
64
+ }>;
65
+ body: import("drizzle-orm/sqlite-core").SQLiteColumn<{
66
+ name: "body";
67
+ tableName: "inbox_messages";
68
+ dataType: "string";
69
+ columnType: "SQLiteText";
70
+ data: string;
71
+ driverParam: string;
72
+ notNull: true;
73
+ hasDefault: false;
74
+ isPrimaryKey: false;
75
+ isAutoincrement: false;
76
+ hasRuntimeDefault: false;
77
+ enumValues: [string, ...string[]];
78
+ baseColumn: never;
79
+ identity: undefined;
80
+ generated: undefined;
81
+ }, {}, {
82
+ length: number | undefined;
83
+ }>;
84
+ status: import("drizzle-orm/sqlite-core").SQLiteColumn<{
85
+ name: "status";
86
+ tableName: "inbox_messages";
87
+ dataType: "string";
88
+ columnType: "SQLiteText";
89
+ data: string;
90
+ driverParam: string;
91
+ notNull: true;
92
+ hasDefault: true;
93
+ isPrimaryKey: false;
94
+ isAutoincrement: false;
95
+ hasRuntimeDefault: false;
96
+ enumValues: [string, ...string[]];
97
+ baseColumn: never;
98
+ identity: undefined;
99
+ generated: undefined;
100
+ }, {}, {
101
+ length: number | undefined;
102
+ }>;
103
+ inReplyTo: import("drizzle-orm/sqlite-core").SQLiteColumn<{
104
+ name: "in_reply_to";
105
+ tableName: "inbox_messages";
106
+ dataType: "string";
107
+ columnType: "SQLiteText";
108
+ data: string;
109
+ driverParam: string;
110
+ notNull: false;
111
+ hasDefault: false;
112
+ isPrimaryKey: false;
113
+ isAutoincrement: false;
114
+ hasRuntimeDefault: false;
115
+ enumValues: [string, ...string[]];
116
+ baseColumn: never;
117
+ identity: undefined;
118
+ generated: undefined;
119
+ }, {}, {
120
+ length: number | undefined;
121
+ }>;
122
+ createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
123
+ name: "created_at";
124
+ tableName: "inbox_messages";
125
+ dataType: "number";
126
+ columnType: "SQLiteInteger";
127
+ data: number;
128
+ driverParam: number;
129
+ notNull: true;
130
+ hasDefault: false;
131
+ isPrimaryKey: false;
132
+ isAutoincrement: false;
133
+ hasRuntimeDefault: false;
134
+ enumValues: undefined;
135
+ baseColumn: never;
136
+ identity: undefined;
137
+ generated: undefined;
138
+ }, {}, {}>;
139
+ updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
140
+ name: "updated_at";
141
+ tableName: "inbox_messages";
142
+ dataType: "number";
143
+ columnType: "SQLiteInteger";
144
+ data: number;
145
+ driverParam: number;
146
+ notNull: true;
147
+ hasDefault: false;
148
+ isPrimaryKey: false;
149
+ isAutoincrement: false;
150
+ hasRuntimeDefault: false;
151
+ enumValues: undefined;
152
+ baseColumn: never;
153
+ identity: undefined;
154
+ generated: undefined;
155
+ }, {}, {}>;
156
+ deliveredAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
157
+ name: "delivered_at";
158
+ tableName: "inbox_messages";
159
+ dataType: "number";
160
+ columnType: "SQLiteInteger";
161
+ data: number;
162
+ driverParam: number;
163
+ notNull: false;
164
+ hasDefault: false;
165
+ isPrimaryKey: false;
166
+ isAutoincrement: false;
167
+ hasRuntimeDefault: false;
168
+ enumValues: undefined;
169
+ baseColumn: never;
170
+ identity: undefined;
171
+ generated: undefined;
172
+ }, {}, {}>;
173
+ injectAttempts: import("drizzle-orm/sqlite-core").SQLiteColumn<{
174
+ name: "inject_attempts";
175
+ tableName: "inbox_messages";
176
+ dataType: "number";
177
+ columnType: "SQLiteInteger";
178
+ data: number;
179
+ driverParam: number;
180
+ notNull: true;
181
+ hasDefault: true;
182
+ isPrimaryKey: false;
183
+ isAutoincrement: false;
184
+ hasRuntimeDefault: false;
185
+ enumValues: undefined;
186
+ baseColumn: never;
187
+ identity: undefined;
188
+ generated: undefined;
189
+ }, {}, {}>;
190
+ injectError: import("drizzle-orm/sqlite-core").SQLiteColumn<{
191
+ name: "inject_error";
192
+ tableName: "inbox_messages";
193
+ dataType: "string";
194
+ columnType: "SQLiteText";
195
+ data: string;
196
+ driverParam: string;
197
+ notNull: false;
198
+ hasDefault: false;
199
+ isPrimaryKey: false;
200
+ isAutoincrement: false;
201
+ hasRuntimeDefault: false;
202
+ enumValues: [string, ...string[]];
203
+ baseColumn: never;
204
+ identity: undefined;
205
+ generated: undefined;
206
+ }, {}, {
207
+ length: number | undefined;
208
+ }>;
209
+ };
210
+ dialect: "sqlite";
211
+ }>;
212
+ //# sourceMappingURL=inbox-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-messages.d.ts","sourceRoot":"","sources":["../../src/schema/inbox-messages.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBzB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+ /**
3
+ * Drizzle schema definition for durable inter-agent inbox messages.
4
+ */
5
+ export const inboxMessages = sqliteTable('inbox_messages', {
6
+ id: text('id').primaryKey(),
7
+ fromId: text('from_id'),
8
+ toId: text('to_id').notNull(),
9
+ body: text('body').notNull(),
10
+ status: text('status').notNull().default('queued'),
11
+ inReplyTo: text('in_reply_to'),
12
+ createdAt: integer('created_at').notNull(),
13
+ updatedAt: integer('updated_at').notNull(),
14
+ deliveredAt: integer('delivered_at'),
15
+ injectAttempts: integer('inject_attempts').notNull().default(0),
16
+ injectError: text('inject_error'),
17
+ }, (table) => [index('idx_inbox_messages_to_status').on(table.toId, table.status)]);
@@ -2,5 +2,6 @@ export { index, integer, text } from 'drizzle-orm/sqlite-core';
2
2
  export * from './common';
3
3
  export { generateCreateTableSql } from './ddl';
4
4
  export { type DefinedTable, defineTable } from './define-table';
5
+ export { inboxMessages } from './inbox-messages';
5
6
  export { queueJobs } from './queue-jobs';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/D,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/D,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
@@ -2,4 +2,5 @@ export { index, integer, text } from 'drizzle-orm/sqlite-core';
2
2
  export * from './common.js';
3
3
  export { generateCreateTableSql } from './ddl.js';
4
4
  export { defineTable } from './define-table.js';
5
+ export { inboxMessages } from './inbox-messages.js';
5
6
  export { queueJobs } from './queue-jobs.js';
@@ -1,2 +1,3 @@
1
+ export { inboxMessages } from './inbox-messages';
1
2
  export { queueJobs } from './queue-jobs';
2
3
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/schema/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/schema/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
@@ -1 +1,2 @@
1
+ export { inboxMessages } from './inbox-messages.js';
1
2
  export { queueJobs } from './queue-jobs.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-db",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
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",
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/adapters/d1.d.ts",
41
41
  "import": "./dist/adapters/d1.js"
42
42
  },
43
+ "./inbox": {
44
+ "types": "./dist/inbox.d.ts",
45
+ "import": "./dist/inbox.js"
46
+ },
43
47
  "./schema": {
44
48
  "types": "./dist/schema/index.d.ts",
45
49
  "import": "./dist/schema/index.js"
@@ -62,7 +66,8 @@
62
66
  "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"
63
67
  },
64
68
  "dependencies": {
65
- "@gobing-ai/ts-runtime": "^0.2.7"
69
+ "@gobing-ai/ts-runtime": "^0.2.9",
70
+ "@gobing-ai/ts-utils": "^0.2.9"
66
71
  },
67
72
  "peerDependencies": {
68
73
  "drizzle-orm": ">=0.38.0",
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared structural types for drizzle's fluent query builders.
3
+ *
4
+ * `InternalDb` is typed as the bare drizzle union (ADR-005 keeps drizzle internal),
5
+ * so its fluent builders (`insert().values().returning()`, `update().set().where()`,
6
+ * `select(proj).from()...`) are not surfaced as call-shapes. Each DAO previously
7
+ * re-declared these narrowings inline — three files independently describing the
8
+ * same `update → set → where → returning` chain. Centralising them here means the
9
+ * cast shape lives in one place: when drizzle's builder surface shifts, this file
10
+ * changes, not every DAO.
11
+ *
12
+ * These are deliberately minimal — each type narrows only the chain a DAO actually
13
+ * invokes. Table-specific projections (`select({ status, count })`) stay local to
14
+ * their DAO; only the recurring chain skeletons live here.
15
+ *
16
+ * @internal
17
+ */
18
+
19
+ /** Terminal `.returning()` returning the written rows. */
20
+ export interface ReturningRows {
21
+ returning: () => Promise<unknown[]>;
22
+ }
23
+
24
+ /** `insert(table).values(record)` → returning, with optional upsert. */
25
+ export interface InsertBuilder {
26
+ values: (record: unknown) => ReturningRows & {
27
+ onConflictDoUpdate: (cfg: { target: unknown; set: unknown }) => ReturningRows;
28
+ };
29
+ }
30
+
31
+ /** `update(table).set(data).where(cond)` → `.returning()` rows. */
32
+ export interface UpdateReturningDb {
33
+ update: (table: unknown) => {
34
+ set: (data: unknown) => {
35
+ where: (cond: unknown) => ReturningRows;
36
+ };
37
+ };
38
+ }
39
+
40
+ /** `update(table).set(data).where(cond)` → void (no rows). */
41
+ export interface UpdateVoidDb {
42
+ update: (table: unknown) => {
43
+ set: (data: unknown) => {
44
+ where: (cond: unknown) => Promise<unknown>;
45
+ };
46
+ };
47
+ }
48
+
49
+ /** `update(table).set(data).where(cond)` → `{ changes }` (affected-row count). */
50
+ export interface UpdateChangesDb {
51
+ update: (table: unknown) => {
52
+ set: (data: unknown) => {
53
+ where: (cond: unknown) => Promise<{ changes: number }>;
54
+ };
55
+ };
56
+ }
57
+
58
+ /** `update(table).set(data).where(cond)` → arbitrary single condition (no projection). */
59
+ export interface SimpleUpdateDb {
60
+ update: (table: unknown) => {
61
+ set: (data: unknown) => {
62
+ where: (cond: unknown) => ReturningRows;
63
+ };
64
+ };
65
+ }
66
+
67
+ /** `select(projection).from(table)` exposing `.where(...)` and `.groupBy(...)` terminals. */
68
+ export interface SelectProjectionDb {
69
+ select: (projection: unknown) => {
70
+ from: (table: unknown) => {
71
+ where: (cond: unknown) => Promise<unknown[]>;
72
+ groupBy: (group: unknown) => Promise<unknown[]>;
73
+ };
74
+ };
75
+ }
76
+
77
+ /** `select().from(table).where(cond).orderBy(o).limit(n)` — the ready-rows read chain. */
78
+ export interface SelectOrderedLimitDb {
79
+ select: () => {
80
+ from: (table: unknown) => {
81
+ where: (cond: unknown) => {
82
+ orderBy: (order: unknown) => { limit: (limit: number) => Promise<unknown[]> };
83
+ };
84
+ };
85
+ };
86
+ }
87
+
88
+ /** `select(count).from(table)` that is awaitable and chainable with `.where(...)`. */
89
+ export type CountQuery = Promise<unknown[]> & {
90
+ where: (condition: unknown) => Promise<unknown[]>;
91
+ };
92
+
93
+ /** `select({ value: count() }).from(table)` → awaitable count, optionally filtered. */
94
+ export interface CountSelectDb {
95
+ select: (projection: unknown) => {
96
+ from: (table: unknown) => CountQuery;
97
+ };
98
+ }
@@ -29,4 +29,14 @@ export const embeddedMigrations: EmbeddedMigration[] = [
29
29
  sql: 'ALTER TABLE `queue_jobs` ADD `expires_at` integer;',
30
30
  hash: '7380f8c162352a61b15205af5a87e0e7313a499203dae98fe62151a1dc7fec0e',
31
31
  },
32
+ {
33
+ tag: '0003_inbox_messages',
34
+ sql: "CREATE TABLE `inbox_messages` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`from_id` text,\n\t`to_id` text NOT NULL,\n\t`body` text NOT NULL,\n\t`status` text DEFAULT 'queued' NOT NULL,\n\t`in_reply_to` text,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`delivered_at` integer,\n\t`inject_attempts` integer DEFAULT 0 NOT NULL,\n\t`inject_error` text\n);",
35
+ hash: 'c4ba569172d1be276c42b5c164c668404e6229dcf48b41d7fb2bc93e8d29d11b',
36
+ },
37
+ {
38
+ tag: '0004_inbox_messages_to_status_idx',
39
+ sql: 'CREATE INDEX `idx_inbox_messages_to_status` ON `inbox_messages` (`to_id`,`status`);',
40
+ hash: '3c00f1bce4f569e442f3142df31ff478d6c9b3d460824d46575f50a324bfae61',
41
+ },
32
42
  ];
package/src/entity-dao.ts CHANGED
@@ -2,18 +2,9 @@ import { and, count as countFn, eq, type SQL } from 'drizzle-orm';
2
2
  import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core';
3
3
  import type { DbAdapter } from './adapter';
4
4
  import { BaseDao } from './base-dao';
5
+ import type { CountSelectDb, InsertBuilder, ReturningRows } from './drizzle-builders';
5
6
  import { compilePredicate, type OrderTerm, type Predicate } from './query-spec';
6
7
 
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
8
  type UpdateBuilder = {
18
9
  set: (data: unknown) => {
19
10
  where: (condition: SQL) => ReturningRows;
@@ -24,16 +15,6 @@ type DeleteBuilder = {
24
15
  where: (condition: SQL) => Promise<unknown>;
25
16
  };
26
17
 
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
-
37
18
  /**
38
19
  * Type for tables compatible with EntityDao.
39
20
  * Must have standard columns: createdAt, updatedAt.
@@ -347,7 +328,7 @@ export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> ext
347
328
  async count(where?: Predicate, includeDeleted = false): Promise<number> {
348
329
  const condition = this.withActive(where, includeDeleted);
349
330
  const compiled = condition ? compilePredicate(condition) : undefined;
350
- const base = (this.db as CountDb).select({ value: countFn() }).from(this.table);
331
+ const base = (this.db as CountSelectDb).select({ value: countFn() }).from(this.table);
351
332
  const result = (await (compiled ? base.where(compiled) : base)) as {
352
333
  value: number;
353
334
  }[];
@@ -0,0 +1,91 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import type { DbAdapter } from './adapter';
3
+ import type { UpdateReturningDb } from './drizzle-builders';
4
+ import { EntityDao } from './entity-dao';
5
+ import { inboxMessages } from './schema/inbox-messages';
6
+
7
+ /** Row type inferred from the inbox_messages Drizzle schema. */
8
+ export type InboxMessage = typeof inboxMessages.$inferSelect;
9
+
10
+ /**
11
+ * DAO for durable inter-agent inbox messages.
12
+ */
13
+ export class InboxMessageDao extends EntityDao<typeof inboxMessages, typeof inboxMessages.id> {
14
+ constructor(adapter: DbAdapter) {
15
+ super(adapter, inboxMessages, [inboxMessages.id], 'inbox_messages');
16
+ }
17
+
18
+ async enqueue(fromId: string | null, toId: string, body: string, inReplyTo?: string): Promise<string> {
19
+ const id = crypto.randomUUID();
20
+ await this.create({
21
+ id,
22
+ fromId,
23
+ toId,
24
+ body,
25
+ status: 'queued',
26
+ injectAttempts: 0,
27
+ ...(inReplyTo !== undefined ? { inReplyTo } : {}),
28
+ });
29
+ return id;
30
+ }
31
+
32
+ async drainPending(toId: string): Promise<InboxMessage[]> {
33
+ const now = this.now();
34
+ const rows = await (this.db as UpdateReturningDb)
35
+ .update(inboxMessages)
36
+ .set({
37
+ status: 'injected',
38
+ injectAttempts: sql`${inboxMessages.injectAttempts} + 1`,
39
+ updatedAt: now,
40
+ })
41
+ .where(
42
+ sql`${inboxMessages.id} IN (
43
+ SELECT id
44
+ FROM ${inboxMessages}
45
+ WHERE to_id = ${toId}
46
+ AND status = 'queued'
47
+ ORDER BY created_at
48
+ )
49
+ AND ${inboxMessages.status} = 'queued'`,
50
+ )
51
+ .returning();
52
+
53
+ return rows as InboxMessage[];
54
+ }
55
+
56
+ async markDelivered(msgId: string): Promise<void> {
57
+ await this.update(msgId, {
58
+ status: 'delivered',
59
+ deliveredAt: this.now(),
60
+ });
61
+ }
62
+
63
+ async markFailed(msgId: string, error: string): Promise<void> {
64
+ await this.update(msgId, {
65
+ status: 'failed',
66
+ injectError: error,
67
+ });
68
+ }
69
+
70
+ async inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]> {
71
+ return this.list({
72
+ where: { col: inboxMessages.toId, op: 'eq', value: toId },
73
+ orderBy: [{ col: inboxMessages.createdAt, dir: 'desc' }],
74
+ ...(limit !== undefined ? { limit } : {}),
75
+ ...(offset !== undefined ? { offset } : {}),
76
+ });
77
+ }
78
+
79
+ async countPending(toId: string): Promise<number> {
80
+ return this.count({
81
+ and: [
82
+ { col: inboxMessages.toId, op: 'eq', value: toId },
83
+ { col: inboxMessages.status, op: 'eq', value: 'queued' },
84
+ ],
85
+ });
86
+ }
87
+
88
+ async getById(id: string): Promise<InboxMessage | undefined> {
89
+ return this.findBy(inboxMessages.id, id);
90
+ }
91
+ }
package/src/inbox.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
2
+ export { inboxMessages } from './schema/inbox-messages';
package/src/index.ts CHANGED
@@ -14,7 +14,8 @@ export {
14
14
  type PKValue,
15
15
  type SoftDeletableTable,
16
16
  } from './entity-dao';
17
- export { applyMigrations, type MigrationOptions } from './migrate';
17
+ export { type InboxMessage, InboxMessageDao } from './inbox-message-dao';
18
+ export { applyMigrations, type MigrationLogger, type MigrationOptions } from './migrate';
18
19
  export {
19
20
  type ColRef,
20
21
  type ComparisonOp,
@@ -34,5 +35,6 @@ export {
34
35
  standardColumns,
35
36
  standardColumnsWithSoftDelete,
36
37
  } from './schema/common';
38
+ export { inboxMessages } from './schema/inbox-messages';
37
39
  export { queueJobs } from './schema/queue-jobs';
38
40
  export type { SpanContext } from './span-context';