@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.
|
|
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,
|