@beignet/provider-db-drizzle 0.0.32 → 0.0.33
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 +12 -0
- package/README.md +45 -2
- package/dist/mysql/index.d.ts +8 -1
- package/dist/mysql/index.d.ts.map +1 -1
- package/dist/mysql/index.js +42 -2
- package/dist/mysql/index.js.map +1 -1
- package/dist/postgres/index.d.ts +8 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +12 -1
- 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 -1
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +36 -4
- package/dist/sqlite/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/database-provider/SKILL.md +7 -0
- package/src/mysql/index.ts +62 -3
- package/src/postgres/index.ts +22 -1
- package/src/shared/idempotency-core.ts +33 -14
- package/src/shared/outbox-core.ts +278 -0
- package/src/sqlite/index.ts +68 -10
|
@@ -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,
|
|
@@ -123,6 +124,32 @@ export type DrizzleSqliteTransaction<
|
|
|
123
124
|
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
124
125
|
> = LibSQLTransaction<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
125
126
|
|
|
127
|
+
const sqliteTransactionTails = new WeakMap<object, Promise<void>>();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Queue Beignet-owned transactions per Drizzle client. SQLite only permits one
|
|
131
|
+
* writer at a time, while the local libSQL driver otherwise exposes concurrent
|
|
132
|
+
* transaction attempts as SQLITE_BUSY instead of waiting for the active write.
|
|
133
|
+
*/
|
|
134
|
+
function runSerializedSqliteTransaction<Result>(
|
|
135
|
+
db: object,
|
|
136
|
+
transaction: () => Promise<Result>,
|
|
137
|
+
): Promise<Result> {
|
|
138
|
+
const previous = sqliteTransactionTails.get(db) ?? Promise.resolve();
|
|
139
|
+
const result = previous.then(transaction);
|
|
140
|
+
const tail = result.then(
|
|
141
|
+
() => undefined,
|
|
142
|
+
() => undefined,
|
|
143
|
+
);
|
|
144
|
+
sqliteTransactionTails.set(db, tail);
|
|
145
|
+
|
|
146
|
+
return result.finally(() => {
|
|
147
|
+
if (sqliteTransactionTails.get(db) === tail) {
|
|
148
|
+
sqliteTransactionTails.delete(db);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
async function checkSqliteHealth(client: Client): Promise<DbHealth> {
|
|
127
154
|
try {
|
|
128
155
|
await client.execute("select 1");
|
|
@@ -185,13 +212,15 @@ export function createDrizzleSqliteUnitOfWork<
|
|
|
185
212
|
): Promise<Result> {
|
|
186
213
|
const events = createDomainEventRecorder();
|
|
187
214
|
|
|
188
|
-
const result = await options.db
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
215
|
+
const result = await runSerializedSqliteTransaction(options.db, () =>
|
|
216
|
+
options.db.transaction(async (tx) => {
|
|
217
|
+
const txPorts = options.createTransactionPorts(
|
|
218
|
+
tx as DrizzleSqliteTransaction<TSchema>,
|
|
219
|
+
events,
|
|
220
|
+
);
|
|
221
|
+
return work(txPorts);
|
|
222
|
+
}, options.transactionConfig),
|
|
223
|
+
);
|
|
195
224
|
|
|
196
225
|
if (options.eventBus) {
|
|
197
226
|
await events.flush(options.eventBus);
|
|
@@ -336,6 +365,11 @@ function createSqliteExecutor<TSchema extends Record<string, unknown>>(
|
|
|
336
365
|
},
|
|
337
366
|
|
|
338
367
|
async row(query) {
|
|
368
|
+
if ("all" in db && typeof db.all === "function") {
|
|
369
|
+
const rows = await db.all<Record<string, unknown>>(query);
|
|
370
|
+
return rows[0];
|
|
371
|
+
}
|
|
372
|
+
|
|
339
373
|
return db.get<Record<string, unknown> | undefined>(query);
|
|
340
374
|
},
|
|
341
375
|
|
|
@@ -345,8 +379,12 @@ function createSqliteExecutor<TSchema extends Record<string, unknown>>(
|
|
|
345
379
|
|
|
346
380
|
withTransaction(fn) {
|
|
347
381
|
if ("transaction" in db && typeof db.transaction === "function") {
|
|
348
|
-
return db
|
|
349
|
-
|
|
382
|
+
return runSerializedSqliteTransaction(db, () =>
|
|
383
|
+
db.transaction((tx) =>
|
|
384
|
+
fn(
|
|
385
|
+
createSqliteExecutor(tx as DrizzleSqliteDatabase<TSchema>, table),
|
|
386
|
+
),
|
|
387
|
+
),
|
|
350
388
|
);
|
|
351
389
|
}
|
|
352
390
|
|
|
@@ -411,6 +449,26 @@ export function createDrizzleSqliteOutboxPort<
|
|
|
411
449
|
return createOutboxPortCore(executor, { now: adapterOptions.now });
|
|
412
450
|
}
|
|
413
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Create a Beignet outbox admin port backed by a Drizzle SQLite database.
|
|
454
|
+
*
|
|
455
|
+
* Wire this only into operational contexts that need to inspect, requeue,
|
|
456
|
+
* purge, or prune outbox messages.
|
|
457
|
+
*/
|
|
458
|
+
export function createDrizzleSqliteOutboxAdminPort<
|
|
459
|
+
TSchema extends Record<string, unknown>,
|
|
460
|
+
>(
|
|
461
|
+
db: DrizzleSqliteDatabase<TSchema>,
|
|
462
|
+
adapterOptions: DrizzleSqliteOutboxOptions = {},
|
|
463
|
+
): OutboxAdminPort {
|
|
464
|
+
const table = sql.raw(
|
|
465
|
+
quoteIdentifier(adapterOptions.tableName ?? "outbox_messages", '"'),
|
|
466
|
+
);
|
|
467
|
+
const executor: OutboxSqlExecutor = createSqliteExecutor(db, table);
|
|
468
|
+
|
|
469
|
+
return createOutboxAdminPortCore(executor, { now: adapterOptions.now });
|
|
470
|
+
}
|
|
471
|
+
|
|
414
472
|
/**
|
|
415
473
|
* Options for a Drizzle SQLite-backed idempotency port.
|
|
416
474
|
*/
|