@beignet/provider-db-drizzle 0.0.32 → 0.0.34
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/CHANGELOG.md +18 -0
- package/README.md +47 -7
- package/dist/mysql/index.d.ts +8 -7
- package/dist/mysql/index.d.ts.map +1 -1
- package/dist/mysql/index.js +42 -3
- package/dist/mysql/index.js.map +1 -1
- package/dist/postgres/index.d.ts +8 -7
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +12 -2
- package/dist/postgres/index.js.map +1 -1
- package/dist/shared/idempotency-core.d.ts.map +1 -1
- package/dist/shared/idempotency-core.js +31 -13
- package/dist/shared/idempotency-core.js.map +1 -1
- package/dist/shared/outbox-core.d.ts +6 -1
- package/dist/shared/outbox-core.d.ts.map +1 -1
- package/dist/shared/outbox-core.js +206 -2
- package/dist/shared/outbox-core.js.map +1 -1
- package/dist/sqlite/index.d.ts +8 -7
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +36 -5
- package/dist/sqlite/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/database-provider/SKILL.md +7 -2
- package/src/mysql/index.ts +62 -11
- package/src/postgres/index.ts +22 -9
- package/src/shared/idempotency-core.ts +33 -14
- package/src/shared/outbox-core.ts +278 -0
- package/src/sqlite/index.ts +68 -18
|
@@ -175,11 +175,15 @@ export function createIdempotencyPortCore(
|
|
|
175
175
|
const expiresAt = resolveIdempotencyExpiresAt(input.ttlSec, now);
|
|
176
176
|
const storageKey = createIdempotencyStorageKey(input);
|
|
177
177
|
const scopeKey = normalizeIdempotencyScope(input.scope);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
const reserved = (): IdempotencyReservation => ({
|
|
179
|
+
status: "reserved",
|
|
180
|
+
namespace: input.namespace,
|
|
181
|
+
key: input.key,
|
|
182
|
+
scopeKey,
|
|
183
|
+
fingerprint: input.fingerprint,
|
|
184
|
+
reservedAt: now,
|
|
185
|
+
expiresAt,
|
|
186
|
+
});
|
|
183
187
|
|
|
184
188
|
const rowsAffected = await tx.run(sql`${tx.insertPrefix} ${tx.table} (
|
|
185
189
|
storage_key,
|
|
@@ -208,15 +212,30 @@ export function createIdempotencyPortCore(
|
|
|
208
212
|
)${tx.insertConflictSuffix}`);
|
|
209
213
|
|
|
210
214
|
if (rowsAffected === undefined || rowsAffected > 0) {
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
return reserved();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reset an expired reservation in place after the insert conflict. Doing
|
|
219
|
+
// this after the insert avoids MySQL gap-lock deadlocks when several
|
|
220
|
+
// transactions reserve the same previously missing storage key.
|
|
221
|
+
const expiredRowsAffected = await tx.run(sql`update ${tx.table}
|
|
222
|
+
set
|
|
223
|
+
namespace = ${input.namespace},
|
|
224
|
+
idempotency_key = ${input.key},
|
|
225
|
+
scope_key = ${scopeKey},
|
|
226
|
+
fingerprint = ${input.fingerprint},
|
|
227
|
+
status = 'in-progress',
|
|
228
|
+
result_json = null,
|
|
229
|
+
reserved_at = ${nowIso},
|
|
230
|
+
completed_at = null,
|
|
231
|
+
expires_at = ${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
232
|
+
updated_at = ${nowIso}
|
|
233
|
+
where storage_key = ${storageKey}
|
|
234
|
+
and expires_at is not null
|
|
235
|
+
and expires_at <= ${nowIso}`);
|
|
236
|
+
|
|
237
|
+
if (expiredRowsAffected !== undefined && expiredRowsAffected > 0) {
|
|
238
|
+
return reserved();
|
|
220
239
|
}
|
|
221
240
|
|
|
222
241
|
const row = (await tx.row(
|
|
@@ -13,11 +13,20 @@ import {
|
|
|
13
13
|
type ClaimedOutboxMessage,
|
|
14
14
|
createOutboxMessage,
|
|
15
15
|
DEFAULT_OUTBOX_LEASE_MS,
|
|
16
|
+
OutboxAdminError,
|
|
17
|
+
type OutboxAdminPort,
|
|
16
18
|
OutboxClaimError,
|
|
19
|
+
type OutboxCountMessagesOptions,
|
|
20
|
+
type OutboxDeleteResult,
|
|
17
21
|
type OutboxEnqueueInput,
|
|
22
|
+
type OutboxListMessagesOptions,
|
|
18
23
|
type OutboxMessage,
|
|
24
|
+
type OutboxMessageQuery,
|
|
19
25
|
type OutboxMessageStatus,
|
|
20
26
|
type OutboxPort,
|
|
27
|
+
type OutboxPruneDeliveredInput,
|
|
28
|
+
type OutboxPurgeDeadLetteredInput,
|
|
29
|
+
type OutboxRequeueMessageInput,
|
|
21
30
|
serializeOutboxError,
|
|
22
31
|
} from "@beignet/core/outbox";
|
|
23
32
|
import { type SQL, sql } from "drizzle-orm";
|
|
@@ -25,6 +34,7 @@ import {
|
|
|
25
34
|
encodeTimestamp,
|
|
26
35
|
type OutboxRow,
|
|
27
36
|
rowToClaimedMessage,
|
|
37
|
+
rowToMessage,
|
|
28
38
|
} from "./rows.js";
|
|
29
39
|
|
|
30
40
|
/**
|
|
@@ -93,6 +103,24 @@ function nowFrom(options: OutboxPortCoreOptions): Date {
|
|
|
93
103
|
return options.now?.() ?? new Date();
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
function assertNonEmptyString(name: string, value: string): void {
|
|
107
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
108
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function assertPositiveInteger(name: string, value: number): void {
|
|
113
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
114
|
+
throw new Error(`${name} must be a positive integer`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertValidDate(name: string, value: Date): void {
|
|
119
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
120
|
+
throw new Error(`${name} must be a valid Date`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
function createClaimToken(): string {
|
|
97
125
|
return crypto.randomUUID();
|
|
98
126
|
}
|
|
@@ -107,6 +135,124 @@ async function getOutboxRow(
|
|
|
107
135
|
return row as OutboxRow | undefined;
|
|
108
136
|
}
|
|
109
137
|
|
|
138
|
+
function normalizeStatusFilter(
|
|
139
|
+
status: OutboxMessageQuery["status"],
|
|
140
|
+
): readonly OutboxMessageStatus[] | undefined {
|
|
141
|
+
if (status === undefined) return undefined;
|
|
142
|
+
if (typeof status === "string") return [status];
|
|
143
|
+
return status;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function orConditions(conditions: readonly SQL[]): SQL {
|
|
147
|
+
if (conditions.length === 0) return sql`1 = 0`;
|
|
148
|
+
let condition = conditions[0] ?? sql`1 = 0`;
|
|
149
|
+
for (const next of conditions.slice(1)) {
|
|
150
|
+
condition = sql`${condition} or ${next}`;
|
|
151
|
+
}
|
|
152
|
+
return sql`(${condition})`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function andConditions(conditions: readonly SQL[]): SQL {
|
|
156
|
+
let condition = conditions[0] ?? sql`1 = 1`;
|
|
157
|
+
for (const next of conditions.slice(1)) {
|
|
158
|
+
condition = sql`${condition} and ${next}`;
|
|
159
|
+
}
|
|
160
|
+
return condition;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function whereClause(conditions: readonly SQL[]): SQL {
|
|
164
|
+
if (conditions.length === 0) return sql.raw("");
|
|
165
|
+
return sql` where ${andConditions(conditions)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function messageQueryConditions(query: OutboxMessageQuery = {}): SQL[] {
|
|
169
|
+
const conditions: SQL[] = [];
|
|
170
|
+
const statuses = normalizeStatusFilter(query.status);
|
|
171
|
+
if (statuses !== undefined) {
|
|
172
|
+
conditions.push(
|
|
173
|
+
orConditions(statuses.map((status) => sql`status = ${status}`)),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (query.kind !== undefined) conditions.push(sql`kind = ${query.kind}`);
|
|
177
|
+
if (query.name !== undefined) {
|
|
178
|
+
assertNonEmptyString("name", query.name);
|
|
179
|
+
conditions.push(sql`name = ${query.name}`);
|
|
180
|
+
}
|
|
181
|
+
if (query.updatedBefore !== undefined) {
|
|
182
|
+
assertValidDate("updatedBefore", query.updatedBefore);
|
|
183
|
+
conditions.push(sql`updated_at < ${encodeTimestamp(query.updatedBefore)}`);
|
|
184
|
+
}
|
|
185
|
+
if (query.deliveredBefore !== undefined) {
|
|
186
|
+
assertValidDate("deliveredBefore", query.deliveredBefore);
|
|
187
|
+
conditions.push(sql`delivered_at is not null`);
|
|
188
|
+
conditions.push(
|
|
189
|
+
sql`delivered_at < ${encodeTimestamp(query.deliveredBefore)}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return conditions;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readCount(row: Record<string, unknown> | undefined): number {
|
|
196
|
+
const value = row?.count;
|
|
197
|
+
if (typeof value === "number") return value;
|
|
198
|
+
if (typeof value === "bigint") return Number(value);
|
|
199
|
+
if (typeof value === "string") return Number(value);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function adminListLimit(limit: number | undefined): number {
|
|
204
|
+
const resolved = limit ?? 50;
|
|
205
|
+
assertPositiveInteger("limit", resolved);
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function countRows(
|
|
210
|
+
executor: OutboxSqlExecutor,
|
|
211
|
+
conditions: readonly SQL[],
|
|
212
|
+
): Promise<number> {
|
|
213
|
+
const row = await executor.row(
|
|
214
|
+
sql`select count(*) as count from ${executor.table}${whereClause(conditions)}`,
|
|
215
|
+
);
|
|
216
|
+
return readCount(row);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function limitedDeleteQuery(
|
|
220
|
+
executor: OutboxSqlExecutor,
|
|
221
|
+
conditions: readonly SQL[],
|
|
222
|
+
limit: number | undefined,
|
|
223
|
+
orderBy: SQL,
|
|
224
|
+
): SQL {
|
|
225
|
+
const where = whereClause(conditions);
|
|
226
|
+
if (limit === undefined) {
|
|
227
|
+
return sql`delete from ${executor.table}${where}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return sql`delete from ${executor.table}
|
|
231
|
+
where id in (
|
|
232
|
+
select id from (
|
|
233
|
+
select id from ${executor.table}${where}
|
|
234
|
+
order by ${orderBy}
|
|
235
|
+
limit ${limit}
|
|
236
|
+
) as beignet_outbox_admin_delete
|
|
237
|
+
)`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function deleteRows(
|
|
241
|
+
executor: OutboxSqlExecutor,
|
|
242
|
+
conditions: readonly SQL[],
|
|
243
|
+
limit: number | undefined,
|
|
244
|
+
orderBy: SQL = sql`updated_at asc, created_at asc, id asc`,
|
|
245
|
+
): Promise<OutboxDeleteResult> {
|
|
246
|
+
if (limit !== undefined) assertPositiveInteger("limit", limit);
|
|
247
|
+
const expected = await countRows(executor, conditions);
|
|
248
|
+
const cappedExpected =
|
|
249
|
+
limit === undefined ? expected : Math.min(expected, limit);
|
|
250
|
+
const rowsAffected = await executor.run(
|
|
251
|
+
limitedDeleteQuery(executor, conditions, limit, orderBy),
|
|
252
|
+
);
|
|
253
|
+
return { deleted: rowsAffected ?? cappedExpected };
|
|
254
|
+
}
|
|
255
|
+
|
|
110
256
|
function assertClaimedRowSucceeded(
|
|
111
257
|
row: OutboxRow | undefined,
|
|
112
258
|
input: { id: string; expectedStatus: OutboxMessageStatus },
|
|
@@ -320,3 +466,135 @@ export function createOutboxPortCore(
|
|
|
320
466
|
},
|
|
321
467
|
};
|
|
322
468
|
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create the dialect-independent outbox admin port orchestration on top of a
|
|
472
|
+
* dialect's `OutboxSqlExecutor`.
|
|
473
|
+
*/
|
|
474
|
+
export function createOutboxAdminPortCore(
|
|
475
|
+
executor: OutboxSqlExecutor,
|
|
476
|
+
options: OutboxPortCoreOptions = {},
|
|
477
|
+
): OutboxAdminPort {
|
|
478
|
+
return {
|
|
479
|
+
async listMessages(
|
|
480
|
+
input: OutboxListMessagesOptions = {},
|
|
481
|
+
): Promise<readonly OutboxMessage[]> {
|
|
482
|
+
const limit = adminListLimit(input.limit);
|
|
483
|
+
const conditions = messageQueryConditions(input);
|
|
484
|
+
const rows =
|
|
485
|
+
(await executor.rows(sql`select * from ${executor.table}${whereClause(
|
|
486
|
+
conditions,
|
|
487
|
+
)}
|
|
488
|
+
order by updated_at desc, created_at desc, id desc
|
|
489
|
+
limit ${limit}`)) as unknown as OutboxRow[];
|
|
490
|
+
|
|
491
|
+
return rows.map(rowToMessage);
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
async countMessages(
|
|
495
|
+
input: OutboxCountMessagesOptions = {},
|
|
496
|
+
): Promise<number> {
|
|
497
|
+
return countRows(executor, messageQueryConditions(input));
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
async getMessage(id: string): Promise<OutboxMessage | null> {
|
|
501
|
+
assertNonEmptyString("id", id);
|
|
502
|
+
const row = await getOutboxRow(executor, id);
|
|
503
|
+
return row ? rowToMessage(row) : null;
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
async requeueMessage(
|
|
507
|
+
input: OutboxRequeueMessageInput,
|
|
508
|
+
): Promise<OutboxMessage> {
|
|
509
|
+
assertNonEmptyString("id", input.id);
|
|
510
|
+
const existing = await getOutboxRow(executor, input.id);
|
|
511
|
+
if (!existing) {
|
|
512
|
+
throw new OutboxAdminError({
|
|
513
|
+
id: input.id,
|
|
514
|
+
message: `Outbox message "${input.id}" does not exist.`,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (existing.status !== "deadLettered") {
|
|
518
|
+
throw new OutboxAdminError({
|
|
519
|
+
id: input.id,
|
|
520
|
+
message: `Outbox message "${input.id}" is not dead-lettered.`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const now = input.now ?? nowFrom(options);
|
|
525
|
+
const availableAt = input.availableAt ?? now;
|
|
526
|
+
assertValidDate("now", now);
|
|
527
|
+
assertValidDate("availableAt", availableAt);
|
|
528
|
+
const nowIso = encodeTimestamp(now);
|
|
529
|
+
const availableAtIso = encodeTimestamp(availableAt);
|
|
530
|
+
|
|
531
|
+
const rowsAffected = input.resetAttempts
|
|
532
|
+
? await executor.run(sql`update ${executor.table}
|
|
533
|
+
set
|
|
534
|
+
status = 'pending',
|
|
535
|
+
attempts = 0,
|
|
536
|
+
available_at = ${availableAtIso},
|
|
537
|
+
claimed_at = null,
|
|
538
|
+
locked_until = null,
|
|
539
|
+
claim_token = null,
|
|
540
|
+
updated_at = ${nowIso}
|
|
541
|
+
where id = ${input.id} and status = 'deadLettered'`)
|
|
542
|
+
: await executor.run(sql`update ${executor.table}
|
|
543
|
+
set
|
|
544
|
+
status = 'pending',
|
|
545
|
+
available_at = ${availableAtIso},
|
|
546
|
+
claimed_at = null,
|
|
547
|
+
locked_until = null,
|
|
548
|
+
claim_token = null,
|
|
549
|
+
updated_at = ${nowIso}
|
|
550
|
+
where id = ${input.id} and status = 'deadLettered'`);
|
|
551
|
+
|
|
552
|
+
if (rowsAffected !== undefined && rowsAffected <= 0) {
|
|
553
|
+
throw new OutboxAdminError({
|
|
554
|
+
id: input.id,
|
|
555
|
+
message: `Outbox message "${input.id}" could not be requeued because it changed state.`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const requeued = await getOutboxRow(executor, input.id);
|
|
560
|
+
if (!requeued) {
|
|
561
|
+
throw new OutboxAdminError({
|
|
562
|
+
id: input.id,
|
|
563
|
+
message: `Outbox message "${input.id}" could not be requeued because it no longer exists.`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return rowToMessage(requeued);
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
async purgeDeadLettered(
|
|
571
|
+
input: OutboxPurgeDeadLetteredInput = {},
|
|
572
|
+
): Promise<OutboxDeleteResult> {
|
|
573
|
+
const conditions: SQL[] = [sql`status = 'deadLettered'`];
|
|
574
|
+
if (input.before !== undefined) {
|
|
575
|
+
assertValidDate("before", input.before);
|
|
576
|
+
conditions.push(sql`updated_at < ${encodeTimestamp(input.before)}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return deleteRows(executor, conditions, input.limit);
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
async pruneDelivered(
|
|
583
|
+
input: OutboxPruneDeliveredInput,
|
|
584
|
+
): Promise<OutboxDeleteResult> {
|
|
585
|
+
assertValidDate("before", input.before);
|
|
586
|
+
const conditions: SQL[] = [
|
|
587
|
+
sql`status = 'delivered'`,
|
|
588
|
+
sql`delivered_at is not null`,
|
|
589
|
+
sql`delivered_at < ${encodeTimestamp(input.before)}`,
|
|
590
|
+
];
|
|
591
|
+
|
|
592
|
+
return deleteRows(
|
|
593
|
+
executor,
|
|
594
|
+
conditions,
|
|
595
|
+
input.limit,
|
|
596
|
+
sql`delivered_at asc, updated_at asc, created_at asc, id asc`,
|
|
597
|
+
);
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
package/src/sqlite/index.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import type { IdempotencyPort } from "@beignet/core/idempotency";
|
|
33
|
-
import type { OutboxPort } from "@beignet/core/outbox";
|
|
33
|
+
import type { OutboxAdminPort, OutboxPort } from "@beignet/core/outbox";
|
|
34
34
|
import {
|
|
35
35
|
type AuditLogPort,
|
|
36
36
|
type BufferedDomainEventRecorder,
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
import { quoteIdentifier } from "../shared/identifiers.js";
|
|
64
64
|
import { createDbQueryLogger } from "../shared/instrumentation.js";
|
|
65
65
|
import {
|
|
66
|
+
createOutboxAdminPortCore,
|
|
66
67
|
createOutboxPortCore,
|
|
67
68
|
type OutboxSqlExecutor,
|
|
68
69
|
type SqlExecutorBase,
|
|
@@ -83,13 +84,6 @@ export interface DbPort<
|
|
|
83
84
|
*/
|
|
84
85
|
drizzle: LibSQLDatabase<TSchema>;
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
* Backwards-compatible alias for `drizzle`.
|
|
88
|
-
*
|
|
89
|
-
* @deprecated Use `drizzle` for the Drizzle ORM instance.
|
|
90
|
-
*/
|
|
91
|
-
db: LibSQLDatabase<TSchema>;
|
|
92
|
-
|
|
93
87
|
/**
|
|
94
88
|
* The underlying libSQL client instance.
|
|
95
89
|
* Use this for advanced operations not covered by Drizzle ORM.
|
|
@@ -123,6 +117,32 @@ export type DrizzleSqliteTransaction<
|
|
|
123
117
|
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
124
118
|
> = LibSQLTransaction<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
125
119
|
|
|
120
|
+
const sqliteTransactionTails = new WeakMap<object, Promise<void>>();
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Queue Beignet-owned transactions per Drizzle client. SQLite only permits one
|
|
124
|
+
* writer at a time, while the local libSQL driver otherwise exposes concurrent
|
|
125
|
+
* transaction attempts as SQLITE_BUSY instead of waiting for the active write.
|
|
126
|
+
*/
|
|
127
|
+
function runSerializedSqliteTransaction<Result>(
|
|
128
|
+
db: object,
|
|
129
|
+
transaction: () => Promise<Result>,
|
|
130
|
+
): Promise<Result> {
|
|
131
|
+
const previous = sqliteTransactionTails.get(db) ?? Promise.resolve();
|
|
132
|
+
const result = previous.then(transaction);
|
|
133
|
+
const tail = result.then(
|
|
134
|
+
() => undefined,
|
|
135
|
+
() => undefined,
|
|
136
|
+
);
|
|
137
|
+
sqliteTransactionTails.set(db, tail);
|
|
138
|
+
|
|
139
|
+
return result.finally(() => {
|
|
140
|
+
if (sqliteTransactionTails.get(db) === tail) {
|
|
141
|
+
sqliteTransactionTails.delete(db);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
async function checkSqliteHealth(client: Client): Promise<DbHealth> {
|
|
127
147
|
try {
|
|
128
148
|
await client.execute("select 1");
|
|
@@ -185,13 +205,15 @@ export function createDrizzleSqliteUnitOfWork<
|
|
|
185
205
|
): Promise<Result> {
|
|
186
206
|
const events = createDomainEventRecorder();
|
|
187
207
|
|
|
188
|
-
const result = await options.db
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
const result = await runSerializedSqliteTransaction(options.db, () =>
|
|
209
|
+
options.db.transaction(async (tx) => {
|
|
210
|
+
const txPorts = options.createTransactionPorts(
|
|
211
|
+
tx as DrizzleSqliteTransaction<TSchema>,
|
|
212
|
+
events,
|
|
213
|
+
);
|
|
214
|
+
return work(txPorts);
|
|
215
|
+
}, options.transactionConfig),
|
|
216
|
+
);
|
|
195
217
|
|
|
196
218
|
if (options.eventBus) {
|
|
197
219
|
await events.flush(options.eventBus);
|
|
@@ -336,6 +358,11 @@ function createSqliteExecutor<TSchema extends Record<string, unknown>>(
|
|
|
336
358
|
},
|
|
337
359
|
|
|
338
360
|
async row(query) {
|
|
361
|
+
if ("all" in db && typeof db.all === "function") {
|
|
362
|
+
const rows = await db.all<Record<string, unknown>>(query);
|
|
363
|
+
return rows[0];
|
|
364
|
+
}
|
|
365
|
+
|
|
339
366
|
return db.get<Record<string, unknown> | undefined>(query);
|
|
340
367
|
},
|
|
341
368
|
|
|
@@ -345,8 +372,12 @@ function createSqliteExecutor<TSchema extends Record<string, unknown>>(
|
|
|
345
372
|
|
|
346
373
|
withTransaction(fn) {
|
|
347
374
|
if ("transaction" in db && typeof db.transaction === "function") {
|
|
348
|
-
return db
|
|
349
|
-
|
|
375
|
+
return runSerializedSqliteTransaction(db, () =>
|
|
376
|
+
db.transaction((tx) =>
|
|
377
|
+
fn(
|
|
378
|
+
createSqliteExecutor(tx as DrizzleSqliteDatabase<TSchema>, table),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
350
381
|
);
|
|
351
382
|
}
|
|
352
383
|
|
|
@@ -411,6 +442,26 @@ export function createDrizzleSqliteOutboxPort<
|
|
|
411
442
|
return createOutboxPortCore(executor, { now: adapterOptions.now });
|
|
412
443
|
}
|
|
413
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Create a Beignet outbox admin port backed by a Drizzle SQLite database.
|
|
447
|
+
*
|
|
448
|
+
* Wire this only into operational contexts that need to inspect, requeue,
|
|
449
|
+
* purge, or prune outbox messages.
|
|
450
|
+
*/
|
|
451
|
+
export function createDrizzleSqliteOutboxAdminPort<
|
|
452
|
+
TSchema extends Record<string, unknown>,
|
|
453
|
+
>(
|
|
454
|
+
db: DrizzleSqliteDatabase<TSchema>,
|
|
455
|
+
adapterOptions: DrizzleSqliteOutboxOptions = {},
|
|
456
|
+
): OutboxAdminPort {
|
|
457
|
+
const table = sql.raw(
|
|
458
|
+
quoteIdentifier(adapterOptions.tableName ?? "outbox_messages", '"'),
|
|
459
|
+
);
|
|
460
|
+
const executor: OutboxSqlExecutor = createSqliteExecutor(db, table);
|
|
461
|
+
|
|
462
|
+
return createOutboxAdminPortCore(executor, { now: adapterOptions.now });
|
|
463
|
+
}
|
|
464
|
+
|
|
414
465
|
/**
|
|
415
466
|
* Options for a Drizzle SQLite-backed idempotency port.
|
|
416
467
|
*/
|
|
@@ -645,7 +696,6 @@ export function createDrizzleSqliteProvider<
|
|
|
645
696
|
// 3. Build a typed DbPort
|
|
646
697
|
const dbPort: DbPort<TSchema> = {
|
|
647
698
|
drizzle: db,
|
|
648
|
-
db,
|
|
649
699
|
client,
|
|
650
700
|
checkHealth: () => checkSqliteHealth(client),
|
|
651
701
|
};
|