@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,380 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountDescription,
|
|
3
|
+
AccountItem,
|
|
4
|
+
AccountSchedulerPage,
|
|
5
|
+
CreateAccountInput,
|
|
6
|
+
IAccountRepository,
|
|
7
|
+
ResultList,
|
|
8
|
+
UpdateAccountInput,
|
|
9
|
+
} from "@remit/data-ports";
|
|
10
|
+
import { SyncPhase } from "@remit/domain-enums";
|
|
11
|
+
import { and, asc, eq, gt, inArray, or, sql } from "drizzle-orm";
|
|
12
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
13
|
+
import { NotFoundError } from "../error.js";
|
|
14
|
+
import { randomId } from "../id.js";
|
|
15
|
+
import { decodeToken, encodeToken, resultList } from "../pagination.js";
|
|
16
|
+
import { accountTable } from "../schema/i4-account-config.js";
|
|
17
|
+
import { mailboxTable } from "../schema/i4-mailbox.js";
|
|
18
|
+
import { rowToMailbox } from "./i4-mailbox.js";
|
|
19
|
+
|
|
20
|
+
type DB = NodePgDatabase<Record<string, unknown>>;
|
|
21
|
+
|
|
22
|
+
export function rowToAccount(
|
|
23
|
+
row: typeof accountTable.$inferSelect,
|
|
24
|
+
): AccountItem {
|
|
25
|
+
return {
|
|
26
|
+
accountId: row.accountId,
|
|
27
|
+
accountConfigId: row.accountConfigId,
|
|
28
|
+
username: row.username,
|
|
29
|
+
email: row.email,
|
|
30
|
+
authType: row.authType as AccountItem["authType"],
|
|
31
|
+
passwordHash: row.passwordHash ?? undefined,
|
|
32
|
+
oauthRefreshTokenHash: row.oauthRefreshTokenHash ?? undefined,
|
|
33
|
+
oauthTokenUpdatedAt: row.oauthTokenUpdatedAt ?? undefined,
|
|
34
|
+
imapHost: row.imapHost,
|
|
35
|
+
imapPort: row.imapPort,
|
|
36
|
+
imapTls: row.imapTls,
|
|
37
|
+
imapStartTls: row.imapStartTls,
|
|
38
|
+
smtpEnabled: row.smtpEnabled,
|
|
39
|
+
smtpHost: row.smtpHost,
|
|
40
|
+
smtpPort: row.smtpPort,
|
|
41
|
+
smtpTls: row.smtpTls,
|
|
42
|
+
smtpStartTls: row.smtpStartTls,
|
|
43
|
+
smtpUsername: row.smtpUsername,
|
|
44
|
+
smtpPasswordHash: row.smtpPasswordHash ?? undefined,
|
|
45
|
+
isActive: row.isActive,
|
|
46
|
+
connectionState: row.connectionState as AccountItem["connectionState"],
|
|
47
|
+
lastConnectedAt: row.lastConnectedAt ?? undefined,
|
|
48
|
+
lastSyncAt: row.lastSyncAt ?? undefined,
|
|
49
|
+
lastError: row.lastError ?? undefined,
|
|
50
|
+
syncPhase: (row.syncPhase as AccountItem["syncPhase"]) ?? undefined,
|
|
51
|
+
mailboxCountTotal: row.mailboxCountTotal ?? undefined,
|
|
52
|
+
mailboxCountSynced: row.mailboxCountSynced ?? undefined,
|
|
53
|
+
createdAt: row.createdAt,
|
|
54
|
+
updatedAt: row.updatedAt,
|
|
55
|
+
deletedAt: row.deletedAt ?? undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class AccountRepo implements IAccountRepository {
|
|
60
|
+
constructor(private db: DB) {}
|
|
61
|
+
|
|
62
|
+
async create(input: CreateAccountInput): Promise<AccountItem> {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const accountId = input.accountId ?? randomId();
|
|
65
|
+
const [row] = await this.db
|
|
66
|
+
.insert(accountTable)
|
|
67
|
+
.values({
|
|
68
|
+
accountId,
|
|
69
|
+
accountConfigId: input.accountConfigId,
|
|
70
|
+
username: input.username,
|
|
71
|
+
email: input.email,
|
|
72
|
+
authType: input.authType ?? "password",
|
|
73
|
+
passwordHash: input.passwordHash,
|
|
74
|
+
oauthRefreshTokenHash: input.oauthRefreshTokenHash,
|
|
75
|
+
oauthTokenUpdatedAt: input.oauthTokenUpdatedAt,
|
|
76
|
+
imapHost: input.imapHost,
|
|
77
|
+
imapPort: input.imapPort,
|
|
78
|
+
imapTls: input.imapTls,
|
|
79
|
+
imapStartTls: input.imapStartTls,
|
|
80
|
+
smtpEnabled: input.smtpEnabled ?? false,
|
|
81
|
+
smtpHost: input.smtpHost ?? "",
|
|
82
|
+
smtpPort: input.smtpPort ?? 587,
|
|
83
|
+
smtpTls: input.smtpTls ?? false,
|
|
84
|
+
smtpStartTls: input.smtpStartTls ?? false,
|
|
85
|
+
smtpUsername: input.smtpUsername ?? "",
|
|
86
|
+
smtpPasswordHash: input.smtpPasswordHash,
|
|
87
|
+
isActive: input.isActive,
|
|
88
|
+
connectionState: input.connectionState,
|
|
89
|
+
lastConnectedAt: input.lastConnectedAt,
|
|
90
|
+
lastSyncAt: input.lastSyncAt,
|
|
91
|
+
lastError: input.lastError,
|
|
92
|
+
syncPhase: input.syncPhase,
|
|
93
|
+
mailboxCountTotal: input.mailboxCountTotal,
|
|
94
|
+
mailboxCountSynced: input.mailboxCountSynced,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
})
|
|
98
|
+
.returning();
|
|
99
|
+
return rowToAccount(row);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async get(accountId: string): Promise<AccountItem>;
|
|
103
|
+
async get(accountIds: string[]): Promise<AccountItem[]>;
|
|
104
|
+
async get(
|
|
105
|
+
accountId: string | string[],
|
|
106
|
+
): Promise<AccountItem | AccountItem[]> {
|
|
107
|
+
if (Array.isArray(accountId)) {
|
|
108
|
+
if (accountId.length === 0) return [];
|
|
109
|
+
const rows = await this.db
|
|
110
|
+
.select()
|
|
111
|
+
.from(accountTable)
|
|
112
|
+
.where(inArray(accountTable.accountId, accountId));
|
|
113
|
+
return rows.map(rowToAccount);
|
|
114
|
+
}
|
|
115
|
+
const [row] = await this.db
|
|
116
|
+
.select()
|
|
117
|
+
.from(accountTable)
|
|
118
|
+
.where(eq(accountTable.accountId, accountId));
|
|
119
|
+
if (!row) throw new NotFoundError(`Account not found: ${accountId}`);
|
|
120
|
+
return rowToAccount(row);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async update(
|
|
124
|
+
accountId: string,
|
|
125
|
+
input: UpdateAccountInput,
|
|
126
|
+
remove?: (keyof UpdateAccountInput)[],
|
|
127
|
+
): Promise<AccountItem> {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const updates: Partial<typeof accountTable.$inferInsert> = {
|
|
130
|
+
updatedAt: now,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (input.username !== undefined) updates.username = input.username;
|
|
134
|
+
if (input.email !== undefined) updates.email = input.email;
|
|
135
|
+
if (input.authType !== undefined) updates.authType = input.authType;
|
|
136
|
+
if (input.passwordHash !== undefined)
|
|
137
|
+
updates.passwordHash = input.passwordHash;
|
|
138
|
+
if (input.oauthRefreshTokenHash !== undefined)
|
|
139
|
+
updates.oauthRefreshTokenHash = input.oauthRefreshTokenHash;
|
|
140
|
+
if (input.oauthTokenUpdatedAt !== undefined)
|
|
141
|
+
updates.oauthTokenUpdatedAt = input.oauthTokenUpdatedAt;
|
|
142
|
+
if (input.imapHost !== undefined) updates.imapHost = input.imapHost;
|
|
143
|
+
if (input.imapPort !== undefined) updates.imapPort = input.imapPort;
|
|
144
|
+
if (input.imapTls !== undefined) updates.imapTls = input.imapTls;
|
|
145
|
+
if (input.imapStartTls !== undefined)
|
|
146
|
+
updates.imapStartTls = input.imapStartTls;
|
|
147
|
+
if (input.smtpEnabled !== undefined)
|
|
148
|
+
updates.smtpEnabled = input.smtpEnabled;
|
|
149
|
+
if (input.smtpHost !== undefined) updates.smtpHost = input.smtpHost;
|
|
150
|
+
if (input.smtpPort !== undefined) updates.smtpPort = input.smtpPort;
|
|
151
|
+
if (input.smtpTls !== undefined) updates.smtpTls = input.smtpTls;
|
|
152
|
+
if (input.smtpStartTls !== undefined)
|
|
153
|
+
updates.smtpStartTls = input.smtpStartTls;
|
|
154
|
+
if (input.smtpUsername !== undefined)
|
|
155
|
+
updates.smtpUsername = input.smtpUsername;
|
|
156
|
+
if (input.isActive !== undefined) updates.isActive = input.isActive;
|
|
157
|
+
if (input.connectionState !== undefined)
|
|
158
|
+
updates.connectionState = input.connectionState;
|
|
159
|
+
if (input.lastConnectedAt !== undefined)
|
|
160
|
+
updates.lastConnectedAt = input.lastConnectedAt;
|
|
161
|
+
if (input.lastSyncAt !== undefined) updates.lastSyncAt = input.lastSyncAt;
|
|
162
|
+
if (input.lastError !== undefined) updates.lastError = input.lastError;
|
|
163
|
+
if (input.syncPhase !== undefined) updates.syncPhase = input.syncPhase;
|
|
164
|
+
if (input.mailboxCountTotal !== undefined)
|
|
165
|
+
updates.mailboxCountTotal = input.mailboxCountTotal;
|
|
166
|
+
if (input.mailboxCountSynced !== undefined)
|
|
167
|
+
updates.mailboxCountSynced = input.mailboxCountSynced;
|
|
168
|
+
|
|
169
|
+
// Handle field removal (set to null)
|
|
170
|
+
if (remove) {
|
|
171
|
+
for (const field of remove) {
|
|
172
|
+
if (field === "lastError") updates.lastError = null;
|
|
173
|
+
if (field === "lastConnectedAt") updates.lastConnectedAt = null;
|
|
174
|
+
if (field === "lastSyncAt") updates.lastSyncAt = null;
|
|
175
|
+
if (field === "syncPhase") updates.syncPhase = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const [row] = await this.db
|
|
180
|
+
.update(accountTable)
|
|
181
|
+
.set(updates)
|
|
182
|
+
.where(eq(accountTable.accountId, accountId))
|
|
183
|
+
.returning();
|
|
184
|
+
if (!row) throw new NotFoundError(`Account not found: ${accountId}`);
|
|
185
|
+
return rowToAccount(row);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async markAuthenticated(accountId: string): Promise<AccountItem> {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const [row] = await this.db
|
|
191
|
+
.update(accountTable)
|
|
192
|
+
.set({
|
|
193
|
+
connectionState: "authenticated",
|
|
194
|
+
lastConnectedAt: now,
|
|
195
|
+
lastError: null,
|
|
196
|
+
updatedAt: now,
|
|
197
|
+
})
|
|
198
|
+
.where(eq(accountTable.accountId, accountId))
|
|
199
|
+
.returning();
|
|
200
|
+
if (!row) throw new NotFoundError(`Account not found: ${accountId}`);
|
|
201
|
+
return rowToAccount(row);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async delete(accountId: string): Promise<void> {
|
|
205
|
+
await this.db
|
|
206
|
+
.delete(accountTable)
|
|
207
|
+
.where(eq(accountTable.accountId, accountId));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async deleteMany(accountIds: string[]): Promise<void> {
|
|
211
|
+
if (accountIds.length === 0) return;
|
|
212
|
+
await this.db
|
|
213
|
+
.delete(accountTable)
|
|
214
|
+
.where(inArray(accountTable.accountId, accountIds));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async list(
|
|
218
|
+
accountConfigId: string,
|
|
219
|
+
options?: { limit?: number; continuationToken?: string },
|
|
220
|
+
): Promise<ResultList<AccountItem>> {
|
|
221
|
+
const limit = options?.limit ?? 100;
|
|
222
|
+
const cursor = options?.continuationToken
|
|
223
|
+
? decodeToken(options.continuationToken)
|
|
224
|
+
: undefined;
|
|
225
|
+
const after = cursor
|
|
226
|
+
? {
|
|
227
|
+
createdAt: cursor.createdAt as number,
|
|
228
|
+
accountId: cursor.accountId as string,
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
231
|
+
|
|
232
|
+
const rows = await this.db
|
|
233
|
+
.select()
|
|
234
|
+
.from(accountTable)
|
|
235
|
+
.where(
|
|
236
|
+
and(
|
|
237
|
+
eq(accountTable.accountConfigId, accountConfigId),
|
|
238
|
+
after
|
|
239
|
+
? or(
|
|
240
|
+
gt(accountTable.createdAt, after.createdAt),
|
|
241
|
+
and(
|
|
242
|
+
eq(accountTable.createdAt, after.createdAt),
|
|
243
|
+
gt(accountTable.accountId, after.accountId),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
: undefined,
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
.orderBy(asc(accountTable.createdAt), asc(accountTable.accountId))
|
|
250
|
+
.limit(limit + 1);
|
|
251
|
+
|
|
252
|
+
const hasMore = rows.length > limit;
|
|
253
|
+
const items = rows.slice(0, limit).map(rowToAccount);
|
|
254
|
+
const lastItem = items[items.length - 1];
|
|
255
|
+
return resultList(
|
|
256
|
+
items,
|
|
257
|
+
limit,
|
|
258
|
+
hasMore && lastItem
|
|
259
|
+
? { createdAt: lastItem.createdAt, accountId: lastItem.accountId }
|
|
260
|
+
: undefined,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async listAllByAccountConfig(
|
|
265
|
+
accountConfigId: string,
|
|
266
|
+
): Promise<AccountItem[]> {
|
|
267
|
+
const rows = await this.db
|
|
268
|
+
.select()
|
|
269
|
+
.from(accountTable)
|
|
270
|
+
.where(eq(accountTable.accountConfigId, accountConfigId));
|
|
271
|
+
return rows.map(rowToAccount);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async describe(accountId: string): Promise<AccountDescription> {
|
|
275
|
+
const [accounts, mailboxes] = await Promise.all([
|
|
276
|
+
this.db
|
|
277
|
+
.select()
|
|
278
|
+
.from(accountTable)
|
|
279
|
+
.where(eq(accountTable.accountId, accountId)),
|
|
280
|
+
this.db
|
|
281
|
+
.select()
|
|
282
|
+
.from(mailboxTable)
|
|
283
|
+
.where(eq(mailboxTable.accountId, accountId)),
|
|
284
|
+
]);
|
|
285
|
+
if (accounts.length === 0) {
|
|
286
|
+
throw new NotFoundError(`Account not found: ${accountId}`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
account: accounts.map(rowToAccount),
|
|
290
|
+
mailbox: mailboxes.map(rowToMailbox),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async listAll(): Promise<AccountItem[]> {
|
|
295
|
+
const rows = await this.db.select().from(accountTable);
|
|
296
|
+
return rows.map(rowToAccount);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async incrementMailboxSynced(accountId: string): Promise<AccountItem> {
|
|
300
|
+
// Atomic increment via UPDATE ... SET x = x + 1 RETURNING
|
|
301
|
+
const [row] = await this.db
|
|
302
|
+
.update(accountTable)
|
|
303
|
+
.set({
|
|
304
|
+
mailboxCountSynced: sql`COALESCE(${accountTable.mailboxCountSynced}, 0) + 1`,
|
|
305
|
+
updatedAt: Date.now(),
|
|
306
|
+
})
|
|
307
|
+
.where(eq(accountTable.accountId, accountId))
|
|
308
|
+
.returning();
|
|
309
|
+
|
|
310
|
+
if (!row) throw new NotFoundError(`Account not found: ${accountId}`);
|
|
311
|
+
const updated = rowToAccount(row);
|
|
312
|
+
|
|
313
|
+
const total = updated.mailboxCountTotal ?? 0;
|
|
314
|
+
const synced = updated.mailboxCountSynced ?? 0;
|
|
315
|
+
|
|
316
|
+
if (total > 0 && synced >= total) {
|
|
317
|
+
const [clamped] = await this.db
|
|
318
|
+
.update(accountTable)
|
|
319
|
+
.set({
|
|
320
|
+
syncPhase: SyncPhase.complete,
|
|
321
|
+
mailboxCountSynced: total,
|
|
322
|
+
updatedAt: Date.now(),
|
|
323
|
+
})
|
|
324
|
+
.where(eq(accountTable.accountId, accountId))
|
|
325
|
+
.returning();
|
|
326
|
+
return clamped ? rowToAccount(clamped) : updated;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return updated;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Page through every account system-wide, oldest-created first, for the
|
|
334
|
+
* scheduled-sync tick (#1247). Same keyset-pagination shape as `list()`,
|
|
335
|
+
* just without the accountConfigId scope.
|
|
336
|
+
*/
|
|
337
|
+
async listAllAccountsPage(options?: {
|
|
338
|
+
limit?: number;
|
|
339
|
+
cursor?: string;
|
|
340
|
+
}): Promise<AccountSchedulerPage> {
|
|
341
|
+
const limit = options?.limit ?? 100;
|
|
342
|
+
const cursor = options?.cursor ? decodeToken(options.cursor) : undefined;
|
|
343
|
+
const after = cursor
|
|
344
|
+
? {
|
|
345
|
+
createdAt: cursor.createdAt as number,
|
|
346
|
+
accountId: cursor.accountId as string,
|
|
347
|
+
}
|
|
348
|
+
: undefined;
|
|
349
|
+
|
|
350
|
+
const rows = await this.db
|
|
351
|
+
.select()
|
|
352
|
+
.from(accountTable)
|
|
353
|
+
.where(
|
|
354
|
+
after
|
|
355
|
+
? or(
|
|
356
|
+
gt(accountTable.createdAt, after.createdAt),
|
|
357
|
+
and(
|
|
358
|
+
eq(accountTable.createdAt, after.createdAt),
|
|
359
|
+
gt(accountTable.accountId, after.accountId),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
: undefined,
|
|
363
|
+
)
|
|
364
|
+
.orderBy(asc(accountTable.createdAt), asc(accountTable.accountId))
|
|
365
|
+
.limit(limit + 1);
|
|
366
|
+
|
|
367
|
+
const hasMore = rows.length > limit;
|
|
368
|
+
const items = rows.slice(0, limit).map(rowToAccount);
|
|
369
|
+
const lastItem = items[items.length - 1];
|
|
370
|
+
const cursorOut =
|
|
371
|
+
hasMore && lastItem
|
|
372
|
+
? encodeToken({
|
|
373
|
+
createdAt: lastItem.createdAt,
|
|
374
|
+
accountId: lastItem.accountId,
|
|
375
|
+
})
|
|
376
|
+
: null;
|
|
377
|
+
|
|
378
|
+
return { items, cursor: cursorOut };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const WELLKNOWN_INBOUND_THRESHOLD = 3;
|
|
2
|
+
export const WELLKNOWN_REPLY_THRESHOLD = 1;
|
|
3
|
+
export const WELLKNOWN_INBOUND_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
export interface WellknownEngagementSnapshot {
|
|
6
|
+
inboundCount?: number;
|
|
7
|
+
replyCount?: number;
|
|
8
|
+
lastInboundAt?: number;
|
|
9
|
+
isBulk?: boolean;
|
|
10
|
+
flags?: { wellknown?: { value: boolean } | undefined };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const shouldPromoteWellknown = (
|
|
14
|
+
snapshot: WellknownEngagementSnapshot,
|
|
15
|
+
now: number,
|
|
16
|
+
): boolean => {
|
|
17
|
+
if (snapshot.flags?.wellknown?.value === true) return false;
|
|
18
|
+
|
|
19
|
+
const replyCount = snapshot.replyCount ?? 0;
|
|
20
|
+
const inboundCount = snapshot.inboundCount ?? 0;
|
|
21
|
+
const replied = replyCount >= WELLKNOWN_REPLY_THRESHOLD;
|
|
22
|
+
const inboundEnough =
|
|
23
|
+
!snapshot.isBulk && inboundCount >= WELLKNOWN_INBOUND_THRESHOLD;
|
|
24
|
+
if (!replied && !inboundEnough) return false;
|
|
25
|
+
|
|
26
|
+
const lastInboundAt = snapshot.lastInboundAt;
|
|
27
|
+
if (lastInboundAt === undefined) return false;
|
|
28
|
+
if (now - lastInboundAt > WELLKNOWN_INBOUND_WINDOW_MS) return false;
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
};
|