@remit/drizzle-service 0.0.62 → 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,32 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessageFlagPushItem,
|
|
3
|
+
PutMessageFlagPushInput,
|
|
4
|
+
} from "@remit/data-ports";
|
|
1
5
|
import { and, eq } from "drizzle-orm";
|
|
2
6
|
import type { Db } from "../db.js";
|
|
3
7
|
import { messageFlagPushTable } from "../schema/i4-message-flag-push.js";
|
|
4
8
|
|
|
5
9
|
type DB = Db<Record<string, unknown>>;
|
|
6
10
|
|
|
7
|
-
export type
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
| "queued"
|
|
11
|
-
| "processing"
|
|
12
|
-
| "processed";
|
|
13
|
-
|
|
14
|
-
export interface MessageFlagPushItem {
|
|
15
|
-
messageId: string;
|
|
16
|
-
flagName: string;
|
|
17
|
-
accountId: string;
|
|
18
|
-
accountConfigId: string;
|
|
19
|
-
mailboxId: string;
|
|
20
|
-
operation: FlagPushOperation;
|
|
21
|
-
state: MessageFlagPushState;
|
|
22
|
-
createdAt: number;
|
|
23
|
-
updatedAt: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type PutMessageFlagPushInput = Omit<
|
|
27
|
-
MessageFlagPushItem,
|
|
28
|
-
"state" | "createdAt" | "updatedAt"
|
|
29
|
-
>;
|
|
11
|
+
export type { MessageFlagPushItem, PutMessageFlagPushInput };
|
|
12
|
+
export type FlagPushOperation = MessageFlagPushItem["operation"];
|
|
13
|
+
export type MessageFlagPushState = MessageFlagPushItem["state"];
|
|
30
14
|
|
|
31
15
|
const DEFAULT_STATE: MessageFlagPushState = "pending";
|
|
32
16
|
|
|
@@ -1,30 +1,15 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessagePlacementMoveItem,
|
|
3
|
+
PutMessagePlacementMoveInput,
|
|
4
|
+
} from "@remit/data-ports";
|
|
1
5
|
import { eq } from "drizzle-orm";
|
|
2
6
|
import type { Db } from "../db.js";
|
|
3
7
|
import { messagePlacementMoveTable } from "../schema/i4-message-placement-move.js";
|
|
4
8
|
|
|
5
9
|
type DB = Db<Record<string, unknown>>;
|
|
6
10
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
| "queued"
|
|
10
|
-
| "processing"
|
|
11
|
-
| "processed";
|
|
12
|
-
|
|
13
|
-
export interface MessagePlacementMoveItem {
|
|
14
|
-
messageId: string;
|
|
15
|
-
accountId: string;
|
|
16
|
-
accountConfigId: string;
|
|
17
|
-
sourceMailboxId: string;
|
|
18
|
-
destinationMailboxId: string;
|
|
19
|
-
state: MessagePlacementMoveState;
|
|
20
|
-
createdAt: number;
|
|
21
|
-
updatedAt: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type PutMessagePlacementMoveInput = Omit<
|
|
25
|
-
MessagePlacementMoveItem,
|
|
26
|
-
"state" | "createdAt" | "updatedAt"
|
|
27
|
-
>;
|
|
11
|
+
export type { MessagePlacementMoveItem, PutMessagePlacementMoveInput };
|
|
12
|
+
export type MessagePlacementMoveState = MessagePlacementMoveItem["state"];
|
|
28
13
|
|
|
29
14
|
const DEFAULT_STATE: MessagePlacementMoveState = "pending";
|
|
30
15
|
|
|
@@ -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
|
|
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
|
-
|
|
168
|
-
throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
|
|
169
|
-
return rowToOutboxMessage(row);
|
|
204
|
+
return rows;
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
async updateStatus(
|
|
@@ -19,26 +19,17 @@ import {
|
|
|
19
19
|
type SQL,
|
|
20
20
|
sql,
|
|
21
21
|
} from "drizzle-orm";
|
|
22
|
-
import shortUuid from "short-uuid";
|
|
23
|
-
import { v5 as uuidv5 } from "uuid";
|
|
24
22
|
import type { Db } from "../db.js";
|
|
25
23
|
import { NotFoundError } from "../error.js";
|
|
24
|
+
import { deterministicBase36Id } from "../id.js";
|
|
26
25
|
import { decodeToken } from "../pagination.js";
|
|
27
26
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
28
27
|
import { fromMatch, subjectMatch } from "./thread-search-predicates.js";
|
|
29
28
|
|
|
30
|
-
// ─── ID generation (mirrors remit-electrodb-service/src/id.ts) ───────────────
|
|
31
|
-
|
|
32
|
-
const REMIT_NAMESPACE = "9e89694d-214b-4d9b-99f5-214b4d9b99f5";
|
|
33
|
-
const translator = shortUuid.createTranslator(shortUuid.constants.uuid25Base36);
|
|
34
|
-
|
|
35
|
-
const base36uuidv5 = (name: string): string =>
|
|
36
|
-
translator.fromUUID(uuidv5(name, REMIT_NAMESPACE));
|
|
37
|
-
|
|
38
29
|
export const deriveThreadMessageId = (
|
|
39
30
|
threadId: string,
|
|
40
31
|
messageId: string,
|
|
41
|
-
): string =>
|
|
32
|
+
): string => deterministicBase36Id(`threadmsg:${threadId}:${messageId}`);
|
|
42
33
|
|
|
43
34
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
44
35
|
|