@remit/drizzle-service 0.0.5 → 0.0.7

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.5",
3
+ "version": "0.0.7",
4
4
  "description": "Drizzle ORM service parameterized by dialect (Postgres / SQLite)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -21,9 +21,7 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@remit/api-zod-schemas": "*",
24
- "@types/better-sqlite3": "^7.6.13",
25
24
  "@types/pg": "^8.0.0",
26
- "better-sqlite3": "^12.11.1",
27
25
  "drizzle-kit": "^0.31.1",
28
26
  "embedded-postgres": "^18.4.0-beta.17",
29
27
  "tsx": "*",
@@ -38,7 +36,9 @@
38
36
  "short-uuid": "*",
39
37
  "uuid": "*",
40
38
  "@remit/drizzle-pg-schema": "*",
41
- "@remit/drizzle-sqlite-schema": "*"
39
+ "@remit/drizzle-sqlite-schema": "*",
40
+ "better-sqlite3": "^12.11.1",
41
+ "@types/better-sqlite3": "^7.6.13"
42
42
  },
43
43
  "license": "MIT",
44
44
  "publishConfig": {
@@ -195,4 +195,152 @@ describe("DrizzleThreadMessageRepository (sqlite)", () => {
195
195
  "pages do not overlap",
196
196
  );
197
197
  });
198
+ // #44: Flagged was a client-side filter over the newest page of the primary
199
+ // inboxes, so a star outside that window was invisible. listByStarred is the
200
+ // byStarred access pattern that view now reads.
201
+ test("listByStarred returns starred rows from every mailbox", async () => {
202
+ const acct = "acct-starred";
203
+ const base = Date.now();
204
+ await repo.create(
205
+ makeInput({
206
+ accountConfigId: acct,
207
+ mailboxId: "mbx-inbox",
208
+ subject: "starred in inbox",
209
+ hasStars: true,
210
+ sentDate: base,
211
+ internalDate: base,
212
+ }),
213
+ );
214
+ await repo.create(
215
+ makeInput({
216
+ accountConfigId: acct,
217
+ mailboxId: "mbx-archive",
218
+ subject: "starred in archive",
219
+ hasStars: true,
220
+ sentDate: base - 1,
221
+ internalDate: base - 1,
222
+ }),
223
+ );
224
+ await repo.create(
225
+ makeInput({
226
+ accountConfigId: acct,
227
+ mailboxId: "mbx-inbox",
228
+ subject: "not starred",
229
+ hasStars: false,
230
+ sentDate: base - 2,
231
+ internalDate: base - 2,
232
+ }),
233
+ );
234
+
235
+ const result = await repo.listByStarred(acct, { order: "desc" });
236
+ assert.deepEqual(
237
+ result.items.map((i) => i.subject),
238
+ ["starred in inbox", "starred in archive"],
239
+ );
240
+ });
241
+
242
+ // The star colour is presentation only and defaults to the `none` sentinel,
243
+ // so a row starred without an explicit colour must still be returned.
244
+ test("listByStarred returns a starred row whose colour is the none sentinel", async () => {
245
+ const acct = "acct-starred-none";
246
+ const created = await repo.create(
247
+ makeInput({
248
+ accountConfigId: acct,
249
+ subject: "uncoloured star",
250
+ hasStars: true,
251
+ }),
252
+ );
253
+ assert.equal(created.star, "none");
254
+
255
+ const result = await repo.listByStarred(acct);
256
+ assert.deepEqual(
257
+ result.items.map((i) => i.subject),
258
+ ["uncoloured star"],
259
+ );
260
+ });
261
+
262
+ test("listByStarred narrows to the supplied mailbox set", async () => {
263
+ const acct = "acct-starred-scope";
264
+ await repo.create(
265
+ makeInput({
266
+ accountConfigId: acct,
267
+ mailboxId: "mbx-kept",
268
+ subject: "kept",
269
+ hasStars: true,
270
+ }),
271
+ );
272
+ await repo.create(
273
+ makeInput({
274
+ accountConfigId: acct,
275
+ mailboxId: "mbx-muted",
276
+ subject: "muted",
277
+ hasStars: true,
278
+ }),
279
+ );
280
+
281
+ const result = await repo.listByStarred(acct, {
282
+ mailboxIds: new Set(["mbx-kept"]),
283
+ });
284
+ assert.deepEqual(
285
+ result.items.map((i) => i.subject),
286
+ ["kept"],
287
+ );
288
+ });
289
+
290
+ test("listByStarred excludes soft-deleted rows when asked", async () => {
291
+ const acct = "acct-starred-deleted";
292
+ await repo.create(
293
+ makeInput({
294
+ accountConfigId: acct,
295
+ subject: "live star",
296
+ hasStars: true,
297
+ }),
298
+ );
299
+ await repo.create(
300
+ makeInput({
301
+ accountConfigId: acct,
302
+ subject: "trashed star",
303
+ hasStars: true,
304
+ isDeleted: true,
305
+ }),
306
+ );
307
+
308
+ const result = await repo.listByStarred(acct, { excludeDeleted: true });
309
+ assert.deepEqual(
310
+ result.items.map((i) => i.subject),
311
+ ["live star"],
312
+ );
313
+ });
314
+
315
+ test("listByStarred pages without overlap", async () => {
316
+ const acct = "acct-starred-page";
317
+ const base = Date.now();
318
+ for (let i = 0; i < 5; i++) {
319
+ await repo.create(
320
+ makeInput({
321
+ accountConfigId: acct,
322
+ subject: `star ${i}`,
323
+ hasStars: true,
324
+ sentDate: base - i,
325
+ internalDate: base - i,
326
+ }),
327
+ );
328
+ }
329
+
330
+ const first = await repo.listByStarred(acct, { limit: 2, order: "desc" });
331
+ assert.equal(first.items.length, 2);
332
+ assert.ok(first.continuationToken);
333
+
334
+ const second = await repo.listByStarred(acct, {
335
+ limit: 2,
336
+ order: "desc",
337
+ continuationToken: first.continuationToken,
338
+ });
339
+ assert.equal(second.items.length, 2);
340
+ const firstIds = new Set(first.items.map((i) => i.threadMessageId));
341
+ assert.ok(
342
+ second.items.every((i) => !firstIds.has(i.threadMessageId)),
343
+ "pages do not overlap",
344
+ );
345
+ });
198
346
  });
