rebackend-drizzle 0.1.0

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.
@@ -0,0 +1,387 @@
1
+ import type { InferInsertModel, InferSelectModel, Table } from 'drizzle-orm'
2
+
3
+ import type {
4
+ AnyDomainEventAction,
5
+ EventOutbox,
6
+ OutboxEntryInput,
7
+ OutboxRecord,
8
+ OutboxRecordStatus,
9
+ OutboxSourceType,
10
+ } from 'rebackend'
11
+
12
+ import { resolveDrizzleExecutor } from './transaction'
13
+
14
+ export type DrizzleOutboxSelectRow<TTable extends Table> = InferSelectModel<TTable>
15
+ export type DrizzleOutboxInsertRowShape<TTable extends Table> = InferInsertModel<TTable>
16
+ export type DrizzleOutboxSelectKey<TTable extends Table> = Extract<
17
+ keyof DrizzleOutboxSelectRow<TTable>,
18
+ string
19
+ >
20
+
21
+ export type DrizzleOutboxColumnMap<
22
+ TTable extends Table,
23
+ TIdKey extends DrizzleOutboxSelectKey<TTable>,
24
+ TEventNameKey extends DrizzleOutboxSelectKey<TTable>,
25
+ TPayloadKey extends DrizzleOutboxSelectKey<TTable>,
26
+ TSourceTypeKey extends DrizzleOutboxSelectKey<TTable>,
27
+ TSourceNameKey extends DrizzleOutboxSelectKey<TTable>,
28
+ TStatusKey extends DrizzleOutboxSelectKey<TTable>,
29
+ TAttemptsKey extends DrizzleOutboxSelectKey<TTable>,
30
+ TCreatedAtKey extends DrizzleOutboxSelectKey<TTable>,
31
+ TDispatchedAtKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
32
+ TLastErrorKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
33
+ > = {
34
+ id: TIdKey
35
+ eventName: TEventNameKey
36
+ payload: TPayloadKey
37
+ sourceType: TSourceTypeKey
38
+ sourceName: TSourceNameKey
39
+ status: TStatusKey
40
+ attempts: TAttemptsKey
41
+ createdAt: TCreatedAtKey
42
+ dispatchedAt?: TDispatchedAtKey
43
+ lastError?: TLastErrorKey
44
+ }
45
+
46
+ export type DrizzleOutboxInsertRow = {
47
+ eventName: string
48
+ payload: string
49
+ sourceType: OutboxSourceType
50
+ sourceName: string
51
+ status: OutboxRecordStatus
52
+ attempts: number
53
+ createdAt: Date
54
+ dispatchedAt: Date | null
55
+ lastError: string | null
56
+ }
57
+
58
+ export type DrizzleOutboxStoredRow = DrizzleOutboxInsertRow & {
59
+ id: number
60
+ }
61
+
62
+ export type DrizzleOutboxCodec<TRow, TInsertRow> = {
63
+ toInsertRow(entry: OutboxEntryInput, now: Date): TInsertRow
64
+ fromStoredRow(row: TRow): OutboxRecord
65
+ }
66
+
67
+ export const createJsonDrizzleOutboxCodec = (): DrizzleOutboxCodec<
68
+ DrizzleOutboxStoredRow,
69
+ DrizzleOutboxInsertRow
70
+ > => {
71
+ return {
72
+ toInsertRow(entry, now) {
73
+ return {
74
+ eventName: entry.event.name,
75
+ payload: JSON.stringify({
76
+ key: entry.event.key,
77
+ version: entry.event.version,
78
+ payload: entry.event.payload,
79
+ }),
80
+ sourceType: entry.sourceType,
81
+ sourceName: entry.sourceName,
82
+ status: 'pending',
83
+ attempts: 0,
84
+ createdAt: now,
85
+ dispatchedAt: null,
86
+ lastError: null,
87
+ }
88
+ },
89
+ fromStoredRow(row) {
90
+ const envelope = JSON.parse(row.payload) as {
91
+ key: string
92
+ version: number
93
+ payload: AnyDomainEventAction['payload']
94
+ }
95
+
96
+ return {
97
+ id: row.id,
98
+ event: {
99
+ kind: 'DomainEventAction',
100
+ key: envelope.key,
101
+ name: row.eventName,
102
+ version: envelope.version,
103
+ payload: envelope.payload,
104
+ },
105
+ sourceType: row.sourceType,
106
+ sourceName: row.sourceName,
107
+ status: row.status,
108
+ attempts: row.attempts,
109
+ createdAt: row.createdAt,
110
+ dispatchedAt: row.dispatchedAt ?? undefined,
111
+ lastError: row.lastError ?? undefined,
112
+ }
113
+ },
114
+ }
115
+ }
116
+
117
+ export type DrizzleOutboxOptions<TDb, TTransaction, TStoredRow, TInsertRow> = {
118
+ db: TDb
119
+ codec: DrizzleOutboxCodec<TStoredRow, TInsertRow>
120
+ insertRows: (executor: TDb | TTransaction, rows: readonly TInsertRow[]) => Promise<void>
121
+ listRows: (executor: TDb | TTransaction) => Promise<readonly TStoredRow[]>
122
+ listPendingRows: (executor: TDb | TTransaction, limit?: number) => Promise<readonly TStoredRow[]>
123
+ markRowsDispatched: (
124
+ executor: TDb | TTransaction,
125
+ ids: readonly number[],
126
+ dispatchedAt: Date,
127
+ ) => Promise<void>
128
+ markRowsFailed: (
129
+ executor: TDb | TTransaction,
130
+ ids: readonly number[],
131
+ error: string,
132
+ ) => Promise<void>
133
+ }
134
+
135
+ export const createDrizzleOutbox = <TDb, TTransaction, TStoredRow, TInsertRow>(
136
+ options: DrizzleOutboxOptions<TDb, TTransaction, TStoredRow, TInsertRow>,
137
+ ): EventOutbox => {
138
+ return {
139
+ async append(entries, context) {
140
+ if (entries.length === 0) {
141
+ return
142
+ }
143
+
144
+ const executor = resolveDrizzleExecutor<TDb, TTransaction>(options.db, context)
145
+ const now = new Date()
146
+ const rows = entries.map((entry) => options.codec.toInsertRow(entry, now))
147
+
148
+ await options.insertRows(executor, rows)
149
+ },
150
+ async list() {
151
+ const executor = resolveDrizzleExecutor<TDb, TTransaction>(options.db)
152
+ const rows = await options.listRows(executor)
153
+
154
+ return rows.map((row) => options.codec.fromStoredRow(row))
155
+ },
156
+ async listPending(limit) {
157
+ const executor = resolveDrizzleExecutor<TDb, TTransaction>(options.db)
158
+ const rows = await options.listPendingRows(executor, limit)
159
+
160
+ return rows.map((row) => options.codec.fromStoredRow(row))
161
+ },
162
+ async markDispatched(ids) {
163
+ if (ids.length === 0) {
164
+ return
165
+ }
166
+
167
+ const executor = resolveDrizzleExecutor<TDb, TTransaction>(options.db)
168
+ await options.markRowsDispatched(executor, ids, new Date())
169
+ },
170
+ async markFailed(ids, error) {
171
+ if (ids.length === 0) {
172
+ return
173
+ }
174
+
175
+ const executor = resolveDrizzleExecutor<TDb, TTransaction>(options.db)
176
+ await options.markRowsFailed(executor, ids, error)
177
+ },
178
+ }
179
+ }
180
+
181
+ export type DrizzleTableOutboxOptions<
182
+ TDb,
183
+ TTransaction,
184
+ TTable extends Table,
185
+ TIdKey extends DrizzleOutboxSelectKey<TTable>,
186
+ TEventNameKey extends DrizzleOutboxSelectKey<TTable>,
187
+ TPayloadKey extends DrizzleOutboxSelectKey<TTable>,
188
+ TSourceTypeKey extends DrizzleOutboxSelectKey<TTable>,
189
+ TSourceNameKey extends DrizzleOutboxSelectKey<TTable>,
190
+ TStatusKey extends DrizzleOutboxSelectKey<TTable>,
191
+ TAttemptsKey extends DrizzleOutboxSelectKey<TTable>,
192
+ TCreatedAtKey extends DrizzleOutboxSelectKey<TTable>,
193
+ TDispatchedAtKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
194
+ TLastErrorKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
195
+ > = {
196
+ db: TDb
197
+ table: TTable
198
+ columns: DrizzleOutboxColumnMap<
199
+ TTable,
200
+ TIdKey,
201
+ TEventNameKey,
202
+ TPayloadKey,
203
+ TSourceTypeKey,
204
+ TSourceNameKey,
205
+ TStatusKey,
206
+ TAttemptsKey,
207
+ TCreatedAtKey,
208
+ TDispatchedAtKey,
209
+ TLastErrorKey
210
+ >
211
+ operations: {
212
+ insertRows(input: {
213
+ executor: TDb | TTransaction
214
+ table: TTable
215
+ rows: readonly DrizzleOutboxInsertRowShape<TTable>[]
216
+ }): Promise<void>
217
+ listRows(input: {
218
+ executor: TDb | TTransaction
219
+ table: TTable
220
+ }): Promise<readonly DrizzleOutboxSelectRow<TTable>[]>
221
+ listPendingRows(input: {
222
+ executor: TDb | TTransaction
223
+ table: TTable
224
+ statusKey: TStatusKey
225
+ pendingStatus: OutboxRecordStatus
226
+ limit?: number
227
+ }): Promise<readonly DrizzleOutboxSelectRow<TTable>[]>
228
+ markRowsDispatched(input: {
229
+ executor: TDb | TTransaction
230
+ table: TTable
231
+ ids: readonly number[]
232
+ idKey: TIdKey
233
+ statusKey: TStatusKey
234
+ attemptsKey: TAttemptsKey
235
+ dispatchedAtKey?: TDispatchedAtKey
236
+ dispatchedAt: Date
237
+ }): Promise<void>
238
+ markRowsFailed(input: {
239
+ executor: TDb | TTransaction
240
+ table: TTable
241
+ ids: readonly number[]
242
+ idKey: TIdKey
243
+ statusKey: TStatusKey
244
+ attemptsKey: TAttemptsKey
245
+ lastErrorKey?: TLastErrorKey
246
+ error: string
247
+ }): Promise<void>
248
+ }
249
+ }
250
+
251
+ export const createDrizzleTableOutbox = <
252
+ TDb,
253
+ TTransaction,
254
+ TTable extends Table,
255
+ TIdKey extends DrizzleOutboxSelectKey<TTable>,
256
+ TEventNameKey extends DrizzleOutboxSelectKey<TTable>,
257
+ TPayloadKey extends DrizzleOutboxSelectKey<TTable>,
258
+ TSourceTypeKey extends DrizzleOutboxSelectKey<TTable>,
259
+ TSourceNameKey extends DrizzleOutboxSelectKey<TTable>,
260
+ TStatusKey extends DrizzleOutboxSelectKey<TTable>,
261
+ TAttemptsKey extends DrizzleOutboxSelectKey<TTable>,
262
+ TCreatedAtKey extends DrizzleOutboxSelectKey<TTable>,
263
+ TDispatchedAtKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
264
+ TLastErrorKey extends DrizzleOutboxSelectKey<TTable> | undefined = undefined,
265
+ >(
266
+ options: DrizzleTableOutboxOptions<
267
+ TDb,
268
+ TTransaction,
269
+ TTable,
270
+ TIdKey,
271
+ TEventNameKey,
272
+ TPayloadKey,
273
+ TSourceTypeKey,
274
+ TSourceNameKey,
275
+ TStatusKey,
276
+ TAttemptsKey,
277
+ TCreatedAtKey,
278
+ TDispatchedAtKey,
279
+ TLastErrorKey
280
+ >,
281
+ ): EventOutbox => {
282
+ const codec: DrizzleOutboxCodec<
283
+ DrizzleOutboxSelectRow<TTable>,
284
+ DrizzleOutboxInsertRowShape<TTable>
285
+ > = {
286
+ toInsertRow(entry, now) {
287
+ const row = {
288
+ [options.columns.eventName]: entry.event.name,
289
+ [options.columns.payload]: JSON.stringify({
290
+ key: entry.event.key,
291
+ version: entry.event.version,
292
+ payload: entry.event.payload,
293
+ }),
294
+ [options.columns.sourceType]: entry.sourceType,
295
+ [options.columns.sourceName]: entry.sourceName,
296
+ [options.columns.status]: 'pending',
297
+ [options.columns.attempts]: 0,
298
+ [options.columns.createdAt]: now,
299
+ }
300
+
301
+ return row as DrizzleOutboxInsertRowShape<TTable>
302
+ },
303
+ fromStoredRow(row) {
304
+ const dispatchedAtKey = options.columns.dispatchedAt
305
+ const lastErrorKey = options.columns.lastError
306
+ const rowRecord = row as Record<string, unknown>
307
+ const envelope = JSON.parse(row[options.columns.payload] as string) as {
308
+ key: string
309
+ version: number
310
+ payload: AnyDomainEventAction['payload']
311
+ }
312
+
313
+ return {
314
+ id: row[options.columns.id] as number,
315
+ event: {
316
+ kind: 'DomainEventAction',
317
+ key: envelope.key,
318
+ name: row[options.columns.eventName] as string,
319
+ version: envelope.version,
320
+ payload: envelope.payload,
321
+ },
322
+ sourceType: row[options.columns.sourceType] as OutboxSourceType,
323
+ sourceName: row[options.columns.sourceName] as string,
324
+ status: row[options.columns.status] as OutboxRecordStatus,
325
+ attempts: row[options.columns.attempts] as number,
326
+ createdAt: row[options.columns.createdAt] as OutboxRecord['createdAt'],
327
+ dispatchedAt: dispatchedAtKey
328
+ ? (rowRecord[dispatchedAtKey] as OutboxRecord['dispatchedAt'])
329
+ : undefined,
330
+ lastError: lastErrorKey
331
+ ? (rowRecord[lastErrorKey] as OutboxRecord['lastError'])
332
+ : undefined,
333
+ }
334
+ },
335
+ }
336
+
337
+ return createDrizzleOutbox<
338
+ TDb,
339
+ TTransaction,
340
+ DrizzleOutboxSelectRow<TTable>,
341
+ DrizzleOutboxInsertRowShape<TTable>
342
+ >({
343
+ db: options.db,
344
+ codec,
345
+ insertRows: (executor, rows) =>
346
+ options.operations.insertRows({
347
+ executor,
348
+ table: options.table,
349
+ rows,
350
+ }),
351
+ listRows: (executor) =>
352
+ options.operations.listRows({
353
+ executor,
354
+ table: options.table,
355
+ }),
356
+ listPendingRows: (executor, limit) =>
357
+ options.operations.listPendingRows({
358
+ executor,
359
+ table: options.table,
360
+ statusKey: options.columns.status,
361
+ pendingStatus: 'pending',
362
+ limit,
363
+ }),
364
+ markRowsDispatched: (executor, ids, dispatchedAt) =>
365
+ options.operations.markRowsDispatched({
366
+ executor,
367
+ table: options.table,
368
+ ids,
369
+ idKey: options.columns.id,
370
+ statusKey: options.columns.status,
371
+ attemptsKey: options.columns.attempts,
372
+ dispatchedAtKey: options.columns.dispatchedAt,
373
+ dispatchedAt,
374
+ }),
375
+ markRowsFailed: (executor, ids, error) =>
376
+ options.operations.markRowsFailed({
377
+ executor,
378
+ table: options.table,
379
+ ids,
380
+ idKey: options.columns.id,
381
+ statusKey: options.columns.status,
382
+ attemptsKey: options.columns.attempts,
383
+ lastErrorKey: options.columns.lastError,
384
+ error,
385
+ }),
386
+ })
387
+ }
@@ -0,0 +1,274 @@
1
+ import {
2
+ and,
3
+ eq,
4
+ getTableColumns,
5
+ inArray,
6
+ sql,
7
+ type InferSelectModel,
8
+ type Table,
9
+ } from 'drizzle-orm'
10
+
11
+ import type { AggregateState, AnyAggregateDefinition } from 'rebackend'
12
+
13
+ import {
14
+ createDrizzleTableAggregateRepository,
15
+ type DrizzleSelectKey,
16
+ type DrizzleTableAggregateRepositoryOptions,
17
+ } from './repository'
18
+ import {
19
+ createDrizzleTableOutbox,
20
+ type DrizzleOutboxColumnMap,
21
+ type DrizzleTableOutboxOptions,
22
+ } from './outbox'
23
+
24
+ type QueryRows<TRow> = PromiseLike<readonly TRow[]>
25
+
26
+ type SelectWhereQuery<TRow> = QueryRows<TRow> & {
27
+ limit(limit: number): QueryRows<TRow>
28
+ }
29
+
30
+ type SelectFromQuery<TTable extends Table> = QueryRows<InferSelectModel<TTable>> & {
31
+ where(condition: unknown): SelectWhereQuery<InferSelectModel<TTable>>
32
+ }
33
+
34
+ export type BasicDrizzleTableExecutor<TTable extends Table> = {
35
+ select(): {
36
+ from(table: TTable): SelectFromQuery<TTable>
37
+ }
38
+ insert(table: TTable): {
39
+ values(values: unknown): unknown
40
+ }
41
+ update(table: TTable): {
42
+ set(values: Record<string, unknown>): {
43
+ where(condition: unknown): unknown
44
+ }
45
+ }
46
+ }
47
+
48
+ export type DrizzleMutationSuccessJudge = (result: unknown) => boolean
49
+
50
+ export type BasicDrizzleTableAggregateRepositoryOptions<
51
+ TAggregate extends AnyAggregateDefinition,
52
+ TDb extends BasicDrizzleTableExecutor<TTable>,
53
+ TTransaction extends BasicDrizzleTableExecutor<TTable>,
54
+ TTable extends Table,
55
+ TIdentityRowKey extends DrizzleSelectKey<TTable>,
56
+ TIdentityStateKey extends Extract<keyof AggregateState<TAggregate>, string>,
57
+ TVersionRowKey extends DrizzleSelectKey<TTable> | undefined = undefined,
58
+ TVersionStateKey extends Extract<keyof AggregateState<TAggregate>, string> | undefined =
59
+ undefined,
60
+ > = Omit<
61
+ DrizzleTableAggregateRepositoryOptions<
62
+ TAggregate,
63
+ TDb,
64
+ TTransaction,
65
+ TTable,
66
+ TIdentityRowKey,
67
+ TIdentityStateKey,
68
+ TVersionRowKey,
69
+ TVersionStateKey
70
+ >,
71
+ 'operations'
72
+ > & {
73
+ didUpdateSucceed: DrizzleMutationSuccessJudge
74
+ }
75
+
76
+ export const createBasicDrizzleTableAggregateRepository = <
77
+ TAggregate extends AnyAggregateDefinition,
78
+ TDb extends BasicDrizzleTableExecutor<TTable>,
79
+ TTransaction extends BasicDrizzleTableExecutor<TTable>,
80
+ TTable extends Table,
81
+ TIdentityRowKey extends DrizzleSelectKey<TTable>,
82
+ TIdentityStateKey extends Extract<keyof AggregateState<TAggregate>, string>,
83
+ TVersionRowKey extends DrizzleSelectKey<TTable> | undefined = undefined,
84
+ TVersionStateKey extends Extract<keyof AggregateState<TAggregate>, string> | undefined =
85
+ undefined,
86
+ >(
87
+ options: BasicDrizzleTableAggregateRepositoryOptions<
88
+ TAggregate,
89
+ TDb,
90
+ TTransaction,
91
+ TTable,
92
+ TIdentityRowKey,
93
+ TIdentityStateKey,
94
+ TVersionRowKey,
95
+ TVersionStateKey
96
+ >,
97
+ ) => {
98
+ const columns = getTableColumns(options.table)
99
+ const identityColumn = columns[options.identity.rowKey]
100
+ const versionRowKey = options.version?.rowKey
101
+ const versionColumn = versionRowKey ? columns[versionRowKey] : undefined
102
+
103
+ const repositoryOptions: DrizzleTableAggregateRepositoryOptions<
104
+ TAggregate,
105
+ TDb,
106
+ TTransaction,
107
+ TTable,
108
+ TIdentityRowKey,
109
+ TIdentityStateKey,
110
+ TVersionRowKey,
111
+ TVersionStateKey
112
+ > = {
113
+ ...options,
114
+ operations: {
115
+ async findById({ executor, table, id }) {
116
+ const rows = await executor.select().from(table).where(eq(identityColumn, id)).limit(1)
117
+ return rows[0] ?? null
118
+ },
119
+ async insertOne({ executor, table, row }) {
120
+ await executor.insert(table).values(row)
121
+ },
122
+ async updateById({ executor, table, id, row, optimisticLock, expectedVersion }) {
123
+ const whereCondition =
124
+ optimisticLock && versionColumn && expectedVersion !== undefined
125
+ ? and(eq(identityColumn, id), eq(versionColumn, expectedVersion))
126
+ : eq(identityColumn, id)
127
+
128
+ const result = await executor.update(table).set(row).where(whereCondition)
129
+ return options.didUpdateSucceed(result)
130
+ },
131
+ },
132
+ }
133
+
134
+ return createDrizzleTableAggregateRepository(repositoryOptions)
135
+ }
136
+
137
+ export type BasicDrizzleTableOutboxOptions<
138
+ TDb extends BasicDrizzleTableExecutor<TTable>,
139
+ _TTransaction extends BasicDrizzleTableExecutor<TTable>,
140
+ TTable extends Table,
141
+ TIdKey extends Extract<keyof InferSelectModel<TTable>, string>,
142
+ TEventNameKey extends Extract<keyof InferSelectModel<TTable>, string>,
143
+ TPayloadKey extends Extract<keyof InferSelectModel<TTable>, string>,
144
+ TSourceTypeKey extends Extract<keyof InferSelectModel<TTable>, string>,
145
+ TSourceNameKey extends Extract<keyof InferSelectModel<TTable>, string>,
146
+ TStatusKey extends Extract<keyof InferSelectModel<TTable>, string>,
147
+ TAttemptsKey extends Extract<keyof InferSelectModel<TTable>, string>,
148
+ TCreatedAtKey extends Extract<keyof InferSelectModel<TTable>, string>,
149
+ TDispatchedAtKey extends Extract<keyof InferSelectModel<TTable>, string> | undefined = undefined,
150
+ TLastErrorKey extends Extract<keyof InferSelectModel<TTable>, string> | undefined = undefined,
151
+ > = {
152
+ db: TDb
153
+ table: TTable
154
+ columns: DrizzleOutboxColumnMap<
155
+ TTable,
156
+ TIdKey,
157
+ TEventNameKey,
158
+ TPayloadKey,
159
+ TSourceTypeKey,
160
+ TSourceNameKey,
161
+ TStatusKey,
162
+ TAttemptsKey,
163
+ TCreatedAtKey,
164
+ TDispatchedAtKey,
165
+ TLastErrorKey
166
+ >
167
+ }
168
+
169
+ export const createBasicDrizzleTableOutbox = <
170
+ TDb extends BasicDrizzleTableExecutor<TTable>,
171
+ _TTransaction extends BasicDrizzleTableExecutor<TTable>,
172
+ TTable extends Table,
173
+ TIdKey extends Extract<keyof InferSelectModel<TTable>, string>,
174
+ TEventNameKey extends Extract<keyof InferSelectModel<TTable>, string>,
175
+ TPayloadKey extends Extract<keyof InferSelectModel<TTable>, string>,
176
+ TSourceTypeKey extends Extract<keyof InferSelectModel<TTable>, string>,
177
+ TSourceNameKey extends Extract<keyof InferSelectModel<TTable>, string>,
178
+ TStatusKey extends Extract<keyof InferSelectModel<TTable>, string>,
179
+ TAttemptsKey extends Extract<keyof InferSelectModel<TTable>, string>,
180
+ TCreatedAtKey extends Extract<keyof InferSelectModel<TTable>, string>,
181
+ TDispatchedAtKey extends Extract<keyof InferSelectModel<TTable>, string> | undefined = undefined,
182
+ TLastErrorKey extends Extract<keyof InferSelectModel<TTable>, string> | undefined = undefined,
183
+ >(
184
+ options: BasicDrizzleTableOutboxOptions<
185
+ TDb,
186
+ _TTransaction,
187
+ TTable,
188
+ TIdKey,
189
+ TEventNameKey,
190
+ TPayloadKey,
191
+ TSourceTypeKey,
192
+ TSourceNameKey,
193
+ TStatusKey,
194
+ TAttemptsKey,
195
+ TCreatedAtKey,
196
+ TDispatchedAtKey,
197
+ TLastErrorKey
198
+ >,
199
+ ) => {
200
+ const columns = getTableColumns(options.table)
201
+ const idColumn = columns[options.columns.id]
202
+ const attemptsColumn = columns[options.columns.attempts]
203
+ const statusColumn = columns[options.columns.status]
204
+
205
+ const outboxOptions: DrizzleTableOutboxOptions<
206
+ TDb,
207
+ _TTransaction,
208
+ TTable,
209
+ TIdKey,
210
+ TEventNameKey,
211
+ TPayloadKey,
212
+ TSourceTypeKey,
213
+ TSourceNameKey,
214
+ TStatusKey,
215
+ TAttemptsKey,
216
+ TCreatedAtKey,
217
+ TDispatchedAtKey,
218
+ TLastErrorKey
219
+ > = {
220
+ ...options,
221
+ operations: {
222
+ async insertRows({ executor, table, rows }) {
223
+ await executor.insert(table).values(rows)
224
+ },
225
+ async listRows({ executor, table }) {
226
+ return executor.select().from(table)
227
+ },
228
+ async listPendingRows({ executor, table, pendingStatus, limit }) {
229
+ const query = executor.select().from(table).where(eq(statusColumn, pendingStatus))
230
+ return limit === undefined ? query : query.limit(limit)
231
+ },
232
+ async markRowsDispatched({
233
+ executor,
234
+ table,
235
+ ids,
236
+ statusKey,
237
+ attemptsKey,
238
+ dispatchedAtKey,
239
+ dispatchedAt,
240
+ }) {
241
+ const updateRow: Record<string, unknown> = {
242
+ [statusKey]: 'dispatched',
243
+ [attemptsKey]: sql`${attemptsColumn} + 1`,
244
+ }
245
+
246
+ if (dispatchedAtKey) {
247
+ updateRow[dispatchedAtKey] = dispatchedAt
248
+ }
249
+
250
+ await executor
251
+ .update(table)
252
+ .set(updateRow)
253
+ .where(inArray(idColumn, [...ids]))
254
+ },
255
+ async markRowsFailed({ executor, table, ids, statusKey, attemptsKey, lastErrorKey, error }) {
256
+ const updateRow: Record<string, unknown> = {
257
+ [statusKey]: 'failed',
258
+ [attemptsKey]: sql`${attemptsColumn} + 1`,
259
+ }
260
+
261
+ if (lastErrorKey) {
262
+ updateRow[lastErrorKey] = error
263
+ }
264
+
265
+ await executor
266
+ .update(table)
267
+ .set(updateRow)
268
+ .where(inArray(idColumn, [...ids]))
269
+ },
270
+ },
271
+ }
272
+
273
+ return createDrizzleTableOutbox(outboxOptions)
274
+ }