@remit/drizzle-service 0.0.1
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/drizzle.config.ts +15 -0
- package/package.json +52 -0
- package/src/db.ts +13 -0
- package/src/dialect.ts +13 -0
- package/src/error.ts +37 -0
- package/src/id.ts +50 -0
- package/src/index.ts +62 -0
- package/src/pagination.ts +29 -0
- package/src/repos/cascade-delete.sqlite.test.ts +159 -0
- package/src/repos/cascade-delete.test.ts +423 -0
- package/src/repos/cascade-delete.ts +219 -0
- package/src/repos/envelope.sqlite.test.ts +94 -0
- package/src/repos/envelope.test.ts +225 -0
- package/src/repos/envelope.ts +342 -0
- package/src/repos/filter-anchor.test.ts +131 -0
- package/src/repos/filter-anchor.ts +105 -0
- package/src/repos/filter.test.ts +279 -0
- package/src/repos/filter.ts +253 -0
- package/src/repos/i4-account-config.test.ts +105 -0
- package/src/repos/i4-account-config.ts +257 -0
- package/src/repos/i4-account-export-request.ts +141 -0
- package/src/repos/i4-account-setting.test.ts +94 -0
- package/src/repos/i4-account-setting.ts +92 -0
- package/src/repos/i4-account.test.ts +223 -0
- package/src/repos/i4-account.ts +380 -0
- package/src/repos/i4-address-wellknown.ts +31 -0
- package/src/repos/i4-address.test.ts +358 -0
- package/src/repos/i4-address.ts +613 -0
- package/src/repos/i4-mailbox-lock.test.ts +368 -0
- package/src/repos/i4-mailbox-lock.ts +140 -0
- package/src/repos/i4-mailbox-special-use.ts +165 -0
- package/src/repos/i4-mailbox.test.ts +188 -0
- package/src/repos/i4-mailbox.ts +347 -0
- package/src/repos/i4-message-flag-push.test.ts +189 -0
- package/src/repos/i4-message-flag-push.ts +173 -0
- package/src/repos/i4-message-placement-move.test.ts +135 -0
- package/src/repos/i4-message-placement-move.ts +144 -0
- package/src/repos/i4-organize-job-request.ts +142 -0
- package/src/repos/i4-outbox-message.test.ts +298 -0
- package/src/repos/i4-outbox-message.ts +299 -0
- package/src/repos/label.conformance.sqlite.test.ts +23 -0
- package/src/repos/label.conformance.test.ts +19 -0
- package/src/repos/label.ts +125 -0
- package/src/repos/mappers.ts +171 -0
- package/src/repos/message-flag.ts +162 -0
- package/src/repos/message-label.test.ts +110 -0
- package/src/repos/message-label.ts +96 -0
- package/src/repos/message.sqlite.test.ts +118 -0
- package/src/repos/message.test.ts +488 -0
- package/src/repos/message.ts +558 -0
- package/src/repos/serialized-writes.sqlite.test.ts +199 -0
- package/src/repos/test-helpers.ts +222 -0
- package/src/repos/thread-message.sqlite.test.ts +198 -0
- package/src/repos/thread-message.test.ts +744 -0
- package/src/repos/thread-message.ts +832 -0
- package/src/repos/thread-search-predicates.ts +79 -0
- package/src/repos/unit-of-work.sqlite.test.ts +138 -0
- package/src/repos/unit-of-work.test.ts +105 -0
- package/src/repos/unit-of-work.ts +31 -0
- package/src/schema/active-entities.ts +19 -0
- package/src/schema/i4-account-config.ts +4 -0
- package/src/schema/i4-account-export-request.ts +3 -0
- package/src/schema/i4-account-setting.ts +3 -0
- package/src/schema/i4-address.ts +3 -0
- package/src/schema/i4-mailbox-lock.ts +3 -0
- package/src/schema/i4-mailbox.ts +4 -0
- package/src/schema/i4-message-flag-push.ts +3 -0
- package/src/schema/i4-message-placement-move.ts +3 -0
- package/src/schema/i4-organize-job-request.ts +1 -0
- package/src/schema/i4-outbox-message.ts +3 -0
- package/src/schema/message-data.ts +44 -0
- package/src/schema/outbox.ts +74 -0
- package/src/schema/thread-message.ts +3 -0
- package/src/schema-full-sqlite.ts +11 -0
- package/src/schema-full.ts +16 -0
- package/src/schema.ts +28 -0
- package/src/sqlite-client.ts +52 -0
- package/src/test-db-sqlite.ts +75 -0
- package/src/test-db.ts +76 -0
- package/src/tx.ts +208 -0
- package/src/vps-migrations-drift.test.ts +34 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateOutboxMessageInput,
|
|
3
|
+
IOutboxMessageRepository,
|
|
4
|
+
OutboxMessageItem,
|
|
5
|
+
ResultList,
|
|
6
|
+
UpdateOutboxMessageInput,
|
|
7
|
+
} from "@remit/data-ports";
|
|
8
|
+
import { and, desc, eq, inArray, lt, or } from "drizzle-orm";
|
|
9
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
10
|
+
import { ForbiddenError, NotFoundError } from "../error.js";
|
|
11
|
+
import { decodeToken, resultList } from "../pagination.js";
|
|
12
|
+
import { outboxMessageTable } from "../schema/i4-outbox-message.js";
|
|
13
|
+
|
|
14
|
+
type DB = NodePgDatabase<Record<string, unknown>>;
|
|
15
|
+
|
|
16
|
+
function rowToOutboxMessage(
|
|
17
|
+
row: typeof outboxMessageTable.$inferSelect,
|
|
18
|
+
): OutboxMessageItem {
|
|
19
|
+
return {
|
|
20
|
+
outboxMessageId: row.outboxMessageId,
|
|
21
|
+
accountId: row.accountId,
|
|
22
|
+
accountConfigId: row.accountConfigId,
|
|
23
|
+
fromAddress: row.fromAddress,
|
|
24
|
+
fromName: row.fromName ?? undefined,
|
|
25
|
+
toAddresses: (row.toAddresses as string[]) ?? [],
|
|
26
|
+
ccAddresses: (row.ccAddresses as string[]) ?? [],
|
|
27
|
+
bccAddresses: (row.bccAddresses as string[]) ?? [],
|
|
28
|
+
replyToAddress: row.replyToAddress ?? undefined,
|
|
29
|
+
subject: row.subject ?? undefined,
|
|
30
|
+
messageIdValue: row.messageIdValue,
|
|
31
|
+
inReplyTo: row.inReplyTo ?? undefined,
|
|
32
|
+
references: (row.references as string[]) ?? [],
|
|
33
|
+
textBody: row.textBody ?? undefined,
|
|
34
|
+
htmlBody: row.htmlBody ?? undefined,
|
|
35
|
+
status: row.status as OutboxMessageItem["status"],
|
|
36
|
+
lastError: row.lastError ?? undefined,
|
|
37
|
+
lastSmtpCode: row.lastSmtpCode ?? undefined,
|
|
38
|
+
sentAt: row.sentAt ?? undefined,
|
|
39
|
+
smtpMessageId: row.smtpMessageId ?? undefined,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
updatedAt: row.updatedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class OutboxMessageRepo implements IOutboxMessageRepository {
|
|
46
|
+
constructor(private db: DB) {}
|
|
47
|
+
|
|
48
|
+
async create(input: CreateOutboxMessageInput): Promise<OutboxMessageItem> {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const [row] = await this.db
|
|
51
|
+
.insert(outboxMessageTable)
|
|
52
|
+
.values({
|
|
53
|
+
accountId: input.accountId,
|
|
54
|
+
accountConfigId: input.accountConfigId,
|
|
55
|
+
fromAddress: input.fromAddress,
|
|
56
|
+
fromName: input.fromName,
|
|
57
|
+
toAddresses: input.toAddresses ?? [],
|
|
58
|
+
ccAddresses: input.ccAddresses ?? [],
|
|
59
|
+
bccAddresses: input.bccAddresses ?? [],
|
|
60
|
+
replyToAddress: input.replyToAddress,
|
|
61
|
+
subject: input.subject,
|
|
62
|
+
messageIdValue: input.messageIdValue,
|
|
63
|
+
inReplyTo: input.inReplyTo,
|
|
64
|
+
references: input.references ?? [],
|
|
65
|
+
textBody: input.textBody,
|
|
66
|
+
htmlBody: input.htmlBody,
|
|
67
|
+
status: input.status,
|
|
68
|
+
lastError: input.lastError,
|
|
69
|
+
lastSmtpCode: input.lastSmtpCode,
|
|
70
|
+
sentAt: input.sentAt,
|
|
71
|
+
smtpMessageId: input.smtpMessageId,
|
|
72
|
+
createdAt: now,
|
|
73
|
+
updatedAt: now,
|
|
74
|
+
})
|
|
75
|
+
.returning();
|
|
76
|
+
return rowToOutboxMessage(row);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async get(
|
|
80
|
+
accountConfigId: string,
|
|
81
|
+
outboxMessageId: string,
|
|
82
|
+
mode?: "read" | "act",
|
|
83
|
+
): Promise<OutboxMessageItem>;
|
|
84
|
+
async get(
|
|
85
|
+
accountConfigId: string,
|
|
86
|
+
outboxMessageIds: string[],
|
|
87
|
+
): Promise<OutboxMessageItem[]>;
|
|
88
|
+
async get(
|
|
89
|
+
accountConfigId: string,
|
|
90
|
+
outboxMessageId: string | string[],
|
|
91
|
+
mode: "read" | "act" = "read",
|
|
92
|
+
): Promise<OutboxMessageItem | OutboxMessageItem[]> {
|
|
93
|
+
if (Array.isArray(outboxMessageId)) {
|
|
94
|
+
if (outboxMessageId.length === 0) return [];
|
|
95
|
+
const rows = await this.db
|
|
96
|
+
.select()
|
|
97
|
+
.from(outboxMessageTable)
|
|
98
|
+
.where(
|
|
99
|
+
and(
|
|
100
|
+
eq(outboxMessageTable.accountConfigId, accountConfigId),
|
|
101
|
+
inArray(outboxMessageTable.outboxMessageId, outboxMessageId),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return rows.map(rowToOutboxMessage);
|
|
105
|
+
}
|
|
106
|
+
// Fetch unscoped by id so a foreign row can be distinguished from an
|
|
107
|
+
// absent one — mode="act" reports the former as 403, not 404.
|
|
108
|
+
const [row] = await this.db
|
|
109
|
+
.select()
|
|
110
|
+
.from(outboxMessageTable)
|
|
111
|
+
.where(eq(outboxMessageTable.outboxMessageId, outboxMessageId));
|
|
112
|
+
if (!row) {
|
|
113
|
+
throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
|
|
114
|
+
}
|
|
115
|
+
if (row.accountConfigId !== accountConfigId) {
|
|
116
|
+
if (mode === "act") {
|
|
117
|
+
throw new ForbiddenError(
|
|
118
|
+
`OutboxMessage ${outboxMessageId} not in account config`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
|
|
122
|
+
}
|
|
123
|
+
return rowToOutboxMessage(row);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async update(
|
|
127
|
+
accountConfigId: string,
|
|
128
|
+
outboxMessageId: string,
|
|
129
|
+
input: UpdateOutboxMessageInput,
|
|
130
|
+
): Promise<OutboxMessageItem> {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const updates: Partial<typeof outboxMessageTable.$inferInsert> = {
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
};
|
|
135
|
+
if (input.status !== undefined) updates.status = input.status;
|
|
136
|
+
if (input.lastError !== undefined) updates.lastError = input.lastError;
|
|
137
|
+
if (input.lastSmtpCode !== undefined)
|
|
138
|
+
updates.lastSmtpCode = input.lastSmtpCode;
|
|
139
|
+
if (input.sentAt !== undefined) updates.sentAt = input.sentAt;
|
|
140
|
+
if (input.smtpMessageId !== undefined)
|
|
141
|
+
updates.smtpMessageId = input.smtpMessageId;
|
|
142
|
+
if (input.toAddresses !== undefined)
|
|
143
|
+
updates.toAddresses = input.toAddresses;
|
|
144
|
+
if (input.ccAddresses !== undefined)
|
|
145
|
+
updates.ccAddresses = input.ccAddresses;
|
|
146
|
+
if (input.bccAddresses !== undefined)
|
|
147
|
+
updates.bccAddresses = input.bccAddresses;
|
|
148
|
+
if (input.subject !== undefined) updates.subject = input.subject;
|
|
149
|
+
if (input.textBody !== undefined) updates.textBody = input.textBody;
|
|
150
|
+
if (input.htmlBody !== undefined) updates.htmlBody = input.htmlBody;
|
|
151
|
+
if (input.inReplyTo !== undefined) updates.inReplyTo = input.inReplyTo;
|
|
152
|
+
if (input.references !== undefined) updates.references = input.references;
|
|
153
|
+
|
|
154
|
+
const [row] = await this.db
|
|
155
|
+
.update(outboxMessageTable)
|
|
156
|
+
.set(updates)
|
|
157
|
+
.where(
|
|
158
|
+
and(
|
|
159
|
+
eq(outboxMessageTable.accountConfigId, accountConfigId),
|
|
160
|
+
eq(outboxMessageTable.outboxMessageId, outboxMessageId),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
.returning();
|
|
164
|
+
if (!row)
|
|
165
|
+
throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
|
|
166
|
+
return rowToOutboxMessage(row);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async updateStatus(
|
|
170
|
+
accountConfigId: string,
|
|
171
|
+
outboxMessageId: string,
|
|
172
|
+
status: OutboxMessageItem["status"],
|
|
173
|
+
): Promise<OutboxMessageItem> {
|
|
174
|
+
return this.update(accountConfigId, outboxMessageId, { status });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async markSent(
|
|
178
|
+
accountConfigId: string,
|
|
179
|
+
outboxMessageId: string,
|
|
180
|
+
fields: { sentAt: number; smtpMessageId?: string },
|
|
181
|
+
): Promise<OutboxMessageItem> {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
const [row] = await this.db
|
|
184
|
+
.update(outboxMessageTable)
|
|
185
|
+
.set({
|
|
186
|
+
status: "sent",
|
|
187
|
+
sentAt: fields.sentAt,
|
|
188
|
+
smtpMessageId: fields.smtpMessageId,
|
|
189
|
+
lastError: null,
|
|
190
|
+
lastSmtpCode: null,
|
|
191
|
+
updatedAt: now,
|
|
192
|
+
})
|
|
193
|
+
.where(
|
|
194
|
+
and(
|
|
195
|
+
eq(outboxMessageTable.accountConfigId, accountConfigId),
|
|
196
|
+
eq(outboxMessageTable.outboxMessageId, outboxMessageId),
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
.returning();
|
|
200
|
+
if (!row)
|
|
201
|
+
throw new NotFoundError(`OutboxMessage not found: ${outboxMessageId}`);
|
|
202
|
+
return rowToOutboxMessage(row);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async delete(
|
|
206
|
+
accountConfigId: string,
|
|
207
|
+
outboxMessageId: string,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
await this.db
|
|
210
|
+
.delete(outboxMessageTable)
|
|
211
|
+
.where(
|
|
212
|
+
and(
|
|
213
|
+
eq(outboxMessageTable.accountConfigId, accountConfigId),
|
|
214
|
+
eq(outboxMessageTable.outboxMessageId, outboxMessageId),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async deleteMany(
|
|
220
|
+
accountConfigId: string,
|
|
221
|
+
outboxMessageIds: string[],
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
if (outboxMessageIds.length === 0) return;
|
|
224
|
+
await this.db
|
|
225
|
+
.delete(outboxMessageTable)
|
|
226
|
+
.where(
|
|
227
|
+
and(
|
|
228
|
+
eq(outboxMessageTable.accountConfigId, accountConfigId),
|
|
229
|
+
inArray(outboxMessageTable.outboxMessageId, outboxMessageIds),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async listByAccount(
|
|
235
|
+
accountId: string,
|
|
236
|
+
options?: { limit?: number; continuationToken?: string },
|
|
237
|
+
): Promise<ResultList<OutboxMessageItem>> {
|
|
238
|
+
const limit = options?.limit ?? 100;
|
|
239
|
+
const cursor = options?.continuationToken
|
|
240
|
+
? decodeToken(options.continuationToken)
|
|
241
|
+
: undefined;
|
|
242
|
+
const after = cursor
|
|
243
|
+
? {
|
|
244
|
+
createdAt: cursor.createdAt as number,
|
|
245
|
+
outboxMessageId: cursor.outboxMessageId as string,
|
|
246
|
+
}
|
|
247
|
+
: undefined;
|
|
248
|
+
|
|
249
|
+
const rows = await this.db
|
|
250
|
+
.select()
|
|
251
|
+
.from(outboxMessageTable)
|
|
252
|
+
.where(
|
|
253
|
+
and(
|
|
254
|
+
eq(outboxMessageTable.accountId, accountId),
|
|
255
|
+
after
|
|
256
|
+
? or(
|
|
257
|
+
lt(outboxMessageTable.createdAt, after.createdAt),
|
|
258
|
+
and(
|
|
259
|
+
eq(outboxMessageTable.createdAt, after.createdAt),
|
|
260
|
+
lt(outboxMessageTable.outboxMessageId, after.outboxMessageId),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
: undefined,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
.orderBy(
|
|
267
|
+
desc(outboxMessageTable.createdAt),
|
|
268
|
+
desc(outboxMessageTable.outboxMessageId),
|
|
269
|
+
)
|
|
270
|
+
.limit(limit + 1);
|
|
271
|
+
|
|
272
|
+
const hasMore = rows.length > limit;
|
|
273
|
+
const items = rows.slice(0, limit).map(rowToOutboxMessage);
|
|
274
|
+
const lastItem = items[items.length - 1];
|
|
275
|
+
return resultList(
|
|
276
|
+
items,
|
|
277
|
+
limit,
|
|
278
|
+
hasMore && lastItem
|
|
279
|
+
? {
|
|
280
|
+
createdAt: lastItem.createdAt,
|
|
281
|
+
outboxMessageId: lastItem.outboxMessageId,
|
|
282
|
+
}
|
|
283
|
+
: undefined,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async listQueued(accountId: string): Promise<OutboxMessageItem[]> {
|
|
288
|
+
const rows = await this.db
|
|
289
|
+
.select()
|
|
290
|
+
.from(outboxMessageTable)
|
|
291
|
+
.where(
|
|
292
|
+
and(
|
|
293
|
+
eq(outboxMessageTable.accountId, accountId),
|
|
294
|
+
eq(outboxMessageTable.status, "queued"),
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
return rows.map(rowToOutboxMessage);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { labelRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { randomId } from "../id.js";
|
|
4
|
+
import { labelTable } from "../schema.js";
|
|
5
|
+
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
6
|
+
import { LabelRepo } from "./label.js";
|
|
7
|
+
|
|
8
|
+
let close: (() => Promise<void>) | undefined;
|
|
9
|
+
|
|
10
|
+
labelRepositoryConformance({
|
|
11
|
+
async createRepository() {
|
|
12
|
+
const { db, close: closeDb } = await createSqliteTestDb({
|
|
13
|
+
labels: labelTable,
|
|
14
|
+
});
|
|
15
|
+
close = closeDb;
|
|
16
|
+
return new LabelRepo(db as never);
|
|
17
|
+
},
|
|
18
|
+
teardown: async () => {
|
|
19
|
+
await close?.();
|
|
20
|
+
},
|
|
21
|
+
makeId: () => randomId(),
|
|
22
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
23
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { labelRepositoryConformance } from "@remit/data-ports/conformance";
|
|
2
|
+
import { NotFoundError } from "../error.js";
|
|
3
|
+
import { createTestDb, randomId } from "../test-db.js";
|
|
4
|
+
import { LabelRepo } from "./label.js";
|
|
5
|
+
|
|
6
|
+
let close: (() => Promise<void>) | undefined;
|
|
7
|
+
|
|
8
|
+
labelRepositoryConformance({
|
|
9
|
+
async createRepository() {
|
|
10
|
+
const { db, close: closeDb } = await createTestDb();
|
|
11
|
+
close = closeDb;
|
|
12
|
+
return new LabelRepo(db as never);
|
|
13
|
+
},
|
|
14
|
+
teardown: async () => {
|
|
15
|
+
await close?.();
|
|
16
|
+
},
|
|
17
|
+
makeId: () => randomId(),
|
|
18
|
+
isNotFoundError: (error) => error instanceof NotFoundError,
|
|
19
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateLabelInput,
|
|
3
|
+
ILabelRepository,
|
|
4
|
+
LabelItem,
|
|
5
|
+
UpdateLabelInput,
|
|
6
|
+
} from "@remit/data-ports";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
8
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
9
|
+
import { NotFoundError } from "../error.js";
|
|
10
|
+
import { randomId } from "../id.js";
|
|
11
|
+
import { labelTable } from "../schema.js";
|
|
12
|
+
|
|
13
|
+
type DB = NodePgDatabase<Record<string, unknown>>;
|
|
14
|
+
|
|
15
|
+
const normalize = (name: string): string => name.trim().toLowerCase();
|
|
16
|
+
|
|
17
|
+
function rowToLabel(row: typeof labelTable.$inferSelect): LabelItem {
|
|
18
|
+
return {
|
|
19
|
+
labelId: row.labelId,
|
|
20
|
+
accountConfigId: row.accountConfigId,
|
|
21
|
+
name: row.name,
|
|
22
|
+
normalizedName: row.normalizedName,
|
|
23
|
+
color: row.color,
|
|
24
|
+
createdAt: row.createdAt,
|
|
25
|
+
updatedAt: row.updatedAt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class LabelRepo implements ILabelRepository {
|
|
30
|
+
constructor(private db: DB) {}
|
|
31
|
+
|
|
32
|
+
async create(input: CreateLabelInput): Promise<LabelItem> {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const [row] = await this.db
|
|
35
|
+
.insert(labelTable)
|
|
36
|
+
.values({
|
|
37
|
+
labelId: randomId(),
|
|
38
|
+
accountConfigId: input.accountConfigId,
|
|
39
|
+
name: input.name,
|
|
40
|
+
normalizedName: normalize(input.name),
|
|
41
|
+
color: input.color ?? "Default",
|
|
42
|
+
createdAt: now,
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
})
|
|
45
|
+
.returning();
|
|
46
|
+
return rowToLabel(row);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async get(accountConfigId: string, labelId: string): Promise<LabelItem> {
|
|
50
|
+
const [row] = await this.db
|
|
51
|
+
.select()
|
|
52
|
+
.from(labelTable)
|
|
53
|
+
.where(
|
|
54
|
+
and(
|
|
55
|
+
eq(labelTable.accountConfigId, accountConfigId),
|
|
56
|
+
eq(labelTable.labelId, labelId),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
if (!row) {
|
|
60
|
+
throw new NotFoundError(`Label not found: ${labelId}`);
|
|
61
|
+
}
|
|
62
|
+
return rowToLabel(row);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async update(
|
|
66
|
+
accountConfigId: string,
|
|
67
|
+
labelId: string,
|
|
68
|
+
input: UpdateLabelInput,
|
|
69
|
+
): Promise<LabelItem> {
|
|
70
|
+
const patch =
|
|
71
|
+
input.name !== undefined
|
|
72
|
+
? { ...input, normalizedName: normalize(input.name) }
|
|
73
|
+
: input;
|
|
74
|
+
const [row] = await this.db
|
|
75
|
+
.update(labelTable)
|
|
76
|
+
.set({ ...patch, updatedAt: Date.now() })
|
|
77
|
+
.where(
|
|
78
|
+
and(
|
|
79
|
+
eq(labelTable.accountConfigId, accountConfigId),
|
|
80
|
+
eq(labelTable.labelId, labelId),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
.returning();
|
|
84
|
+
if (!row) {
|
|
85
|
+
throw new NotFoundError(`Label not found: ${labelId}`);
|
|
86
|
+
}
|
|
87
|
+
return rowToLabel(row);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async delete(accountConfigId: string, labelId: string): Promise<void> {
|
|
91
|
+
await this.db
|
|
92
|
+
.delete(labelTable)
|
|
93
|
+
.where(
|
|
94
|
+
and(
|
|
95
|
+
eq(labelTable.accountConfigId, accountConfigId),
|
|
96
|
+
eq(labelTable.labelId, labelId),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async listByAccountConfig(accountConfigId: string): Promise<LabelItem[]> {
|
|
102
|
+
const rows = await this.db
|
|
103
|
+
.select()
|
|
104
|
+
.from(labelTable)
|
|
105
|
+
.where(eq(labelTable.accountConfigId, accountConfigId))
|
|
106
|
+
.orderBy(labelTable.createdAt);
|
|
107
|
+
return rows.map(rowToLabel);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async findByNormalizedName(
|
|
111
|
+
accountConfigId: string,
|
|
112
|
+
normalizedName: string,
|
|
113
|
+
): Promise<LabelItem | null> {
|
|
114
|
+
const [row] = await this.db
|
|
115
|
+
.select()
|
|
116
|
+
.from(labelTable)
|
|
117
|
+
.where(
|
|
118
|
+
and(
|
|
119
|
+
eq(labelTable.accountConfigId, accountConfigId),
|
|
120
|
+
eq(labelTable.normalizedName, normalize(normalizedName)),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
return row ? rowToLabel(row) : null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BodyPartContentItem,
|
|
3
|
+
BodyPartItem,
|
|
4
|
+
BodyPartParameterItem,
|
|
5
|
+
BodyPartStorageItem,
|
|
6
|
+
EnvelopeAddressItem,
|
|
7
|
+
EnvelopeItem,
|
|
8
|
+
MessageReferenceItem,
|
|
9
|
+
RawMessageStorageItem,
|
|
10
|
+
} from "@remit/data-ports";
|
|
11
|
+
import type {
|
|
12
|
+
bodyPartContentTable,
|
|
13
|
+
bodyPartParameterTable,
|
|
14
|
+
bodyPartStorageTable,
|
|
15
|
+
bodyPartTable,
|
|
16
|
+
envelopeAddressTable,
|
|
17
|
+
envelopeTable,
|
|
18
|
+
messageReferenceTable,
|
|
19
|
+
rawMessageStorageTable,
|
|
20
|
+
} from "../schema/message-data.js";
|
|
21
|
+
|
|
22
|
+
export function toEnvelopeItem(
|
|
23
|
+
row: typeof envelopeTable.$inferSelect,
|
|
24
|
+
): EnvelopeItem {
|
|
25
|
+
return {
|
|
26
|
+
envelopeId: row.envelopeId,
|
|
27
|
+
messageId: row.messageId,
|
|
28
|
+
dateValue: row.dateValue,
|
|
29
|
+
dateRaw: row.dateRaw,
|
|
30
|
+
createdAt: row.createdAt,
|
|
31
|
+
updatedAt: row.updatedAt,
|
|
32
|
+
...(row.subject !== null ? { subject: row.subject } : {}),
|
|
33
|
+
...(row.messageIdValue !== null
|
|
34
|
+
? { messageIdValue: row.messageIdValue }
|
|
35
|
+
: {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toMessageReferenceItem(
|
|
40
|
+
row: typeof messageReferenceTable.$inferSelect,
|
|
41
|
+
): MessageReferenceItem {
|
|
42
|
+
return {
|
|
43
|
+
messageReferenceId: row.messageReferenceId,
|
|
44
|
+
messageId: row.messageId,
|
|
45
|
+
envelopeId: row.envelopeId,
|
|
46
|
+
messageIdValue: row.messageIdValue,
|
|
47
|
+
referenceType: row.referenceType,
|
|
48
|
+
referenceOrder: row.referenceOrder,
|
|
49
|
+
createdAt: row.createdAt,
|
|
50
|
+
updatedAt: row.updatedAt,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toEnvelopeAddressItem(
|
|
55
|
+
row: typeof envelopeAddressTable.$inferSelect,
|
|
56
|
+
): EnvelopeAddressItem {
|
|
57
|
+
return {
|
|
58
|
+
envelopeAddressId: row.envelopeAddressId,
|
|
59
|
+
messageId: row.messageId,
|
|
60
|
+
addressId: row.addressId,
|
|
61
|
+
normalizedEmail: row.normalizedEmail,
|
|
62
|
+
addressRole: row.addressRole,
|
|
63
|
+
addressOrder: row.addressOrder,
|
|
64
|
+
createdAt: row.createdAt,
|
|
65
|
+
updatedAt: row.updatedAt,
|
|
66
|
+
...(row.displayName !== null ? { displayName: row.displayName } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function toBodyPartItem(
|
|
71
|
+
row: typeof bodyPartTable.$inferSelect,
|
|
72
|
+
): BodyPartItem {
|
|
73
|
+
return {
|
|
74
|
+
bodyPartId: row.bodyPartId,
|
|
75
|
+
messageId: row.messageId,
|
|
76
|
+
partPath: row.partPath,
|
|
77
|
+
mediaType: row.mediaType,
|
|
78
|
+
mediaSubtype: row.mediaSubtype,
|
|
79
|
+
transferEncoding: row.transferEncoding,
|
|
80
|
+
sizeOctets: row.sizeOctets,
|
|
81
|
+
isMultipart: row.isMultipart,
|
|
82
|
+
createdAt: row.createdAt,
|
|
83
|
+
updatedAt: row.updatedAt,
|
|
84
|
+
...(row.parentBodyPartId !== null
|
|
85
|
+
? { parentBodyPartId: row.parentBodyPartId }
|
|
86
|
+
: {}),
|
|
87
|
+
...(row.contentId !== null ? { contentId: row.contentId } : {}),
|
|
88
|
+
...(row.contentDescription !== null
|
|
89
|
+
? { contentDescription: row.contentDescription }
|
|
90
|
+
: {}),
|
|
91
|
+
...(row.lineCount !== null ? { lineCount: row.lineCount } : {}),
|
|
92
|
+
...(row.md5Hash !== null ? { md5Hash: row.md5Hash } : {}),
|
|
93
|
+
...(row.disposition !== null ? { disposition: row.disposition } : {}),
|
|
94
|
+
...(row.dispositionFilename !== null
|
|
95
|
+
? { dispositionFilename: row.dispositionFilename }
|
|
96
|
+
: {}),
|
|
97
|
+
...(row.language !== null ? { language: row.language } : {}),
|
|
98
|
+
...(row.location !== null ? { location: row.location } : {}),
|
|
99
|
+
...(row.multipartSubtype !== null
|
|
100
|
+
? { multipartSubtype: row.multipartSubtype }
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toBodyPartParameterItem(
|
|
106
|
+
row: typeof bodyPartParameterTable.$inferSelect,
|
|
107
|
+
): BodyPartParameterItem {
|
|
108
|
+
return {
|
|
109
|
+
bodyPartParameterId: row.bodyPartParameterId,
|
|
110
|
+
messageId: row.messageId,
|
|
111
|
+
bodyPartId: row.bodyPartId,
|
|
112
|
+
parameterName: row.parameterName,
|
|
113
|
+
parameterValue: row.parameterValue,
|
|
114
|
+
createdAt: row.createdAt,
|
|
115
|
+
updatedAt: row.updatedAt,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function toRawMessageStorageItem(
|
|
120
|
+
row: typeof rawMessageStorageTable.$inferSelect,
|
|
121
|
+
): RawMessageStorageItem {
|
|
122
|
+
return {
|
|
123
|
+
rawStorageId: row.rawStorageId,
|
|
124
|
+
messageId: row.messageId,
|
|
125
|
+
storageType: row.storageType,
|
|
126
|
+
storageLocation: row.storageLocation,
|
|
127
|
+
storageKey: row.storageKey,
|
|
128
|
+
sizeBytes: row.sizeBytes,
|
|
129
|
+
checksumSha256: row.checksumSha256,
|
|
130
|
+
contentEncoding: row.contentEncoding,
|
|
131
|
+
storedAt: row.storedAt,
|
|
132
|
+
createdAt: row.createdAt,
|
|
133
|
+
updatedAt: row.updatedAt,
|
|
134
|
+
...(row.expiresAt !== null ? { expiresAt: row.expiresAt } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function toBodyPartStorageItem(
|
|
139
|
+
row: typeof bodyPartStorageTable.$inferSelect,
|
|
140
|
+
): BodyPartStorageItem {
|
|
141
|
+
return {
|
|
142
|
+
bodyPartStorageId: row.bodyPartStorageId,
|
|
143
|
+
messageId: row.messageId,
|
|
144
|
+
bodyPartId: row.bodyPartId,
|
|
145
|
+
storageType: row.storageType,
|
|
146
|
+
storageLocation: row.storageLocation,
|
|
147
|
+
storageKey: row.storageKey,
|
|
148
|
+
decodedSizeBytes: row.decodedSizeBytes,
|
|
149
|
+
checksumSha256: row.checksumSha256,
|
|
150
|
+
contentEncoding: row.contentEncoding,
|
|
151
|
+
isDeduped: row.isDeduped,
|
|
152
|
+
storedAt: row.storedAt,
|
|
153
|
+
createdAt: row.createdAt,
|
|
154
|
+
updatedAt: row.updatedAt,
|
|
155
|
+
...(row.dedupHash !== null ? { dedupHash: row.dedupHash } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function toBodyPartContentItem(
|
|
160
|
+
row: typeof bodyPartContentTable.$inferSelect,
|
|
161
|
+
): BodyPartContentItem {
|
|
162
|
+
return {
|
|
163
|
+
bodyPartContentId: row.bodyPartContentId,
|
|
164
|
+
messageId: row.messageId,
|
|
165
|
+
bodyPartId: row.bodyPartId,
|
|
166
|
+
content: row.content,
|
|
167
|
+
contentLength: row.contentLength,
|
|
168
|
+
createdAt: row.createdAt,
|
|
169
|
+
updatedAt: row.updatedAt,
|
|
170
|
+
};
|
|
171
|
+
}
|