@@ -43,6 +43,9 @@ export const deriveThreadMessageId = (
43
43
 
44
44
  export const THREAD_SEARCH_MAX_LIMIT = 500;
45
45
 
46
+ /** Page size used when a list caller supplies no explicit limit. */
47
+ const DEFAULT_LIST_LIMIT = 100;
48
+
46
49
  export const clampThreadSearchLimit = (limit?: number): number => {
47
50
  if (limit === undefined || !Number.isFinite(limit)) {
48
51
  return THREAD_SEARCH_MAX_LIMIT;
@@ -493,6 +496,61 @@ export class DrizzleThreadMessageRepository
493
496
  };
494
497
  }
495
498
 
499
+ async listByStarred(
500
+ accountConfigId: string,
501
+ options?: {
502
+ order?: "asc" | "desc";
503
+ limit?: number;
504
+ continuationToken?: string;
505
+ mailboxIds?: Set<string>;
506
+ excludeDeleted?: boolean;
507
+ },
508
+ ): Promise<ResultList<ThreadMessageItem>> {
509
+ const order = options?.order ?? "desc";
510
+ // Default before the has-more check, or an absent limit silently truncates
511
+ // at the implicit cap with no continuation token.
512
+ const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
513
+ const cursor = options?.continuationToken
514
+ ? decodeDateCursor(options.continuationToken)
515
+ : null;
516
+
517
+ const mailboxCond = options?.mailboxIds?.size
518
+ ? inArray(threadMessageTable.mailboxId, [...options.mailboxIds])
519
+ : undefined;
520
+
521
+ const rows = await this.db
522
+ .select()
523
+ .from(threadMessageTable)
524
+ .where(
525
+ and(
526
+ eq(threadMessageTable.accountConfigId, accountConfigId),
527
+ eq(threadMessageTable.hasStars, true),
528
+ mailboxCond,
529
+ options?.excludeDeleted
530
+ ? eq(threadMessageTable.isDeleted, false)
531
+ : undefined,
532
+ sentDateCursorCond(order, cursor),
533
+ ),
534
+ )
535
+ .orderBy(
536
+ order === "desc"
537
+ ? desc(threadMessageTable.sentDate)
538
+ : asc(threadMessageTable.sentDate),
539
+ asc(threadMessageTable.threadMessageId),
540
+ )
541
+ .limit(limit);
542
+
543
+ const lastRow = rows[rows.length - 1];
544
+ const hasMore = rows.length === limit;
545
+ return {
546
+ items: rows.map(toItem),
547
+ continuationToken:
548
+ hasMore && lastRow
549
+ ? encodeDateCursor(lastRow.sentDate, lastRow.threadMessageId)
550
+ : undefined,
551
+ };
552
+ }
553
+
496
554
  async listByThread(
497
555
  threadId: string,
498
556
  accountConfigId: string,