@remit/drizzle-service 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Drizzle ORM service parameterized by dialect (Postgres / SQLite)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -212,6 +212,76 @@ describe("DrizzleThreadMessageRepository (sqlite)", () => {
212
212
  );
213
213
  });
214
214
 
215
+ test("listByThread defaults to oldest first, by the date the mail was sent", async () => {
216
+ const acct = "acct-conversation-dates";
217
+ const threadId = "t-conversation-dates";
218
+ const base = Date.now();
219
+ // The folder a message was delivered to decides its internalDate, so a
220
+ // reply synced from Sent can carry an earlier internalDate than the
221
+ // message it answers. sentDate is what the conversation is ordered by.
222
+ const turns = [
223
+ { messageId: "m-question", sentDate: base, internalDate: base + 5000 },
224
+ { messageId: "m-answer", sentDate: base + 1000, internalDate: base },
225
+ ];
226
+ for (const turn of turns) {
227
+ await repo.create(
228
+ makeInput({
229
+ accountConfigId: acct,
230
+ threadId,
231
+ messageId: turn.messageId,
232
+ sentDate: turn.sentDate,
233
+ internalDate: turn.internalDate,
234
+ }),
235
+ );
236
+ }
237
+
238
+ const result = await repo.listByThread(threadId, acct, {
239
+ excludeDeleted: true,
240
+ });
241
+ assert.deepEqual(
242
+ result.items.map((item) => item.messageId),
243
+ ["m-question", "m-answer"],
244
+ "the reply follows the message it answers",
245
+ );
246
+ });
247
+
248
+ test("listByThread paginates in sentDate order without skipping or repeating", async () => {
249
+ const acct = "acct-conversation-pages";
250
+ const threadId = "t-conversation-pages";
251
+ const base = Date.now();
252
+ for (let index = 0; index < 5; index++) {
253
+ await repo.create(
254
+ makeInput({
255
+ accountConfigId: acct,
256
+ threadId,
257
+ messageId: `m-page-${index}`,
258
+ sentDate: base + index * 1000,
259
+ internalDate: base,
260
+ }),
261
+ );
262
+ }
263
+
264
+ const seen: string[] = [];
265
+ let continuationToken: string | undefined;
266
+ do {
267
+ const page = await repo.listByThread(threadId, acct, {
268
+ limit: 2,
269
+ continuationToken,
270
+ excludeDeleted: true,
271
+ });
272
+ seen.push(...page.items.map((item) => item.messageId));
273
+ continuationToken = page.continuationToken;
274
+ } while (continuationToken);
275
+
276
+ assert.deepEqual(seen, [
277
+ "m-page-0",
278
+ "m-page-1",
279
+ "m-page-2",
280
+ "m-page-3",
281
+ "m-page-4",
282
+ ]);
283
+ });
284
+
215
285
  test("listByThread excludes soft-deleted messages but keeps the rest of the conversation", async () => {
216
286
  const acct = "acct-conversation-deleted";
217
287
  const threadId = "t-conversation-deleted";
@@ -56,7 +56,6 @@ export const clampThreadSearchLimit = (limit?: number): number => {
56
56
  // ─── Cursor ──────────────────────────────────────────────────────────────────
57
57
 
58
58
  type DateCursor = { s: number; id: string };
59
- type ThreadCursor = { d: number; id: string };
60
59
  type AccountCursor = { id: string };
61
60
 
62
61
  function encodeDateCursor(sentDate: number, threadMessageId: string): string {
@@ -73,23 +72,6 @@ function decodeDateCursor(token: string): DateCursor | null {
73
72
  }
74
73
  }
75
74
 
76
- function encodeThreadCursor(
77
- internalDate: number,
78
- threadMessageId: string,
79
- ): string {
80
- return Buffer.from(
81
- JSON.stringify({ d: internalDate, id: threadMessageId }),
82
- ).toString("base64");
83
- }
84
-
85
- function decodeThreadCursor(token: string): ThreadCursor | null {
86
- try {
87
- return JSON.parse(Buffer.from(token, "base64").toString()) as ThreadCursor;
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
75
  function encodeAccountCursor(threadMessageId: string): string {
94
76
  return Buffer.from(JSON.stringify({ id: threadMessageId })).toString(
95
77
  "base64",
@@ -563,22 +545,22 @@ export class DrizzleThreadMessageRepository
563
545
  ): Promise<ResultList<ThreadMessageItem>> {
564
546
  const order = options?.order ?? "asc";
565
547
  const cursor = options?.continuationToken
566
- ? decodeThreadCursor(options.continuationToken)
548
+ ? decodeDateCursor(options.continuationToken)
567
549
  : null;
568
550
 
569
551
  const cursorCond = cursor
570
552
  ? order === "asc"
571
553
  ? or(
572
- gt(threadMessageTable.internalDate, cursor.d),
554
+ gt(threadMessageTable.sentDate, cursor.s),
573
555
  and(
574
- eq(threadMessageTable.internalDate, cursor.d),
556
+ eq(threadMessageTable.sentDate, cursor.s),
575
557
  gt(threadMessageTable.threadMessageId, cursor.id),
576
558
  ),
577
559
  )
578
560
  : or(
579
- lt(threadMessageTable.internalDate, cursor.d),
561
+ lt(threadMessageTable.sentDate, cursor.s),
580
562
  and(
581
- eq(threadMessageTable.internalDate, cursor.d),
563
+ eq(threadMessageTable.sentDate, cursor.s),
582
564
  gt(threadMessageTable.threadMessageId, cursor.id),
583
565
  ),
584
566
  )
@@ -599,8 +581,8 @@ export class DrizzleThreadMessageRepository
599
581
  )
600
582
  .orderBy(
601
583
  order === "asc"
602
- ? asc(threadMessageTable.internalDate)
603
- : desc(threadMessageTable.internalDate),
584
+ ? asc(threadMessageTable.sentDate)
585
+ : desc(threadMessageTable.sentDate),
604
586
  asc(threadMessageTable.threadMessageId),
605
587
  )
606
588
  .limit(options?.limit ?? 100);
@@ -612,7 +594,7 @@ export class DrizzleThreadMessageRepository
612
594
  items: rows.map(toItem),
613
595
  continuationToken:
614
596
  hasMore && lastRow
615
- ? encodeThreadCursor(lastRow.internalDate, lastRow.threadMessageId)
597
+ ? encodeDateCursor(lastRow.sentDate, lastRow.threadMessageId)
616
598
  : undefined,
617
599
  };
618
600
  }