@remit/drizzle-service 0.0.99 → 0.0.101
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 +1 -1
- package/src/batch.sqlite.test.ts +78 -0
- package/src/batch.ts +46 -0
- package/src/databases.ts +70 -0
- package/src/db.ts +10 -9
- package/src/generated-enum.test.ts +8 -0
- package/src/generated-id.test.ts +19 -0
- package/src/id.test.ts +0 -8
- package/src/id.ts +0 -6
- package/src/index.ts +16 -6
- package/src/repair/address-display-name.sqlite.test.ts +3 -5
- package/src/repair/address-display-name.ts +8 -3
- package/src/repair/junk-only-address.sqlite.test.ts +8 -36
- package/src/repair/stranded-sent-outbox.sqlite.test.ts +2 -9
- package/src/repos/account.conformance.sqlite.test.ts +1 -1
- package/src/repos/calendar-default.sqlite.test.ts +6 -6
- package/src/repos/calendar-put.sqlite.test.ts +40 -39
- package/src/repos/calendar-store.ts +273 -0
- package/src/repos/cascade-delete.sqlite.test.ts +7 -9
- package/src/repos/cascade-delete.test.ts +6 -13
- package/src/repos/cascade-delete.ts +127 -97
- package/src/repos/envelope.test.ts +1 -17
- package/src/repos/envelope.ts +83 -67
- package/src/repos/filter.test.ts +22 -2
- package/src/repos/filter.ts +1 -5
- package/src/repos/i4-account-config.test.ts +2 -2
- package/src/repos/i4-account-config.ts +5 -2
- package/src/repos/i4-account.test.ts +6 -2
- package/src/repos/i4-account.ts +12 -56
- package/src/repos/i4-address-junk-move.sqlite.test.ts +1 -1
- package/src/repos/i4-address-merge-flags.sqlite.test.ts +1 -2
- package/src/repos/i4-address.ts +58 -90
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +6 -2
- package/src/repos/i4-mailbox-special-use.ts +9 -6
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -14
- package/src/repos/i4-mailbox.test.ts +17 -62
- package/src/repos/i4-mailbox.ts +121 -94
- package/src/repos/i4-organize-job-request.ts +0 -4
- package/src/repos/i4-outbox-attachment.ts +62 -33
- package/src/repos/mappers.ts +0 -16
- package/src/repos/message.sqlite.test.ts +32 -106
- package/src/repos/message.test.ts +6 -167
- package/src/repos/message.ts +216 -228
- package/src/repos/sync-warning.ts +76 -0
- package/src/repos/thread-message-body-muted.sqlite.test.ts +5 -10
- package/src/repos/thread-message-field-terms.sqlite.test.ts +5 -1
- package/src/repos/thread-message.sqlite.test.ts +2 -1
- package/src/repos/thread-message.test.ts +17 -4
- package/src/repos/thread-message.ts +502 -285
- package/src/repos/thread-search-predicates.ts +24 -26
- package/src/schema/message-data.ts +4 -8
- package/src/schema/sync-warning.ts +3 -0
- package/src/schema-sets/app.ts +17 -0
- package/src/schema-sets/mail.ts +32 -0
- package/src/schema-sets/search.ts +5 -0
- package/src/schema-version-accounting.sqlite.test.ts +56 -43
- package/src/schema.ts +3 -4
- package/src/search/thread-message-search.sqlite.test.ts +131 -0
- package/src/search/thread-message-search.ts +271 -0
- package/src/sqlite-client.ts +5 -7
- package/src/sqlite-repositories.ts +75 -0
- package/src/test-db-sqlite.ts +5 -13
- package/src/test-shipped-sqlite-schema.ts +46 -45
- package/src/test-sqlite.ts +35 -11
- package/src/vps-migrations-drift.sqlite.test.ts +81 -15
- package/src/account-granted-scopes-migration.sqlite.test.ts +0 -66
- package/src/account-synced-services-migration.sqlite.test.ts +0 -56
- package/src/mailbox-path-dedupe.sqlite.test.ts +0 -462
- package/src/mailbox-sync-status-backfill.sqlite.test.ts +0 -118
- package/src/outbox-created-cleanup.sqlite.test.ts +0 -83
- package/src/repair/duplicate-mailbox-mail.ts +0 -98
- package/src/repair/thread-message-category-contract.ts +0 -505
- package/src/repair/thread-message-category.sqlite.test.ts +0 -100
- package/src/repair/thread-message-category.ts +0 -415
- package/src/repos/calendar-migrations.sqlite.test.ts +0 -345
- package/src/repos/calendar-unit-of-work.ts +0 -31
- package/src/repos/config-import-atomicity.sqlite.test.ts +0 -255
- package/src/repos/filter-anchor-transaction.test.ts +0 -136
- package/src/repos/filter-anchor-transaction.ts +0 -45
- package/src/repos/filter-anchor.test.ts +0 -131
- package/src/repos/filter-anchor.ts +0 -105
- package/src/repos/i4-mailbox-special-use.structure.test.ts +0 -27
- package/src/repos/message-placement-backfill.sqlite.test.ts +0 -190
- package/src/repos/search-index-shape.test.ts +0 -82
- package/src/repos/search-index-shape.ts +0 -56
- package/src/repos/serialized-writes.sqlite.test.ts +0 -199
- package/src/repos/unit-of-work.sqlite.test.ts +0 -138
- package/src/repos/unit-of-work.test.ts +0 -109
- package/src/repos/unit-of-work.ts +0 -31
- package/src/schema/outbox.ts +0 -46
- package/src/schema-full-sqlite.ts +0 -13
- package/src/tx.sqlite.test.ts +0 -83
- package/src/tx.ts +0 -233
|
@@ -13,20 +13,27 @@ import {
|
|
|
13
13
|
asc,
|
|
14
14
|
desc,
|
|
15
15
|
eq,
|
|
16
|
-
|
|
16
|
+
exists,
|
|
17
17
|
gt,
|
|
18
18
|
inArray,
|
|
19
19
|
lt,
|
|
20
|
+
not,
|
|
20
21
|
or,
|
|
21
22
|
type SQL,
|
|
23
|
+
type SQLWrapper,
|
|
22
24
|
sql,
|
|
23
25
|
} from "drizzle-orm";
|
|
26
|
+
import { runBatch } from "../batch.js";
|
|
24
27
|
import type { Db } from "../db.js";
|
|
25
28
|
import { NotFoundError } from "../error.js";
|
|
26
29
|
import { deterministicBase36Id } from "../id.js";
|
|
27
30
|
import { decodeToken } from "../pagination.js";
|
|
28
31
|
import { addressTable } from "../schema/i4-address.js";
|
|
29
32
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
33
|
+
import type {
|
|
34
|
+
SearchIndex,
|
|
35
|
+
SearchPosition,
|
|
36
|
+
} from "../search/thread-message-search.js";
|
|
30
37
|
import {
|
|
31
38
|
bodyMatch,
|
|
32
39
|
fromMatch,
|
|
@@ -174,51 +181,64 @@ function toItem(row: Row): ThreadMessageItem {
|
|
|
174
181
|
const mutedSender = (): SQL =>
|
|
175
182
|
sql`exists (select 1 from ${addressTable} where ${addressTable.accountConfigId} = ${threadMessageTable.accountConfigId} and ${addressTable.normalizedEmail} = lower(coalesce(${threadMessageTable.fromEmail}, '')) and json_extract(coalesce(nullif(${addressTable.flags}, ''), '{}'), '$.muted.value') = 1)`;
|
|
176
183
|
|
|
184
|
+
// The columns the search index is built from; an update that sets none of them
|
|
185
|
+
// leaves the index as it is.
|
|
186
|
+
const touchesSearchIndex = (input: UpdateThreadMessageInput): boolean =>
|
|
187
|
+
input.mailboxId !== undefined ||
|
|
188
|
+
input.sentDate !== undefined ||
|
|
189
|
+
input.listId !== undefined ||
|
|
190
|
+
input.subject !== undefined ||
|
|
191
|
+
input.fromName !== undefined ||
|
|
192
|
+
input.fromEmail !== undefined ||
|
|
193
|
+
input.snippet !== undefined;
|
|
194
|
+
|
|
177
195
|
const queryTerms = (query: string | undefined): string[] =>
|
|
178
196
|
query ? query.split(/\s+/).filter(Boolean) : [];
|
|
179
197
|
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
// The two halves of a search: the text condition, over the search index, and
|
|
199
|
+
// the conditions over the thread message row itself.
|
|
200
|
+
interface SearchConditions {
|
|
201
|
+
text: SQL | undefined;
|
|
202
|
+
row: SQL[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Translate SearchOptions into conditions: subject/from/query as indexed text
|
|
206
|
+
// predicates, muted as a subquery over the sender's address, the rest as plain
|
|
207
|
+
// column equalities. A multi-word `query` matches rows where every token
|
|
208
|
+
// appears in the subject, the from fields or the body preview (AND across
|
|
209
|
+
// tokens, OR across fields) — the same shape as the DynamoDB model.
|
|
210
|
+
function buildSearchConditions(search: SearchOptions): SearchConditions {
|
|
211
|
+
const text: SQL[] = [];
|
|
212
|
+
if (search.subject) text.push(subjectMatch(search.subject));
|
|
213
|
+
if (search.from) text.push(fromMatch(search.from));
|
|
214
|
+
for (const token of queryTerms(search.query)) {
|
|
215
|
+
text.push(
|
|
216
|
+
sql`(${subjectMatch(token)} or ${fromMatch(token)} or ${bodyMatch(token)})`,
|
|
217
|
+
);
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
const row: SQL[] = [];
|
|
200
221
|
if (search.muted !== undefined) {
|
|
201
|
-
|
|
222
|
+
row.push(search.muted ? mutedSender() : sql`not ${mutedSender()}`);
|
|
202
223
|
}
|
|
203
|
-
|
|
204
224
|
if (search.unread !== undefined) {
|
|
205
|
-
|
|
225
|
+
row.push(eq(threadMessageTable.isRead, !search.unread));
|
|
206
226
|
}
|
|
207
227
|
if (search.starred !== undefined) {
|
|
208
|
-
|
|
228
|
+
row.push(eq(threadMessageTable.hasStars, search.starred));
|
|
209
229
|
}
|
|
210
230
|
if (search.attachments !== undefined) {
|
|
211
|
-
|
|
231
|
+
row.push(eq(threadMessageTable.hasAttachment, search.attachments));
|
|
212
232
|
}
|
|
213
233
|
// An equality over a column on the row, so it sits inside the keyset window
|
|
214
234
|
// rather than filtering what the window returned: a page is a full page of
|
|
215
235
|
// matches however rare the category is. Served by
|
|
216
236
|
// tm_by_mailbox_category_date.
|
|
217
237
|
if (search.category?.length) {
|
|
218
|
-
|
|
238
|
+
row.push(inArray(threadMessageTable.category, search.category));
|
|
219
239
|
}
|
|
220
240
|
|
|
221
|
-
return
|
|
241
|
+
return { text: text.length > 0 ? and(...text) : undefined, row };
|
|
222
242
|
}
|
|
223
243
|
|
|
224
244
|
const fieldTermCondition = (term: ThreadMessageFieldTerm): SQL => {
|
|
@@ -245,6 +265,41 @@ function buildFieldTermCondition(
|
|
|
245
265
|
return operator === "or" ? or(...conditions) : and(...conditions);
|
|
246
266
|
}
|
|
247
267
|
|
|
268
|
+
const CANDIDATES_PER_READ = 500;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Where a count over a text search stops. Without text a count is one
|
|
272
|
+
* aggregate; with text it walks the matches, and a number past this is not
|
|
273
|
+
* worth walking further for.
|
|
274
|
+
*/
|
|
275
|
+
export const TEXT_SEARCH_COUNT_CAP = 1000;
|
|
276
|
+
|
|
277
|
+
// A list of ids bound as one JSON parameter, however long it is.
|
|
278
|
+
const inJson = (column: SQLWrapper, values: readonly string[]): SQL =>
|
|
279
|
+
sql`${column} in (select value from json_each(${JSON.stringify(values)}))`;
|
|
280
|
+
|
|
281
|
+
interface PageQuery {
|
|
282
|
+
accountConfigId: string;
|
|
283
|
+
mailboxIds?: readonly string[];
|
|
284
|
+
text: SQL | undefined;
|
|
285
|
+
rank?: SQL<number>;
|
|
286
|
+
row: Array<SQL | undefined>;
|
|
287
|
+
order: "asc" | "desc";
|
|
288
|
+
after: SearchPosition | null;
|
|
289
|
+
limit: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type RankedRow = Row & { rank: number };
|
|
293
|
+
|
|
294
|
+
const positionOf = (cursor: DateCursor | RankedDateCursor | null) =>
|
|
295
|
+
cursor
|
|
296
|
+
? {
|
|
297
|
+
rank: "r" in cursor ? cursor.r : 0,
|
|
298
|
+
sentDate: cursor.s,
|
|
299
|
+
threadMessageId: cursor.id,
|
|
300
|
+
}
|
|
301
|
+
: null;
|
|
302
|
+
|
|
248
303
|
// Keyset cursor over (sent_date, thread_message_id). `desc` walks newest→oldest,
|
|
249
304
|
// `asc` oldest→newest; the id tiebreak keeps paging stable across equal dates.
|
|
250
305
|
function sentDateCursorCond(
|
|
@@ -265,27 +320,186 @@ function sentDateCursorCond(
|
|
|
265
320
|
);
|
|
266
321
|
}
|
|
267
322
|
|
|
268
|
-
function rankedDateCursorCond(
|
|
269
|
-
order: "asc" | "desc",
|
|
270
|
-
rank: SQL<number>,
|
|
271
|
-
cursor: RankedDateCursor | null,
|
|
272
|
-
): SQL | undefined {
|
|
273
|
-
if (!cursor) return undefined;
|
|
274
|
-
return or(
|
|
275
|
-
lt(rank, cursor.r),
|
|
276
|
-
and(eq(rank, cursor.r), sentDateCursorCond(order, cursor)),
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
323
|
// ─── Repository ──────────────────────────────────────────────────────────────
|
|
281
324
|
|
|
282
325
|
export class DrizzleThreadMessageRepository
|
|
283
326
|
implements IThreadMessageRepository
|
|
284
327
|
{
|
|
285
|
-
|
|
328
|
+
constructor(
|
|
329
|
+
private readonly db: Db<Record<string, unknown>>,
|
|
330
|
+
private readonly search: SearchIndex,
|
|
331
|
+
) {}
|
|
286
332
|
|
|
287
|
-
|
|
288
|
-
|
|
333
|
+
/**
|
|
334
|
+
* One page of rows in date order (ranked first when asked). Without a text
|
|
335
|
+
* condition it is one query over the mail database. With one, the search
|
|
336
|
+
* index finds, scopes and orders the matches and hands over a page of ids at
|
|
337
|
+
* a time; the mail database keeps the ones its own conditions accept, until
|
|
338
|
+
* the page is full or the matches run out.
|
|
339
|
+
*/
|
|
340
|
+
private async page(query: PageQuery): Promise<RankedRow[]> {
|
|
341
|
+
if (!query.text) {
|
|
342
|
+
const rows = await this.db
|
|
343
|
+
.select()
|
|
344
|
+
.from(threadMessageTable)
|
|
345
|
+
.where(
|
|
346
|
+
and(
|
|
347
|
+
eq(threadMessageTable.accountConfigId, query.accountConfigId),
|
|
348
|
+
query.mailboxIds
|
|
349
|
+
? inArray(threadMessageTable.mailboxId, [...query.mailboxIds])
|
|
350
|
+
: undefined,
|
|
351
|
+
...query.row,
|
|
352
|
+
sentDateCursorCond(
|
|
353
|
+
query.order,
|
|
354
|
+
query.after && {
|
|
355
|
+
s: query.after.sentDate,
|
|
356
|
+
id: query.after.threadMessageId,
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
.orderBy(
|
|
362
|
+
query.order === "desc"
|
|
363
|
+
? desc(threadMessageTable.sentDate)
|
|
364
|
+
: asc(threadMessageTable.sentDate),
|
|
365
|
+
asc(threadMessageTable.threadMessageId),
|
|
366
|
+
)
|
|
367
|
+
.limit(query.limit);
|
|
368
|
+
return rows.map((row) => ({ ...row, rank: 0 }));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const page: RankedRow[] = [];
|
|
372
|
+
await this.eachMatch({ ...query, text: query.text }, (rows) => {
|
|
373
|
+
page.push(...rows.slice(0, query.limit - page.length));
|
|
374
|
+
return page.length < query.limit;
|
|
375
|
+
});
|
|
376
|
+
return page;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Walk every match of a text query in order, a page of candidates at a
|
|
381
|
+
* time, handing each page's accepted rows to `visit` until it returns false.
|
|
382
|
+
*/
|
|
383
|
+
/**
|
|
384
|
+
* The mail rows behind one page of candidates, kept to the query's folder
|
|
385
|
+
* scope and row conditions: a candidate the index still holds for a row
|
|
386
|
+
* that has since moved folders, or gone, does not come back.
|
|
387
|
+
*/
|
|
388
|
+
private matchedRows(
|
|
389
|
+
query: Omit<PageQuery, "limit" | "text" | "after" | "order">,
|
|
390
|
+
ids: readonly string[],
|
|
391
|
+
) {
|
|
392
|
+
return and(
|
|
393
|
+
eq(threadMessageTable.accountConfigId, query.accountConfigId),
|
|
394
|
+
inJson(threadMessageTable.threadMessageId, ids),
|
|
395
|
+
query.mailboxIds
|
|
396
|
+
? inJson(threadMessageTable.mailboxId, query.mailboxIds)
|
|
397
|
+
: undefined,
|
|
398
|
+
...query.row,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Walk every match of a text query in order, a page of candidates at a
|
|
404
|
+
* time, handing each page's accepted rows to `visit` until it returns false.
|
|
405
|
+
*/
|
|
406
|
+
private async eachMatch(
|
|
407
|
+
query: Omit<PageQuery, "limit" | "text"> & { text: SQL },
|
|
408
|
+
visit: (rows: RankedRow[]) => boolean,
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
let after = query.after;
|
|
411
|
+
for (;;) {
|
|
412
|
+
const candidates = await this.search.candidates({
|
|
413
|
+
accountConfigId: query.accountConfigId,
|
|
414
|
+
mailboxIds: query.mailboxIds,
|
|
415
|
+
where: query.text,
|
|
416
|
+
rank: query.rank,
|
|
417
|
+
order: query.order,
|
|
418
|
+
after,
|
|
419
|
+
limit: CANDIDATES_PER_READ,
|
|
420
|
+
});
|
|
421
|
+
const last = candidates.at(-1);
|
|
422
|
+
if (!last) return;
|
|
423
|
+
const rows = await this.db
|
|
424
|
+
.select()
|
|
425
|
+
.from(threadMessageTable)
|
|
426
|
+
.where(
|
|
427
|
+
this.matchedRows(
|
|
428
|
+
query,
|
|
429
|
+
candidates.map((candidate) => candidate.threadMessageId),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
const byId = new Map(rows.map((row) => [row.threadMessageId, row]));
|
|
433
|
+
const accepted = candidates.flatMap((candidate) => {
|
|
434
|
+
const row = byId.get(candidate.threadMessageId);
|
|
435
|
+
return row ? [{ ...row, rank: candidate.rank }] : [];
|
|
436
|
+
});
|
|
437
|
+
if (!visit(accepted)) return;
|
|
438
|
+
if (candidates.length < CANDIDATES_PER_READ) return;
|
|
439
|
+
after = last;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* How many rows a search accepts, or how many conversations they belong to.
|
|
445
|
+
* Without a text condition it is one COUNT; with one it walks the matches,
|
|
446
|
+
* reading only their thread ids, and stops at {@link TEXT_SEARCH_COUNT_CAP}.
|
|
447
|
+
*/
|
|
448
|
+
private async countMatches(
|
|
449
|
+
query: Omit<PageQuery, "limit" | "after" | "order">,
|
|
450
|
+
unit: "rows" | "threads",
|
|
451
|
+
): Promise<number> {
|
|
452
|
+
if (!query.text) {
|
|
453
|
+
const [{ count }] = await this.db
|
|
454
|
+
.select({
|
|
455
|
+
count:
|
|
456
|
+
unit === "threads"
|
|
457
|
+
? sql<number>`cast(count(distinct ${threadMessageTable.threadId}) as int)`
|
|
458
|
+
: sql<number>`cast(count(*) as int)`,
|
|
459
|
+
})
|
|
460
|
+
.from(threadMessageTable)
|
|
461
|
+
.where(
|
|
462
|
+
and(
|
|
463
|
+
eq(threadMessageTable.accountConfigId, query.accountConfigId),
|
|
464
|
+
query.mailboxIds
|
|
465
|
+
? inArray(threadMessageTable.mailboxId, [...query.mailboxIds])
|
|
466
|
+
: undefined,
|
|
467
|
+
...query.row,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
return count;
|
|
471
|
+
}
|
|
472
|
+
const threads = new Set<string>();
|
|
473
|
+
let rows = 0;
|
|
474
|
+
let after: SearchPosition | null = null;
|
|
475
|
+
for (;;) {
|
|
476
|
+
const candidates = await this.search.candidates({
|
|
477
|
+
accountConfigId: query.accountConfigId,
|
|
478
|
+
mailboxIds: query.mailboxIds,
|
|
479
|
+
where: query.text,
|
|
480
|
+
order: "desc",
|
|
481
|
+
after,
|
|
482
|
+
limit: CANDIDATES_PER_READ,
|
|
483
|
+
});
|
|
484
|
+
const last = candidates.at(-1);
|
|
485
|
+
if (!last) break;
|
|
486
|
+
const matched = await this.db
|
|
487
|
+
.select({ threadId: threadMessageTable.threadId })
|
|
488
|
+
.from(threadMessageTable)
|
|
489
|
+
.where(
|
|
490
|
+
this.matchedRows(
|
|
491
|
+
query,
|
|
492
|
+
candidates.map((candidate) => candidate.threadMessageId),
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
rows += matched.length;
|
|
496
|
+
for (const row of matched) threads.add(row.threadId);
|
|
497
|
+
const counted = unit === "threads" ? threads.size : rows;
|
|
498
|
+
if (counted >= TEXT_SEARCH_COUNT_CAP) return TEXT_SEARCH_COUNT_CAP;
|
|
499
|
+
if (candidates.length < CANDIDATES_PER_READ) break;
|
|
500
|
+
after = last;
|
|
501
|
+
}
|
|
502
|
+
return unit === "threads" ? threads.size : rows;
|
|
289
503
|
}
|
|
290
504
|
|
|
291
505
|
async close(): Promise<void> {
|
|
@@ -332,7 +546,10 @@ export class DrizzleThreadMessageRepository
|
|
|
332
546
|
.values(row)
|
|
333
547
|
.onConflictDoNothing()
|
|
334
548
|
.returning();
|
|
335
|
-
if (inserted)
|
|
549
|
+
if (inserted) {
|
|
550
|
+
await this.search.sync([threadMessageId]);
|
|
551
|
+
return toItem(inserted);
|
|
552
|
+
}
|
|
336
553
|
|
|
337
554
|
const [existing] = await this.db
|
|
338
555
|
.select()
|
|
@@ -431,6 +648,7 @@ export class DrizzleThreadMessageRepository
|
|
|
431
648
|
)
|
|
432
649
|
.returning();
|
|
433
650
|
if (!row) throw new NotFoundError("ThreadMessage not found");
|
|
651
|
+
if (touchesSearchIndex(input)) await this.search.sync([threadMessageId]);
|
|
434
652
|
return toItem(row);
|
|
435
653
|
}
|
|
436
654
|
|
|
@@ -446,6 +664,7 @@ export class DrizzleThreadMessageRepository
|
|
|
446
664
|
eq(threadMessageTable.threadMessageId, threadMessageId),
|
|
447
665
|
),
|
|
448
666
|
);
|
|
667
|
+
await this.search.sync([threadMessageId]);
|
|
449
668
|
}
|
|
450
669
|
|
|
451
670
|
async deleteMany(
|
|
@@ -462,6 +681,7 @@ export class DrizzleThreadMessageRepository
|
|
|
462
681
|
inArray(threadMessageTable.threadMessageId, ids),
|
|
463
682
|
),
|
|
464
683
|
);
|
|
684
|
+
await this.search.sync(ids);
|
|
465
685
|
}
|
|
466
686
|
|
|
467
687
|
async listByAccount(
|
|
@@ -509,56 +729,31 @@ export class DrizzleThreadMessageRepository
|
|
|
509
729
|
search?: SearchOptions;
|
|
510
730
|
},
|
|
511
731
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
: undefined;
|
|
538
|
-
|
|
539
|
-
const rows = await this.db
|
|
540
|
-
.select()
|
|
541
|
-
.from(threadMessageTable)
|
|
542
|
-
.where(
|
|
543
|
-
and(
|
|
544
|
-
eq(threadMessageTable.accountConfigId, accountConfigId),
|
|
545
|
-
mailboxCond,
|
|
546
|
-
options?.excludeDeleted
|
|
547
|
-
? eq(threadMessageTable.isDeleted, false)
|
|
548
|
-
: undefined,
|
|
549
|
-
...(options?.search ? buildSearchConditions(options.search) : []),
|
|
550
|
-
cursorCond,
|
|
551
|
-
),
|
|
552
|
-
)
|
|
553
|
-
.orderBy(
|
|
554
|
-
order === "desc"
|
|
555
|
-
? desc(threadMessageTable.sentDate)
|
|
556
|
-
: asc(threadMessageTable.sentDate),
|
|
557
|
-
asc(threadMessageTable.threadMessageId),
|
|
558
|
-
)
|
|
559
|
-
.limit(options?.limit ?? 100);
|
|
560
|
-
|
|
561
|
-
const lastRow = rows[rows.length - 1];
|
|
732
|
+
const search = options?.search
|
|
733
|
+
? buildSearchConditions(options.search)
|
|
734
|
+
: { text: undefined, row: [] };
|
|
735
|
+
const rows = await this.page({
|
|
736
|
+
accountConfigId,
|
|
737
|
+
mailboxIds: options?.inboxMailboxIds?.size
|
|
738
|
+
? [...options.inboxMailboxIds]
|
|
739
|
+
: undefined,
|
|
740
|
+
text: search.text,
|
|
741
|
+
row: [
|
|
742
|
+
options?.excludeDeleted
|
|
743
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
744
|
+
: undefined,
|
|
745
|
+
...search.row,
|
|
746
|
+
],
|
|
747
|
+
order: options?.order ?? "desc",
|
|
748
|
+
after: positionOf(
|
|
749
|
+
options?.continuationToken
|
|
750
|
+
? decodeDateCursor(options.continuationToken)
|
|
751
|
+
: null,
|
|
752
|
+
),
|
|
753
|
+
limit: options?.limit ?? 100,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const lastRow = rows.at(-1);
|
|
562
757
|
const hasMore =
|
|
563
758
|
options?.limit !== undefined && rows.length === options.limit;
|
|
564
759
|
return {
|
|
@@ -588,57 +783,40 @@ export class DrizzleThreadMessageRepository
|
|
|
588
783
|
excludeDeleted?: boolean;
|
|
589
784
|
},
|
|
590
785
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
591
|
-
const order = options?.order ?? "desc";
|
|
592
786
|
const limit = clampThreadSearchLimit(options?.limit);
|
|
593
787
|
const cursor = options?.continuationToken
|
|
594
788
|
? decodeSearchCursor(options.continuationToken)
|
|
595
789
|
: null;
|
|
596
790
|
const ranked = cursor?.v !== 1;
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
...buildSearchConditions(search),
|
|
620
|
-
cursor?.v === 1
|
|
621
|
-
? sentDateCursorCond(order, cursor)
|
|
622
|
-
: rankedDateCursorCond(order, senderRank, cursor),
|
|
623
|
-
),
|
|
624
|
-
)
|
|
625
|
-
.orderBy(
|
|
626
|
-
...(ranked ? [desc(senderRank)] : []),
|
|
627
|
-
order === "desc"
|
|
628
|
-
? desc(threadMessageTable.sentDate)
|
|
629
|
-
: asc(threadMessageTable.sentDate),
|
|
630
|
-
asc(threadMessageTable.threadMessageId),
|
|
631
|
-
)
|
|
632
|
-
.limit(limit);
|
|
633
|
-
|
|
634
|
-
const lastRow = rows[rows.length - 1];
|
|
791
|
+
const terms = queryTerms(search.query);
|
|
792
|
+
const conditions = buildSearchConditions(search);
|
|
793
|
+
|
|
794
|
+
const rows = await this.page({
|
|
795
|
+
accountConfigId,
|
|
796
|
+
mailboxIds: options?.mailboxIds?.size
|
|
797
|
+
? [...options.mailboxIds]
|
|
798
|
+
: undefined,
|
|
799
|
+
text: conditions.text,
|
|
800
|
+
rank: ranked && terms.length > 0 ? senderMatchRank(terms) : undefined,
|
|
801
|
+
row: [
|
|
802
|
+
options?.excludeDeleted
|
|
803
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
804
|
+
: undefined,
|
|
805
|
+
...conditions.row,
|
|
806
|
+
],
|
|
807
|
+
order: options?.order ?? "desc",
|
|
808
|
+
after: positionOf(cursor),
|
|
809
|
+
limit,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const lastRow = rows.at(-1);
|
|
635
813
|
return {
|
|
636
814
|
items: rows.map(toItem),
|
|
637
815
|
continuationToken:
|
|
638
816
|
rows.length === limit && lastRow
|
|
639
817
|
? ranked
|
|
640
818
|
? encodeRankedDateCursor(
|
|
641
|
-
lastRow.
|
|
819
|
+
lastRow.rank,
|
|
642
820
|
lastRow.sentDate,
|
|
643
821
|
lastRow.threadMessageId,
|
|
644
822
|
)
|
|
@@ -667,42 +845,33 @@ export class DrizzleThreadMessageRepository
|
|
|
667
845
|
mailboxIds?: readonly string[];
|
|
668
846
|
},
|
|
669
847
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
670
|
-
const order = options?.order ?? "desc";
|
|
671
848
|
const limit = clampThreadSearchLimit(options?.limit);
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
:
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
sentDateCursorCond(order, cursor),
|
|
690
|
-
),
|
|
691
|
-
)
|
|
692
|
-
.orderBy(
|
|
693
|
-
order === "desc"
|
|
694
|
-
? desc(threadMessageTable.sentDate)
|
|
695
|
-
: asc(threadMessageTable.sentDate),
|
|
696
|
-
asc(threadMessageTable.threadMessageId),
|
|
697
|
-
)
|
|
698
|
-
.limit(limit);
|
|
849
|
+
const rows = await this.page({
|
|
850
|
+
accountConfigId,
|
|
851
|
+
mailboxIds: options?.mailboxIds,
|
|
852
|
+
text: buildFieldTermCondition(terms, options?.operator ?? "and"),
|
|
853
|
+
row: [
|
|
854
|
+
options?.excludeDeleted
|
|
855
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
856
|
+
: undefined,
|
|
857
|
+
],
|
|
858
|
+
order: options?.order ?? "desc",
|
|
859
|
+
after: positionOf(
|
|
860
|
+
options?.continuationToken
|
|
861
|
+
? decodeDateCursor(options.continuationToken)
|
|
862
|
+
: null,
|
|
863
|
+
),
|
|
864
|
+
limit,
|
|
865
|
+
});
|
|
699
866
|
|
|
700
|
-
const lastRow = rows[rows.length - 1];
|
|
701
867
|
return {
|
|
702
868
|
items: rows.map(toItem),
|
|
703
869
|
continuationToken:
|
|
704
|
-
rows.length === limit &&
|
|
705
|
-
? encodeDateCursor(
|
|
870
|
+
rows.length === limit && rows.at(-1)
|
|
871
|
+
? encodeDateCursor(
|
|
872
|
+
rows[rows.length - 1].sentDate,
|
|
873
|
+
rows[rows.length - 1].threadMessageId,
|
|
874
|
+
)
|
|
706
875
|
: undefined,
|
|
707
876
|
};
|
|
708
877
|
}
|
|
@@ -729,26 +898,23 @@ export class DrizzleThreadMessageRepository
|
|
|
729
898
|
excludeDeleted?: boolean;
|
|
730
899
|
},
|
|
731
900
|
): Promise<number> {
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
.where(
|
|
742
|
-
and(
|
|
743
|
-
eq(threadMessageTable.accountConfigId, accountConfigId),
|
|
744
|
-
mailboxCond,
|
|
901
|
+
const conditions = buildSearchConditions(search);
|
|
902
|
+
return this.countMatches(
|
|
903
|
+
{
|
|
904
|
+
accountConfigId,
|
|
905
|
+
mailboxIds: options?.mailboxIds?.size
|
|
906
|
+
? [...options.mailboxIds]
|
|
907
|
+
: undefined,
|
|
908
|
+
text: conditions.text,
|
|
909
|
+
row: [
|
|
745
910
|
options?.excludeDeleted
|
|
746
911
|
? eq(threadMessageTable.isDeleted, false)
|
|
747
912
|
: undefined,
|
|
748
|
-
...
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
913
|
+
...conditions.row,
|
|
914
|
+
],
|
|
915
|
+
},
|
|
916
|
+
"threads",
|
|
917
|
+
);
|
|
752
918
|
}
|
|
753
919
|
|
|
754
920
|
async listByStarred(
|
|
@@ -762,48 +928,42 @@ export class DrizzleThreadMessageRepository
|
|
|
762
928
|
search?: SearchOptions;
|
|
763
929
|
},
|
|
764
930
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
765
|
-
const order = options?.order ?? "desc";
|
|
766
931
|
// Default before the has-more check, or an absent limit silently truncates
|
|
767
932
|
// at the implicit cap with no continuation token.
|
|
768
933
|
const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
|
|
769
|
-
const
|
|
770
|
-
?
|
|
771
|
-
:
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
order === "desc"
|
|
794
|
-
? desc(threadMessageTable.sentDate)
|
|
795
|
-
: asc(threadMessageTable.sentDate),
|
|
796
|
-
asc(threadMessageTable.threadMessageId),
|
|
797
|
-
)
|
|
798
|
-
.limit(limit);
|
|
934
|
+
const search = options?.search
|
|
935
|
+
? buildSearchConditions(options.search)
|
|
936
|
+
: { text: undefined, row: [] };
|
|
937
|
+
const rows = await this.page({
|
|
938
|
+
accountConfigId,
|
|
939
|
+
mailboxIds: options?.mailboxIds?.size
|
|
940
|
+
? [...options.mailboxIds]
|
|
941
|
+
: undefined,
|
|
942
|
+
text: search.text,
|
|
943
|
+
row: [
|
|
944
|
+
eq(threadMessageTable.hasStars, true),
|
|
945
|
+
options?.excludeDeleted
|
|
946
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
947
|
+
: undefined,
|
|
948
|
+
...search.row,
|
|
949
|
+
],
|
|
950
|
+
order: options?.order ?? "desc",
|
|
951
|
+
after: positionOf(
|
|
952
|
+
options?.continuationToken
|
|
953
|
+
? decodeDateCursor(options.continuationToken)
|
|
954
|
+
: null,
|
|
955
|
+
),
|
|
956
|
+
limit,
|
|
957
|
+
});
|
|
799
958
|
|
|
800
|
-
const lastRow = rows[rows.length - 1];
|
|
801
|
-
const hasMore = rows.length === limit;
|
|
802
959
|
return {
|
|
803
960
|
items: rows.map(toItem),
|
|
804
961
|
continuationToken:
|
|
805
|
-
|
|
806
|
-
? encodeDateCursor(
|
|
962
|
+
rows.length === limit && rows.at(-1)
|
|
963
|
+
? encodeDateCursor(
|
|
964
|
+
rows[rows.length - 1].sentDate,
|
|
965
|
+
rows[rows.length - 1].threadMessageId,
|
|
966
|
+
)
|
|
807
967
|
: undefined,
|
|
808
968
|
};
|
|
809
969
|
}
|
|
@@ -874,6 +1034,71 @@ export class DrizzleThreadMessageRepository
|
|
|
874
1034
|
};
|
|
875
1035
|
}
|
|
876
1036
|
|
|
1037
|
+
async rekeyThread(
|
|
1038
|
+
accountConfigId: string,
|
|
1039
|
+
fromThreadId: string,
|
|
1040
|
+
toThreadId: string,
|
|
1041
|
+
): Promise<{ moved: number; dropped: number }> {
|
|
1042
|
+
if (fromThreadId === toThreadId) return { moved: 0, dropped: 0 };
|
|
1043
|
+
const rows = await this.db
|
|
1044
|
+
.select({
|
|
1045
|
+
threadMessageId: threadMessageTable.threadMessageId,
|
|
1046
|
+
messageId: threadMessageTable.messageId,
|
|
1047
|
+
})
|
|
1048
|
+
.from(threadMessageTable)
|
|
1049
|
+
.where(
|
|
1050
|
+
and(
|
|
1051
|
+
eq(threadMessageTable.accountConfigId, accountConfigId),
|
|
1052
|
+
eq(threadMessageTable.threadId, fromThreadId),
|
|
1053
|
+
),
|
|
1054
|
+
);
|
|
1055
|
+
if (rows.length === 0) return { moved: 0, dropped: 0 };
|
|
1056
|
+
|
|
1057
|
+
// Per row: drop it when the target row already exists, otherwise move it
|
|
1058
|
+
// onto the target id. The delete runs first, so the two statements never
|
|
1059
|
+
// both apply to one row, and a rerun finds only rows a failed run left.
|
|
1060
|
+
const items = rows.flatMap((row) => {
|
|
1061
|
+
const target = deriveThreadMessageId(toThreadId, row.messageId);
|
|
1062
|
+
const targetStored = exists(
|
|
1063
|
+
this.db
|
|
1064
|
+
.select({ threadMessageId: threadMessageTable.threadMessageId })
|
|
1065
|
+
.from(threadMessageTable)
|
|
1066
|
+
.where(eq(threadMessageTable.threadMessageId, target)),
|
|
1067
|
+
);
|
|
1068
|
+
const source = and(
|
|
1069
|
+
eq(threadMessageTable.threadMessageId, row.threadMessageId),
|
|
1070
|
+
eq(threadMessageTable.threadId, fromThreadId),
|
|
1071
|
+
);
|
|
1072
|
+
return [
|
|
1073
|
+
this.db
|
|
1074
|
+
.delete(threadMessageTable)
|
|
1075
|
+
.where(and(source, targetStored))
|
|
1076
|
+
.returning({ threadMessageId: threadMessageTable.threadMessageId }),
|
|
1077
|
+
this.db
|
|
1078
|
+
.update(threadMessageTable)
|
|
1079
|
+
.set({ threadId: toThreadId, threadMessageId: target })
|
|
1080
|
+
.where(and(source, not(targetStored)))
|
|
1081
|
+
.returning({ threadMessageId: threadMessageTable.threadMessageId }),
|
|
1082
|
+
];
|
|
1083
|
+
});
|
|
1084
|
+
const results = await runBatch(this.db, items);
|
|
1085
|
+
await this.search.sync(
|
|
1086
|
+
rows.flatMap((row) => [
|
|
1087
|
+
row.threadMessageId,
|
|
1088
|
+
deriveThreadMessageId(toThreadId, row.messageId),
|
|
1089
|
+
]),
|
|
1090
|
+
);
|
|
1091
|
+
const count = (parity: number): number =>
|
|
1092
|
+
results.reduce<number>(
|
|
1093
|
+
(total, result, index) =>
|
|
1094
|
+
index % 2 === parity && Array.isArray(result)
|
|
1095
|
+
? total + result.length
|
|
1096
|
+
: total,
|
|
1097
|
+
0,
|
|
1098
|
+
);
|
|
1099
|
+
return { moved: count(1), dropped: count(0) };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
877
1102
|
async findByMessageId(
|
|
878
1103
|
accountConfigId: string,
|
|
879
1104
|
messageId: string,
|
|
@@ -992,40 +1217,35 @@ export class DrizzleThreadMessageRepository
|
|
|
992
1217
|
excludeDeleted?: boolean;
|
|
993
1218
|
},
|
|
994
1219
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
995
|
-
const order = options?.order ?? "desc";
|
|
996
1220
|
const limit = options?.count ?? 100;
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
order === "desc"
|
|
1017
|
-
? desc(threadMessageTable.sentDate)
|
|
1018
|
-
: asc(threadMessageTable.sentDate),
|
|
1019
|
-
asc(threadMessageTable.threadMessageId),
|
|
1020
|
-
)
|
|
1021
|
-
.limit(limit);
|
|
1221
|
+
const conditions = buildSearchConditions(search);
|
|
1222
|
+
const rows = await this.page({
|
|
1223
|
+
accountConfigId,
|
|
1224
|
+
mailboxIds: [mailboxId],
|
|
1225
|
+
text: conditions.text,
|
|
1226
|
+
row: [
|
|
1227
|
+
options?.excludeDeleted
|
|
1228
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
1229
|
+
: undefined,
|
|
1230
|
+
...conditions.row,
|
|
1231
|
+
],
|
|
1232
|
+
order: options?.order ?? "desc",
|
|
1233
|
+
after: positionOf(
|
|
1234
|
+
options?.continuationToken
|
|
1235
|
+
? decodeDateCursor(options.continuationToken)
|
|
1236
|
+
: null,
|
|
1237
|
+
),
|
|
1238
|
+
limit,
|
|
1239
|
+
});
|
|
1022
1240
|
|
|
1023
|
-
const lastRow = rows[rows.length - 1];
|
|
1024
1241
|
return {
|
|
1025
1242
|
items: rows.map(toItem),
|
|
1026
1243
|
continuationToken:
|
|
1027
|
-
rows.length === limit &&
|
|
1028
|
-
? encodeDateCursor(
|
|
1244
|
+
rows.length === limit && rows.at(-1)
|
|
1245
|
+
? encodeDateCursor(
|
|
1246
|
+
rows[rows.length - 1].sentDate,
|
|
1247
|
+
rows[rows.length - 1].threadMessageId,
|
|
1248
|
+
)
|
|
1029
1249
|
: undefined,
|
|
1030
1250
|
};
|
|
1031
1251
|
}
|
|
@@ -1048,45 +1268,40 @@ export class DrizzleThreadMessageRepository
|
|
|
1048
1268
|
options?: {
|
|
1049
1269
|
order?: "asc" | "desc";
|
|
1050
1270
|
limit?: number;
|
|
1051
|
-
continuationToken?: string;
|
|
1052
1271
|
attributes?: string[];
|
|
1272
|
+
continuationToken?: string;
|
|
1053
1273
|
excludeDeleted?: boolean;
|
|
1054
1274
|
},
|
|
1055
1275
|
): Promise<ResultList<ThreadMessageItem>> {
|
|
1056
|
-
const order = options?.order ?? "desc";
|
|
1057
1276
|
const limit = clampThreadSearchLimit(options?.limit);
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
order === "desc"
|
|
1078
|
-
? desc(threadMessageTable.sentDate)
|
|
1079
|
-
: asc(threadMessageTable.sentDate),
|
|
1080
|
-
asc(threadMessageTable.threadMessageId),
|
|
1081
|
-
)
|
|
1082
|
-
.limit(limit);
|
|
1277
|
+
const conditions = buildSearchConditions(search);
|
|
1278
|
+
const rows = await this.page({
|
|
1279
|
+
accountConfigId,
|
|
1280
|
+
mailboxIds: [mailboxId],
|
|
1281
|
+
text: conditions.text,
|
|
1282
|
+
row: [
|
|
1283
|
+
options?.excludeDeleted
|
|
1284
|
+
? eq(threadMessageTable.isDeleted, false)
|
|
1285
|
+
: undefined,
|
|
1286
|
+
...conditions.row,
|
|
1287
|
+
],
|
|
1288
|
+
order: options?.order ?? "desc",
|
|
1289
|
+
after: positionOf(
|
|
1290
|
+
options?.continuationToken
|
|
1291
|
+
? decodeDateCursor(options.continuationToken)
|
|
1292
|
+
: null,
|
|
1293
|
+
),
|
|
1294
|
+
limit,
|
|
1295
|
+
});
|
|
1083
1296
|
|
|
1084
|
-
const lastRow = rows[rows.length - 1];
|
|
1085
1297
|
return {
|
|
1086
1298
|
items: rows.map(toItem),
|
|
1087
1299
|
continuationToken:
|
|
1088
|
-
rows.length === limit &&
|
|
1089
|
-
? encodeDateCursor(
|
|
1300
|
+
rows.length === limit && rows.at(-1)
|
|
1301
|
+
? encodeDateCursor(
|
|
1302
|
+
rows[rows.length - 1].sentDate,
|
|
1303
|
+
rows[rows.length - 1].threadMessageId,
|
|
1304
|
+
)
|
|
1090
1305
|
: undefined,
|
|
1091
1306
|
};
|
|
1092
1307
|
}
|
|
@@ -1105,20 +1320,21 @@ export class DrizzleThreadMessageRepository
|
|
|
1105
1320
|
order?: "asc" | "desc";
|
|
1106
1321
|
},
|
|
1107
1322
|
): Promise<number> {
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1323
|
+
const conditions = buildSearchConditions(search);
|
|
1324
|
+
return this.countMatches(
|
|
1325
|
+
{
|
|
1326
|
+
accountConfigId,
|
|
1327
|
+
mailboxIds: [mailboxId],
|
|
1328
|
+
text: conditions.text,
|
|
1329
|
+
row: [
|
|
1115
1330
|
options?.excludeDeleted
|
|
1116
1331
|
? eq(threadMessageTable.isDeleted, false)
|
|
1117
1332
|
: undefined,
|
|
1118
|
-
...
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1333
|
+
...conditions.row,
|
|
1334
|
+
],
|
|
1335
|
+
},
|
|
1336
|
+
"rows",
|
|
1337
|
+
);
|
|
1122
1338
|
}
|
|
1123
1339
|
|
|
1124
1340
|
async listAllByAccount(
|
|
@@ -1136,6 +1352,7 @@ export class DrizzleThreadMessageRepository
|
|
|
1136
1352
|
.delete(threadMessageTable)
|
|
1137
1353
|
.where(eq(threadMessageTable.accountConfigId, accountConfigId))
|
|
1138
1354
|
.returning({ id: threadMessageTable.threadMessageId });
|
|
1355
|
+
await this.search.removeAccount(accountConfigId);
|
|
1139
1356
|
return result.length;
|
|
1140
1357
|
}
|
|
1141
1358
|
}
|