@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,613 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AddressFlags,
|
|
3
|
+
AddressItem,
|
|
4
|
+
CreateAddressInput,
|
|
5
|
+
CreateEnvelopeAddressInput,
|
|
6
|
+
EnvelopeAddressItem,
|
|
7
|
+
FlagsMergePatch,
|
|
8
|
+
IAddressRepository,
|
|
9
|
+
ResultList,
|
|
10
|
+
UpdateAddressInput,
|
|
11
|
+
} from "@remit/data-ports";
|
|
12
|
+
import { and, asc, eq, gt, inArray, or, sql } from "drizzle-orm";
|
|
13
|
+
import type { Db } from "../db.js";
|
|
14
|
+
import { NotFoundError } from "../error.js";
|
|
15
|
+
import { envelopeAddressId as deriveEnvelopeAddressId } from "../id.js";
|
|
16
|
+
import { decodeToken, resultList } from "../pagination.js";
|
|
17
|
+
import { addressTable } from "../schema/i4-address.js";
|
|
18
|
+
import { envelopeAddressTable } from "../schema/message-data.js";
|
|
19
|
+
import { shouldPromoteWellknown } from "./i4-address-wellknown.js";
|
|
20
|
+
|
|
21
|
+
type DB = Db<Record<string, unknown>>;
|
|
22
|
+
|
|
23
|
+
export function rowToAddress(
|
|
24
|
+
row: typeof addressTable.$inferSelect,
|
|
25
|
+
): AddressItem {
|
|
26
|
+
return {
|
|
27
|
+
addressId: row.addressId,
|
|
28
|
+
accountConfigId: row.accountConfigId,
|
|
29
|
+
displayName: row.displayName ?? undefined,
|
|
30
|
+
localPart: row.localPart,
|
|
31
|
+
domain: row.domain,
|
|
32
|
+
normalizedEmail: row.normalizedEmail,
|
|
33
|
+
normalizedCompound: row.normalizedCompound,
|
|
34
|
+
flags: (row.flags ?? {}) as AddressItem["flags"],
|
|
35
|
+
inboundCount: row.inboundCount,
|
|
36
|
+
outboundCount: row.outboundCount,
|
|
37
|
+
replyCount: row.replyCount,
|
|
38
|
+
lastInboundAt: row.lastInboundAt,
|
|
39
|
+
lastOutboundAt: row.lastOutboundAt ?? undefined,
|
|
40
|
+
lastReplyAt: row.lastReplyAt,
|
|
41
|
+
createdAt: row.createdAt,
|
|
42
|
+
updatedAt: row.updatedAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rowToEnvelopeAddress(
|
|
47
|
+
row: typeof envelopeAddressTable.$inferSelect,
|
|
48
|
+
): EnvelopeAddressItem {
|
|
49
|
+
return {
|
|
50
|
+
envelopeAddressId: row.envelopeAddressId,
|
|
51
|
+
messageId: row.messageId,
|
|
52
|
+
addressId: row.addressId,
|
|
53
|
+
displayName: row.displayName ?? undefined,
|
|
54
|
+
normalizedEmail: row.normalizedEmail,
|
|
55
|
+
addressRole: row.addressRole as EnvelopeAddressItem["addressRole"],
|
|
56
|
+
addressOrder: row.addressOrder,
|
|
57
|
+
createdAt: row.createdAt,
|
|
58
|
+
updatedAt: row.updatedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const VIP_SUGGESTIONS_DEFAULT_LIMIT = 10;
|
|
63
|
+
|
|
64
|
+
export class AddressRepo implements IAddressRepository {
|
|
65
|
+
constructor(private db: DB) {}
|
|
66
|
+
|
|
67
|
+
async createAddress(input: CreateAddressInput): Promise<AddressItem> {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const [row] = await this.db
|
|
70
|
+
.insert(addressTable)
|
|
71
|
+
.values({
|
|
72
|
+
addressId: input.addressId,
|
|
73
|
+
accountConfigId: input.accountConfigId,
|
|
74
|
+
displayName: input.displayName,
|
|
75
|
+
localPart: input.localPart,
|
|
76
|
+
domain: input.domain,
|
|
77
|
+
normalizedEmail: input.normalizedEmail,
|
|
78
|
+
normalizedCompound: input.normalizedCompound,
|
|
79
|
+
flags: input.flags ?? {},
|
|
80
|
+
inboundCount: input.inboundCount ?? 0,
|
|
81
|
+
outboundCount: input.outboundCount ?? 0,
|
|
82
|
+
replyCount: input.replyCount ?? 0,
|
|
83
|
+
lastInboundAt: input.lastInboundAt ?? 0,
|
|
84
|
+
lastOutboundAt: input.lastOutboundAt,
|
|
85
|
+
lastReplyAt: input.lastReplyAt ?? 0,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
})
|
|
89
|
+
.returning();
|
|
90
|
+
return rowToAddress(row);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async upsertAddress(input: CreateAddressInput): Promise<AddressItem> {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const [row] = await this.db
|
|
96
|
+
.insert(addressTable)
|
|
97
|
+
.values({
|
|
98
|
+
addressId: input.addressId,
|
|
99
|
+
accountConfigId: input.accountConfigId,
|
|
100
|
+
displayName: input.displayName,
|
|
101
|
+
localPart: input.localPart,
|
|
102
|
+
domain: input.domain,
|
|
103
|
+
normalizedEmail: input.normalizedEmail,
|
|
104
|
+
normalizedCompound: input.normalizedCompound,
|
|
105
|
+
flags: input.flags ?? {},
|
|
106
|
+
inboundCount: input.inboundCount ?? 0,
|
|
107
|
+
outboundCount: input.outboundCount ?? 0,
|
|
108
|
+
replyCount: input.replyCount ?? 0,
|
|
109
|
+
lastInboundAt: input.lastInboundAt ?? 0,
|
|
110
|
+
lastOutboundAt: input.lastOutboundAt,
|
|
111
|
+
lastReplyAt: input.lastReplyAt ?? 0,
|
|
112
|
+
createdAt: now,
|
|
113
|
+
updatedAt: now,
|
|
114
|
+
})
|
|
115
|
+
.onConflictDoUpdate({
|
|
116
|
+
target: addressTable.addressId,
|
|
117
|
+
set: {
|
|
118
|
+
displayName: input.displayName ?? sql`${addressTable.displayName}`,
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
.returning();
|
|
123
|
+
return rowToAddress(row);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getAddress(
|
|
127
|
+
accountConfigId: string,
|
|
128
|
+
addressId: string,
|
|
129
|
+
): Promise<AddressItem>;
|
|
130
|
+
async getAddress(
|
|
131
|
+
accountConfigId: string,
|
|
132
|
+
addressIds: string[],
|
|
133
|
+
): Promise<AddressItem[]>;
|
|
134
|
+
async getAddress(
|
|
135
|
+
accountConfigId: string,
|
|
136
|
+
addressId: string | string[],
|
|
137
|
+
): Promise<AddressItem | AddressItem[]> {
|
|
138
|
+
if (Array.isArray(addressId)) {
|
|
139
|
+
if (addressId.length === 0) return [];
|
|
140
|
+
const rows = await this.db
|
|
141
|
+
.select()
|
|
142
|
+
.from(addressTable)
|
|
143
|
+
.where(
|
|
144
|
+
and(
|
|
145
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
146
|
+
inArray(addressTable.addressId, addressId),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
return rows.map(rowToAddress);
|
|
150
|
+
}
|
|
151
|
+
const [row] = await this.db
|
|
152
|
+
.select()
|
|
153
|
+
.from(addressTable)
|
|
154
|
+
.where(
|
|
155
|
+
and(
|
|
156
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
157
|
+
eq(addressTable.addressId, addressId),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
if (!row) throw new NotFoundError(`Address not found: ${addressId}`);
|
|
161
|
+
return rowToAddress(row);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async updateAddress(
|
|
165
|
+
accountConfigId: string,
|
|
166
|
+
addressId: string,
|
|
167
|
+
input: UpdateAddressInput,
|
|
168
|
+
): Promise<AddressItem> {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
const updates: Partial<typeof addressTable.$inferInsert> = {
|
|
171
|
+
updatedAt: now,
|
|
172
|
+
};
|
|
173
|
+
if (input.displayName !== undefined)
|
|
174
|
+
updates.displayName = input.displayName;
|
|
175
|
+
if (input.flags !== undefined) updates.flags = input.flags as never;
|
|
176
|
+
if (input.inboundCount !== undefined)
|
|
177
|
+
updates.inboundCount = input.inboundCount;
|
|
178
|
+
if (input.outboundCount !== undefined)
|
|
179
|
+
updates.outboundCount = input.outboundCount;
|
|
180
|
+
if (input.replyCount !== undefined) updates.replyCount = input.replyCount;
|
|
181
|
+
if (input.lastInboundAt !== undefined)
|
|
182
|
+
updates.lastInboundAt = input.lastInboundAt;
|
|
183
|
+
if (input.lastOutboundAt !== undefined)
|
|
184
|
+
updates.lastOutboundAt = input.lastOutboundAt;
|
|
185
|
+
if (input.lastReplyAt !== undefined)
|
|
186
|
+
updates.lastReplyAt = input.lastReplyAt;
|
|
187
|
+
|
|
188
|
+
const [row] = await this.db
|
|
189
|
+
.update(addressTable)
|
|
190
|
+
.set(updates)
|
|
191
|
+
.where(
|
|
192
|
+
and(
|
|
193
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
194
|
+
eq(addressTable.addressId, addressId),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
.returning();
|
|
198
|
+
if (!row) throw new NotFoundError(`Address not found: ${addressId}`);
|
|
199
|
+
return rowToAddress(row);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async mergeFlags(
|
|
203
|
+
accountConfigId: string,
|
|
204
|
+
addressId: string,
|
|
205
|
+
patch: FlagsMergePatch,
|
|
206
|
+
): Promise<AddressItem> {
|
|
207
|
+
const current = await this.getAddress(accountConfigId, addressId);
|
|
208
|
+
const next: AddressFlags = { ...(current.flags ?? {}) };
|
|
209
|
+
for (const [key, value] of Object.entries(patch) as [
|
|
210
|
+
keyof AddressFlags,
|
|
211
|
+
AddressFlags[keyof AddressFlags] | null | undefined,
|
|
212
|
+
][]) {
|
|
213
|
+
if (value === undefined) continue;
|
|
214
|
+
if (value === null) {
|
|
215
|
+
delete next[key];
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
(next[key] as AddressFlags[keyof AddressFlags]) = value;
|
|
219
|
+
}
|
|
220
|
+
const [row] = await this.db
|
|
221
|
+
.update(addressTable)
|
|
222
|
+
.set({ flags: next as never, updatedAt: Date.now() })
|
|
223
|
+
.where(
|
|
224
|
+
and(
|
|
225
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
226
|
+
eq(addressTable.addressId, addressId),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
.returning();
|
|
230
|
+
if (!row) throw new NotFoundError(`Address not found: ${addressId}`);
|
|
231
|
+
return rowToAddress(row);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async promoteWellknownByUser(
|
|
235
|
+
accountConfigId: string,
|
|
236
|
+
addressId: string,
|
|
237
|
+
now: number,
|
|
238
|
+
): Promise<AddressItem> {
|
|
239
|
+
const current = await this.getAddress(accountConfigId, addressId);
|
|
240
|
+
const next: AddressFlags = {
|
|
241
|
+
...(current.flags ?? {}),
|
|
242
|
+
wellknown: { value: true, setAt: now, setBy: "user-junk-rescue" },
|
|
243
|
+
};
|
|
244
|
+
const [row] = await this.db
|
|
245
|
+
.update(addressTable)
|
|
246
|
+
.set({ flags: next as never, updatedAt: Date.now() })
|
|
247
|
+
.where(
|
|
248
|
+
and(
|
|
249
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
250
|
+
eq(addressTable.addressId, addressId),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
.returning();
|
|
254
|
+
if (!row) throw new NotFoundError(`Address not found: ${addressId}`);
|
|
255
|
+
return rowToAddress(row);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async demoteSenderTrust(
|
|
259
|
+
accountConfigId: string,
|
|
260
|
+
addressId: string,
|
|
261
|
+
_now: number,
|
|
262
|
+
): Promise<AddressItem> {
|
|
263
|
+
const current = await this.getAddress(accountConfigId, addressId);
|
|
264
|
+
const { wellknown: _w, vip: _v, ...rest } = current.flags ?? {};
|
|
265
|
+
const [row] = await this.db
|
|
266
|
+
.update(addressTable)
|
|
267
|
+
.set({
|
|
268
|
+
flags: rest as never,
|
|
269
|
+
inboundCount: 0,
|
|
270
|
+
replyCount: 0,
|
|
271
|
+
updatedAt: Date.now(),
|
|
272
|
+
})
|
|
273
|
+
.where(
|
|
274
|
+
and(
|
|
275
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
276
|
+
eq(addressTable.addressId, addressId),
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
.returning();
|
|
280
|
+
if (!row) throw new NotFoundError(`Address not found: ${addressId}`);
|
|
281
|
+
return rowToAddress(row);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async deleteAddress(
|
|
285
|
+
accountConfigId: string,
|
|
286
|
+
addressId: string,
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
await this.db
|
|
289
|
+
.delete(addressTable)
|
|
290
|
+
.where(
|
|
291
|
+
and(
|
|
292
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
293
|
+
eq(addressTable.addressId, addressId),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async incrementInboundCount(
|
|
299
|
+
accountConfigId: string,
|
|
300
|
+
addressId: string,
|
|
301
|
+
now: number,
|
|
302
|
+
_isBulk?: boolean,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const current = await this.getAddress(accountConfigId, addressId);
|
|
305
|
+
const post = {
|
|
306
|
+
...current,
|
|
307
|
+
inboundCount: (current.inboundCount ?? 0) + 1,
|
|
308
|
+
lastInboundAt: now,
|
|
309
|
+
};
|
|
310
|
+
if (shouldPromoteWellknown(post, now)) {
|
|
311
|
+
const nextFlags: AddressFlags = {
|
|
312
|
+
...(current.flags ?? {}),
|
|
313
|
+
wellknown: { value: true, setAt: now, setBy: "auto-engagement" },
|
|
314
|
+
};
|
|
315
|
+
await this.db
|
|
316
|
+
.update(addressTable)
|
|
317
|
+
.set({
|
|
318
|
+
inboundCount: sql`${addressTable.inboundCount} + 1`,
|
|
319
|
+
lastInboundAt: now,
|
|
320
|
+
flags: nextFlags as never,
|
|
321
|
+
updatedAt: Date.now(),
|
|
322
|
+
})
|
|
323
|
+
.where(
|
|
324
|
+
and(
|
|
325
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
326
|
+
eq(addressTable.addressId, addressId),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
await this.db
|
|
331
|
+
.update(addressTable)
|
|
332
|
+
.set({
|
|
333
|
+
inboundCount: sql`${addressTable.inboundCount} + 1`,
|
|
334
|
+
lastInboundAt: now,
|
|
335
|
+
updatedAt: Date.now(),
|
|
336
|
+
})
|
|
337
|
+
.where(
|
|
338
|
+
and(
|
|
339
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
340
|
+
eq(addressTable.addressId, addressId),
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async incrementOutboundCount(
|
|
347
|
+
accountConfigId: string,
|
|
348
|
+
addressId: string,
|
|
349
|
+
now: number,
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
await this.db
|
|
352
|
+
.update(addressTable)
|
|
353
|
+
.set({
|
|
354
|
+
outboundCount: sql`${addressTable.outboundCount} + 1`,
|
|
355
|
+
lastOutboundAt: now,
|
|
356
|
+
updatedAt: Date.now(),
|
|
357
|
+
})
|
|
358
|
+
.where(
|
|
359
|
+
and(
|
|
360
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
361
|
+
eq(addressTable.addressId, addressId),
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async incrementReplyCount(
|
|
367
|
+
accountConfigId: string,
|
|
368
|
+
addressId: string,
|
|
369
|
+
now: number,
|
|
370
|
+
): Promise<void> {
|
|
371
|
+
const current = await this.getAddress(accountConfigId, addressId);
|
|
372
|
+
const post = {
|
|
373
|
+
...current,
|
|
374
|
+
replyCount: (current.replyCount ?? 0) + 1,
|
|
375
|
+
};
|
|
376
|
+
if (shouldPromoteWellknown(post, now)) {
|
|
377
|
+
const nextFlags: AddressFlags = {
|
|
378
|
+
...(current.flags ?? {}),
|
|
379
|
+
wellknown: { value: true, setAt: now, setBy: "auto-engagement" },
|
|
380
|
+
};
|
|
381
|
+
await this.db
|
|
382
|
+
.update(addressTable)
|
|
383
|
+
.set({
|
|
384
|
+
replyCount: sql`${addressTable.replyCount} + 1`,
|
|
385
|
+
lastReplyAt: now,
|
|
386
|
+
flags: nextFlags as never,
|
|
387
|
+
updatedAt: Date.now(),
|
|
388
|
+
})
|
|
389
|
+
.where(
|
|
390
|
+
and(
|
|
391
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
392
|
+
eq(addressTable.addressId, addressId),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
await this.db
|
|
397
|
+
.update(addressTable)
|
|
398
|
+
.set({
|
|
399
|
+
replyCount: sql`${addressTable.replyCount} + 1`,
|
|
400
|
+
lastReplyAt: now,
|
|
401
|
+
updatedAt: Date.now(),
|
|
402
|
+
})
|
|
403
|
+
.where(
|
|
404
|
+
and(
|
|
405
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
406
|
+
eq(addressTable.addressId, addressId),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async deleteManyAddresses(
|
|
413
|
+
accountConfigId: string,
|
|
414
|
+
addressIds: string[],
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
if (addressIds.length === 0) return;
|
|
417
|
+
await this.db
|
|
418
|
+
.delete(addressTable)
|
|
419
|
+
.where(
|
|
420
|
+
and(
|
|
421
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
422
|
+
inArray(addressTable.addressId, addressIds),
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async listSuggestedVips(input: {
|
|
428
|
+
accountConfigId: string;
|
|
429
|
+
limit?: number;
|
|
430
|
+
}): Promise<AddressItem[]> {
|
|
431
|
+
const { accountConfigId, limit = VIP_SUGGESTIONS_DEFAULT_LIMIT } = input;
|
|
432
|
+
const rows = await this.db
|
|
433
|
+
.select()
|
|
434
|
+
.from(addressTable)
|
|
435
|
+
.where(eq(addressTable.accountConfigId, accountConfigId));
|
|
436
|
+
|
|
437
|
+
const candidates = rows
|
|
438
|
+
.map(rowToAddress)
|
|
439
|
+
.filter(
|
|
440
|
+
(a) =>
|
|
441
|
+
a.flags?.wellknown?.value === true &&
|
|
442
|
+
a.flags?.vip?.value !== true &&
|
|
443
|
+
(a.replyCount ?? 0) >= 1,
|
|
444
|
+
)
|
|
445
|
+
.sort((a, b) => {
|
|
446
|
+
const aScore = (a.replyCount ?? 0) + (a.inboundCount ?? 0);
|
|
447
|
+
const bScore = (b.replyCount ?? 0) + (b.inboundCount ?? 0);
|
|
448
|
+
if (bScore !== aScore) return bScore - aScore;
|
|
449
|
+
return (b.lastInboundAt ?? 0) - (a.lastInboundAt ?? 0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return candidates.slice(0, limit);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async listByAccountConfig(input: {
|
|
456
|
+
accountConfigId: string;
|
|
457
|
+
normalizedCompound?: string;
|
|
458
|
+
cursor?: string;
|
|
459
|
+
limit?: number;
|
|
460
|
+
}): Promise<ResultList<AddressItem>> {
|
|
461
|
+
const { accountConfigId, normalizedCompound, cursor, limit = 100 } = input;
|
|
462
|
+
const decoded = cursor ? decodeToken(cursor) : undefined;
|
|
463
|
+
const after = decoded
|
|
464
|
+
? {
|
|
465
|
+
normalizedCompound: decoded.normalizedCompound as string,
|
|
466
|
+
addressId: decoded.addressId as string,
|
|
467
|
+
}
|
|
468
|
+
: undefined;
|
|
469
|
+
|
|
470
|
+
const rows = await this.db
|
|
471
|
+
.select()
|
|
472
|
+
.from(addressTable)
|
|
473
|
+
.where(
|
|
474
|
+
and(
|
|
475
|
+
eq(addressTable.accountConfigId, accountConfigId),
|
|
476
|
+
normalizedCompound
|
|
477
|
+
? sql`${addressTable.normalizedCompound} LIKE ${`${normalizedCompound}%`}`
|
|
478
|
+
: undefined,
|
|
479
|
+
after
|
|
480
|
+
? or(
|
|
481
|
+
gt(addressTable.normalizedCompound, after.normalizedCompound),
|
|
482
|
+
and(
|
|
483
|
+
eq(addressTable.normalizedCompound, after.normalizedCompound),
|
|
484
|
+
gt(addressTable.addressId, after.addressId),
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
: undefined,
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
.orderBy(
|
|
491
|
+
asc(addressTable.normalizedCompound),
|
|
492
|
+
asc(addressTable.addressId),
|
|
493
|
+
)
|
|
494
|
+
.limit(limit + 1);
|
|
495
|
+
|
|
496
|
+
const hasMore = rows.length > limit;
|
|
497
|
+
const items = rows.slice(0, limit).map(rowToAddress);
|
|
498
|
+
const lastItem = items[items.length - 1];
|
|
499
|
+
return resultList(
|
|
500
|
+
items,
|
|
501
|
+
limit,
|
|
502
|
+
hasMore && lastItem
|
|
503
|
+
? {
|
|
504
|
+
normalizedCompound: lastItem.normalizedCompound,
|
|
505
|
+
addressId: lastItem.addressId,
|
|
506
|
+
}
|
|
507
|
+
: undefined,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async createEnvelopeAddress(
|
|
512
|
+
input: CreateEnvelopeAddressInput,
|
|
513
|
+
): Promise<EnvelopeAddressItem> {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
const envelopeAddressId = deriveEnvelopeAddressId(
|
|
516
|
+
input.messageId,
|
|
517
|
+
input.addressRole,
|
|
518
|
+
input.addressOrder,
|
|
519
|
+
);
|
|
520
|
+
const [row] = await this.db
|
|
521
|
+
.insert(envelopeAddressTable)
|
|
522
|
+
.values({
|
|
523
|
+
envelopeAddressId,
|
|
524
|
+
messageId: input.messageId,
|
|
525
|
+
addressId: input.addressId,
|
|
526
|
+
displayName: input.displayName,
|
|
527
|
+
normalizedEmail: input.normalizedEmail,
|
|
528
|
+
addressRole: input.addressRole,
|
|
529
|
+
addressOrder: input.addressOrder,
|
|
530
|
+
createdAt: now,
|
|
531
|
+
updatedAt: now,
|
|
532
|
+
})
|
|
533
|
+
.returning();
|
|
534
|
+
return rowToEnvelopeAddress(row);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async upsertEnvelopeAddress(
|
|
538
|
+
input: CreateEnvelopeAddressInput,
|
|
539
|
+
): Promise<EnvelopeAddressItem> {
|
|
540
|
+
const now = Date.now();
|
|
541
|
+
const envelopeAddressId = deriveEnvelopeAddressId(
|
|
542
|
+
input.messageId,
|
|
543
|
+
input.addressRole,
|
|
544
|
+
input.addressOrder,
|
|
545
|
+
);
|
|
546
|
+
const [row] = await this.db
|
|
547
|
+
.insert(envelopeAddressTable)
|
|
548
|
+
.values({
|
|
549
|
+
envelopeAddressId,
|
|
550
|
+
messageId: input.messageId,
|
|
551
|
+
addressId: input.addressId,
|
|
552
|
+
displayName: input.displayName,
|
|
553
|
+
normalizedEmail: input.normalizedEmail,
|
|
554
|
+
addressRole: input.addressRole,
|
|
555
|
+
addressOrder: input.addressOrder,
|
|
556
|
+
createdAt: now,
|
|
557
|
+
updatedAt: now,
|
|
558
|
+
})
|
|
559
|
+
.onConflictDoNothing()
|
|
560
|
+
.returning();
|
|
561
|
+
if (!row) {
|
|
562
|
+
return this.getEnvelopeAddress(envelopeAddressId);
|
|
563
|
+
}
|
|
564
|
+
return rowToEnvelopeAddress(row);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async getEnvelopeAddress(
|
|
568
|
+
envelopeAddressId: string,
|
|
569
|
+
): Promise<EnvelopeAddressItem>;
|
|
570
|
+
async getEnvelopeAddress(
|
|
571
|
+
envelopeAddressIds: string[],
|
|
572
|
+
): Promise<EnvelopeAddressItem[]>;
|
|
573
|
+
async getEnvelopeAddress(
|
|
574
|
+
envelopeAddressId: string | string[],
|
|
575
|
+
): Promise<EnvelopeAddressItem | EnvelopeAddressItem[]> {
|
|
576
|
+
if (Array.isArray(envelopeAddressId)) {
|
|
577
|
+
if (envelopeAddressId.length === 0) return [];
|
|
578
|
+
const rows = await this.db
|
|
579
|
+
.select()
|
|
580
|
+
.from(envelopeAddressTable)
|
|
581
|
+
.where(
|
|
582
|
+
inArray(envelopeAddressTable.envelopeAddressId, envelopeAddressId),
|
|
583
|
+
);
|
|
584
|
+
return rows.map(rowToEnvelopeAddress);
|
|
585
|
+
}
|
|
586
|
+
const [row] = await this.db
|
|
587
|
+
.select()
|
|
588
|
+
.from(envelopeAddressTable)
|
|
589
|
+
.where(eq(envelopeAddressTable.envelopeAddressId, envelopeAddressId));
|
|
590
|
+
if (!row)
|
|
591
|
+
throw new NotFoundError(
|
|
592
|
+
`EnvelopeAddress not found: ${envelopeAddressId}`,
|
|
593
|
+
);
|
|
594
|
+
return rowToEnvelopeAddress(row);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async deleteEnvelopeAddress(envelopeAddressId: string): Promise<void> {
|
|
598
|
+
await this.db
|
|
599
|
+
.delete(envelopeAddressTable)
|
|
600
|
+
.where(eq(envelopeAddressTable.envelopeAddressId, envelopeAddressId));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async deleteManyEnvelopeAddresses(
|
|
604
|
+
envelopeAddressIds: string[],
|
|
605
|
+
): Promise<void> {
|
|
606
|
+
if (envelopeAddressIds.length === 0) return;
|
|
607
|
+
await this.db
|
|
608
|
+
.delete(envelopeAddressTable)
|
|
609
|
+
.where(
|
|
610
|
+
inArray(envelopeAddressTable.envelopeAddressId, envelopeAddressIds),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|