@remit/drizzle-service 0.0.74 → 0.0.76

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.74",
3
+ "version": "0.0.76",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./src/index.ts",
11
11
  "default": "./src/index.ts"
12
+ },
13
+ "./test-sqlite": {
14
+ "types": "./src/test-sqlite.ts",
15
+ "default": "./src/test-sqlite.ts"
12
16
  }
13
17
  },
14
18
  "scripts": {
@@ -19,6 +19,17 @@ import { rowToMailbox } from "./i4-mailbox.js";
19
19
 
20
20
  type DB = Db<Record<string, unknown>>;
21
21
 
22
+ /**
23
+ * A stored failure is a sentence the account card shows, not a transcript.
24
+ * Long enough for what a mail server says when it refuses, short enough that a
25
+ * chatty one — or a stack-carrying `Error.message` from a sync — cannot fill the
26
+ * column. Clamped here so every writer is bounded, whatever it hands over.
27
+ */
28
+ const LAST_ERROR_MAX_LENGTH = 500;
29
+
30
+ const clampLastError = (value: string | undefined): string | undefined =>
31
+ value === undefined ? undefined : value.slice(0, LAST_ERROR_MAX_LENGTH);
32
+
22
33
  export function rowToAccount(
23
34
  row: typeof accountTable.$inferSelect,
24
35
  ): AccountItem {
@@ -88,7 +99,7 @@ export class AccountRepo implements IAccountRepository {
88
99
  connectionState: input.connectionState,
89
100
  lastConnectedAt: input.lastConnectedAt,
90
101
  lastSyncAt: input.lastSyncAt,
91
- lastError: input.lastError,
102
+ lastError: clampLastError(input.lastError),
92
103
  syncPhase: input.syncPhase,
93
104
  mailboxCountTotal: input.mailboxCountTotal,
94
105
  mailboxCountSynced: input.mailboxCountSynced,
@@ -159,7 +170,8 @@ export class AccountRepo implements IAccountRepository {
159
170
  if (input.lastConnectedAt !== undefined)
160
171
  updates.lastConnectedAt = input.lastConnectedAt;
161
172
  if (input.lastSyncAt !== undefined) updates.lastSyncAt = input.lastSyncAt;
162
- if (input.lastError !== undefined) updates.lastError = input.lastError;
173
+ if (input.lastError !== undefined)
174
+ updates.lastError = clampLastError(input.lastError);
163
175
  if (input.syncPhase !== undefined) updates.syncPhase = input.syncPhase;
164
176
  if (input.mailboxCountTotal !== undefined)
165
177
  updates.mailboxCountTotal = input.mailboxCountTotal;
@@ -321,6 +321,57 @@ describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox"
321
321
  );
322
322
  assert.equal(count, 5, "asc counts the whole match set too");
323
323
  });
324
+
325
+ // ── Scenario 7 ────────────────────────────────────────────────────────────
326
+ // The count and the result set are two answers to one predicate, and the
327
+ // surfaces that render the number stopped paging to derive it (#307). This
328
+ // is the guard from the other direction: walk every page and assert the walk
329
+ // and the count agree, so the number cannot silently drift from the rows.
330
+ test("the count agrees with a full page-through of the same predicate", async () => {
331
+ const acct = uuid();
332
+ const mbx = uuid();
333
+ const now = Date.now();
334
+ await seed(acct, mbx, [
335
+ ...Array.from({ length: 17 }, (_, i) => ({
336
+ subject: `gamma ${i}`,
337
+ sentDate: now - i,
338
+ internalDate: now - i,
339
+ })),
340
+ { subject: "delta", sentDate: now - 100, internalDate: now - 100 },
341
+ ]);
342
+
343
+ const walked = new Set<string>();
344
+ let continuationToken: string | undefined;
345
+ let pages = 0;
346
+ do {
347
+ const page = await repo.searchByMailboxWindow(
348
+ acct,
349
+ mbx,
350
+ { subject: "gamma" },
351
+ { excludeDeleted: true, limit: 5, continuationToken },
352
+ );
353
+ for (const row of page.items) walked.add(row.threadMessageId);
354
+ continuationToken = page.continuationToken;
355
+ pages += 1;
356
+ } while (continuationToken && pages < 10);
357
+ assert.ok(
358
+ !continuationToken,
359
+ "the walk never reached the end of the pages",
360
+ );
361
+
362
+ const count = await repo.countByMailbox(
363
+ acct,
364
+ mbx,
365
+ { subject: "gamma" },
366
+ { excludeDeleted: true },
367
+ );
368
+ assert.equal(
369
+ count,
370
+ walked.size,
371
+ "the count and the rows the predicate returns disagree",
372
+ );
373
+ assert.equal(count, 17, "the unmatched row leaked into one of the two");
374
+ });
324
375
  });
325
376
 
326
377
  // ─── searchByDate: the unified listing's cross-folder search mode ─────────────
@@ -0,0 +1,39 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import type { Db } from "./db.js";
4
+ import {
5
+ applyMigration,
6
+ migrationJournal,
7
+ } from "./test-shipped-sqlite-schema.js";
8
+
9
+ /**
10
+ * The store a test runs against: the committed SQLite entity migrations, in the
11
+ * order the migrator runs them, over the same engine a self-host deployment
12
+ * boots. Applying the shipped DDL rather than pushing the drizzle table objects
13
+ * is the point — a test then fails when the two drift (reader#73).
14
+ */
15
+ export const applyShippedMigrations = (sqlite: Database.Database): void => {
16
+ for (const entry of [...migrationJournal()].sort(
17
+ (left, right) => left.idx - right.idx,
18
+ )) {
19
+ applyMigration(sqlite, entry.tag);
20
+ }
21
+ };
22
+
23
+ /**
24
+ * A migrated in-memory database and a drizzle handle over it, for a test that
25
+ * wants one repo and no file to clean up.
26
+ */
27
+ export const createShippedSqliteDb = (): {
28
+ db: Db<Record<string, unknown>>;
29
+ sqlite: Database.Database;
30
+ close: () => void;
31
+ } => {
32
+ const sqlite = new Database(":memory:");
33
+ applyShippedMigrations(sqlite);
34
+ return {
35
+ db: drizzle(sqlite) as unknown as Db<Record<string, unknown>>,
36
+ sqlite,
37
+ close: () => sqlite.close(),
38
+ };
39
+ };