@remit/drizzle-service 0.0.63 → 0.0.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.63",
3
+ "version": "0.0.64",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -105,6 +105,52 @@ describe("OutboxMessageRepo", () => {
105
105
  await repo.delete(accountConfigId, msg.outboxMessageId);
106
106
  });
107
107
 
108
+ test("updateIfStatus writes on the expected status and refuses any other", async () => {
109
+ const accountConfigId = randomId();
110
+ const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
111
+
112
+ const written = await repo.updateIfStatus(
113
+ accountConfigId,
114
+ msg.outboxMessageId,
115
+ "queued",
116
+ { status: "failed", lastError: "the queue refused it" },
117
+ );
118
+ assert.equal(written?.status, "failed");
119
+
120
+ // The compare-and-set every outbox transition rests on: a caller that read
121
+ // `queued` and decided on it must not overwrite a row the worker has since
122
+ // moved. `null` says another writer got there first.
123
+ const refused = await repo.updateIfStatus(
124
+ accountConfigId,
125
+ msg.outboxMessageId,
126
+ "queued",
127
+ { status: "draft" },
128
+ );
129
+ assert.equal(refused, null);
130
+ const still = await repo.get(accountConfigId, msg.outboxMessageId);
131
+ assert.equal(still.status, "failed");
132
+
133
+ await repo.delete(accountConfigId, msg.outboxMessageId);
134
+ });
135
+
136
+ test("cross-tenant: updateIfStatus refuses a foreign accountConfig", async () => {
137
+ const accountConfigId = randomId();
138
+ const other = randomId();
139
+ const msg = await repo.create(makeOutboxInput(randomId(), accountConfigId));
140
+
141
+ const refused = await repo.updateIfStatus(
142
+ other,
143
+ msg.outboxMessageId,
144
+ "queued",
145
+ { status: "failed" },
146
+ );
147
+ assert.equal(refused, null);
148
+ const still = await repo.get(accountConfigId, msg.outboxMessageId);
149
+ assert.equal(still.status, "queued");
150
+
151
+ await repo.delete(accountConfigId, msg.outboxMessageId);
152
+ });
153
+
108
154
  test("markSent clears lastError and lastSmtpCode", async () => {
109
155
  const accountConfigId = randomId();
110
156
  const msg = await repo.create({
@@ -129,6 +129,40 @@ export class OutboxMessageRepo implements IOutboxMessageRepository {
129
129
  outboxMessageId: string,
130
130
  input: UpdateOutboxMessageInput,
131
131
  ): Promise<OutboxMessageItem> {
132
+ const [row] = await this.applyUpdate(
133
+ accountConfigId,
134
+ outboxMessageId,
135
+ input,
136
+ );
137
+ if (!row)
138
+ throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
139
+ return rowToOutboxMessage(row);
140
+ }
141
+
142
+ async updateIfStatus(
143
+ accountConfigId: string,
144
+ outboxMessageId: string,
145
+ expected: OutboxMessageItem["status"],
146
+ input: UpdateOutboxMessageInput,
147
+ ): Promise<OutboxMessageItem | null> {
148
+ const [row] = await this.applyUpdate(
149
+ accountConfigId,
150
+ outboxMessageId,
151
+ input,
152
+ expected,
153
+ );
154
+ // No row means the status moved or the row went away — both say another
155
+ // writer got there first, which is the caller's answer rather than an
156
+ // error here.
157
+ return row ? rowToOutboxMessage(row) : null;
158
+ }
159
+
160
+ private async applyUpdate(
161
+ accountConfigId: string,
162
+ outboxMessageId: string,
163
+ input: UpdateOutboxMessageInput,
164
+ expected?: OutboxMessageItem["status"],
165
+ ): Promise<(typeof outboxMessageTable.$inferSelect)[]> {
132
166
  const now = Date.now();
133
167
  const updates: Partial<typeof outboxMessageTable.$inferInsert> = {
134
168
  updatedAt: now,
@@ -154,19 +188,20 @@ export class OutboxMessageRepo implements IOutboxMessageRepository {
154
188
  if (input.inReplyTo !== undefined) updates.inReplyTo = input.inReplyTo;
155
189
  if (input.references !== undefined) updates.references = input.references;
156
190
 
157
- const [row] = await this.db
191
+ const rows = await this.db
158
192
  .update(outboxMessageTable)
159
193
  .set(updates)
160
194
  .where(
161
195
  and(
162
196
  eq(outboxMessageTable.accountConfigId, accountConfigId),
163
197
  eq(outboxMessageTable.outboxMessageId, outboxMessageId),
198
+ expected === undefined
199
+ ? undefined
200
+ : eq(outboxMessageTable.status, expected),
164
201
  ),
165
202
  )
166
203
  .returning();
167
- if (!row)
168
- throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
169
- return rowToOutboxMessage(row);
204
+ return rows;
170
205
  }
171
206
 
172
207
  async updateStatus(