@remit/drizzle-service 0.0.98 → 0.0.100

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.
Files changed (93) hide show
  1. package/package.json +1 -1
  2. package/src/batch.sqlite.test.ts +78 -0
  3. package/src/batch.ts +45 -0
  4. package/src/databases.ts +70 -0
  5. package/src/db.ts +1 -4
  6. package/src/generated-enum.test.ts +8 -0
  7. package/src/generated-id.test.ts +19 -0
  8. package/src/id.test.ts +0 -8
  9. package/src/id.ts +0 -6
  10. package/src/index.ts +16 -6
  11. package/src/repair/address-display-name.sqlite.test.ts +3 -5
  12. package/src/repair/address-display-name.ts +8 -3
  13. package/src/repair/junk-only-address.sqlite.test.ts +8 -36
  14. package/src/repair/stranded-sent-outbox.sqlite.test.ts +2 -9
  15. package/src/repos/account.conformance.sqlite.test.ts +1 -1
  16. package/src/repos/calendar-default.sqlite.test.ts +6 -6
  17. package/src/repos/calendar-put.sqlite.test.ts +40 -39
  18. package/src/repos/calendar-store.ts +273 -0
  19. package/src/repos/cascade-delete.sqlite.test.ts +7 -9
  20. package/src/repos/cascade-delete.test.ts +6 -13
  21. package/src/repos/cascade-delete.ts +127 -97
  22. package/src/repos/envelope.test.ts +1 -17
  23. package/src/repos/envelope.ts +83 -67
  24. package/src/repos/filter.test.ts +22 -2
  25. package/src/repos/filter.ts +1 -5
  26. package/src/repos/i4-account-config.test.ts +2 -2
  27. package/src/repos/i4-account-config.ts +5 -2
  28. package/src/repos/i4-account.test.ts +6 -2
  29. package/src/repos/i4-account.ts +12 -56
  30. package/src/repos/i4-address-junk-move.sqlite.test.ts +1 -1
  31. package/src/repos/i4-address-merge-flags.sqlite.test.ts +1 -2
  32. package/src/repos/i4-address.ts +58 -90
  33. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +6 -2
  34. package/src/repos/i4-mailbox-special-use.ts +9 -6
  35. package/src/repos/i4-mailbox.sqlite.test.ts +12 -14
  36. package/src/repos/i4-mailbox.test.ts +17 -62
  37. package/src/repos/i4-mailbox.ts +121 -94
  38. package/src/repos/i4-organize-job-request.ts +0 -4
  39. package/src/repos/i4-outbox-attachment.ts +62 -33
  40. package/src/repos/mappers.ts +0 -16
  41. package/src/repos/message.sqlite.test.ts +62 -102
  42. package/src/repos/message.test.ts +6 -167
  43. package/src/repos/message.ts +227 -218
  44. package/src/repos/sync-warning.ts +76 -0
  45. package/src/repos/thread-message-body-muted.sqlite.test.ts +5 -10
  46. package/src/repos/thread-message-field-terms.sqlite.test.ts +5 -1
  47. package/src/repos/thread-message.sqlite.test.ts +2 -1
  48. package/src/repos/thread-message.test.ts +17 -4
  49. package/src/repos/thread-message.ts +502 -285
  50. package/src/repos/thread-search-predicates.ts +24 -26
  51. package/src/schema/message-data.ts +4 -8
  52. package/src/schema/sync-warning.ts +3 -0
  53. package/src/schema-sets/app.ts +17 -0
  54. package/src/schema-sets/mail.ts +32 -0
  55. package/src/schema-sets/search.ts +5 -0
  56. package/src/schema-version-accounting.sqlite.test.ts +56 -43
  57. package/src/schema.ts +3 -4
  58. package/src/search/thread-message-search.sqlite.test.ts +131 -0
  59. package/src/search/thread-message-search.ts +271 -0
  60. package/src/sqlite-client.ts +5 -7
  61. package/src/sqlite-repositories.ts +75 -0
  62. package/src/test-db-sqlite.ts +5 -13
  63. package/src/test-shipped-sqlite-schema.ts +46 -45
  64. package/src/test-sqlite.ts +35 -11
  65. package/src/vps-migrations-drift.sqlite.test.ts +81 -15
  66. package/src/account-granted-scopes-migration.sqlite.test.ts +0 -66
  67. package/src/account-synced-services-migration.sqlite.test.ts +0 -56
  68. package/src/mailbox-path-dedupe.sqlite.test.ts +0 -462
  69. package/src/mailbox-sync-status-backfill.sqlite.test.ts +0 -118
  70. package/src/outbox-created-cleanup.sqlite.test.ts +0 -83
  71. package/src/repair/duplicate-mailbox-mail.ts +0 -98
  72. package/src/repair/thread-message-category-contract.ts +0 -505
  73. package/src/repair/thread-message-category.sqlite.test.ts +0 -100
  74. package/src/repair/thread-message-category.ts +0 -415
  75. package/src/repos/calendar-migrations.sqlite.test.ts +0 -345
  76. package/src/repos/calendar-unit-of-work.ts +0 -31
  77. package/src/repos/config-import-atomicity.sqlite.test.ts +0 -255
  78. package/src/repos/filter-anchor-transaction.test.ts +0 -136
  79. package/src/repos/filter-anchor-transaction.ts +0 -45
  80. package/src/repos/filter-anchor.test.ts +0 -131
  81. package/src/repos/filter-anchor.ts +0 -105
  82. package/src/repos/i4-mailbox-special-use.structure.test.ts +0 -27
  83. package/src/repos/message-placement-backfill.sqlite.test.ts +0 -190
  84. package/src/repos/search-index-shape.test.ts +0 -82
  85. package/src/repos/search-index-shape.ts +0 -56
  86. package/src/repos/serialized-writes.sqlite.test.ts +0 -199
  87. package/src/repos/unit-of-work.sqlite.test.ts +0 -138
  88. package/src/repos/unit-of-work.test.ts +0 -109
  89. package/src/repos/unit-of-work.ts +0 -31
  90. package/src/schema/outbox.ts +0 -46
  91. package/src/schema-full-sqlite.ts +0 -13
  92. package/src/tx.sqlite.test.ts +0 -83
  93. package/src/tx.ts +0 -233
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import { eq } from "drizzle-orm";
4
+ import { runBatch } from "./batch.js";
5
+ import { calendarTable } from "./schema.js";
6
+ import { createSqliteTestDb, type SqliteTestDb } from "./test-db-sqlite.js";
7
+
8
+ const SCHEMA = { calendars: calendarTable };
9
+
10
+ const collection = (calendarId: string): typeof calendarTable.$inferInsert => ({
11
+ calendarId,
12
+ accountConfigId: "account-config-batch",
13
+ urlSegment: calendarId,
14
+ displayName: calendarId,
15
+ color: "Cal1",
16
+ componentSet: "VeventOnly",
17
+ source: "UserCreated",
18
+ timezone: "",
19
+ subscriptionUrl: "",
20
+ subscriptionEnabled: false,
21
+ syncSequence: 0,
22
+ createdAt: 0,
23
+ updatedAt: 0,
24
+ });
25
+
26
+ describe("a batch on better-sqlite3", () => {
27
+ let db: SqliteTestDb<typeof SCHEMA>;
28
+ let close: () => Promise<void>;
29
+
30
+ before(async () => {
31
+ ({ db, close } = await createSqliteTestDb(SCHEMA));
32
+ });
33
+
34
+ after(() => close());
35
+
36
+ const stored = () => db.select().from(calendarTable);
37
+
38
+ test("answers each statement in order, reads seeing the writes before them", async () => {
39
+ const [inserted, read] = await runBatch(db, [
40
+ db.insert(calendarTable).values(collection("first")).returning(),
41
+ db
42
+ .select()
43
+ .from(calendarTable)
44
+ .where(eq(calendarTable.calendarId, "first")),
45
+ ] as const);
46
+
47
+ assert.deepEqual(
48
+ inserted.map((row) => row.calendarId),
49
+ ["first"],
50
+ );
51
+ assert.deepEqual(
52
+ read.map((row) => row.calendarId),
53
+ ["first"],
54
+ );
55
+ });
56
+
57
+ test("a statement that fails takes every earlier one with it", async () => {
58
+ const before = await stored();
59
+
60
+ await assert.rejects(
61
+ runBatch(db, [
62
+ db.insert(calendarTable).values(collection("second")),
63
+ db
64
+ .update(calendarTable)
65
+ .set({ syncSequence: 7 })
66
+ .where(eq(calendarTable.calendarId, "first")),
67
+ db.insert(calendarTable).values(collection("first")),
68
+ ]),
69
+ /UNIQUE constraint failed/,
70
+ );
71
+
72
+ assert.deepEqual(await stored(), before);
73
+ });
74
+
75
+ test("an empty batch writes nothing and answers nothing", async () => {
76
+ assert.deepEqual(await runBatch(db, []), []);
77
+ });
78
+ });
package/src/batch.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { BatchItem, BatchResponse } from "drizzle-orm/batch";
2
+ import type { Db } from "./db.js";
3
+
4
+ export type SqliteBatchItem = BatchItem<"sqlite">;
5
+
6
+ interface SyncPreparable {
7
+ _prepare(): { execute(): { sync(): unknown } };
8
+ }
9
+
10
+ const isSyncPreparable = (value: unknown): value is SyncPreparable =>
11
+ typeof value === "object" &&
12
+ value !== null &&
13
+ typeof Reflect.get(value, "_prepare") === "function";
14
+
15
+ const runSync = (item: SqliteBatchItem): unknown => {
16
+ if (!isSyncPreparable(item)) {
17
+ throw new TypeError("batch item is not a drizzle sqlite query");
18
+ }
19
+ return item._prepare().execute().sync();
20
+ };
21
+
22
+ /**
23
+ * The shape of D1's `batch()`: every statement commits together or not at all,
24
+ * and none of them reads another's result mid-flight. A write that needs a
25
+ * value reads it before the batch and is written so a stale read loses safely
26
+ * (a conditional `WHERE`, an upsert, a deterministic id).
27
+ *
28
+ * On better-sqlite3 the batch is one synchronous IMMEDIATE transaction: it
29
+ * never yields the event loop, so no other write on the shared connection lands
30
+ * inside it, and IMMEDIATE takes the file's write lock before the first
31
+ * statement so a writer in another process waits out the busy timeout.
32
+ */
33
+ export const runBatch = async <
34
+ TSchema extends Record<string, unknown>,
35
+ T extends readonly SqliteBatchItem[],
36
+ >(
37
+ db: Db<TSchema>,
38
+ items: T,
39
+ ): Promise<BatchResponse<T>> => {
40
+ if (items.length === 0) return [] as unknown as BatchResponse<T>;
41
+ const results = db.transaction(() => items.map(runSync), {
42
+ behavior: "immediate",
43
+ });
44
+ return results as unknown as BatchResponse<T>;
45
+ };
@@ -0,0 +1,70 @@
1
+ import type { Db } from "./db.js";
2
+ import {
3
+ type MessageDataSchema,
4
+ messageDataSchema,
5
+ } from "./schema/message-data.js";
6
+ import { createSqliteDatabase } from "./sqlite-client.js";
7
+
8
+ // The three databases a self-host instance runs on (#1420). The app database
9
+ // holds what lives in the client: accounts, config, rules, settings and auth.
10
+ // The tenant mail database holds what lives on the IMAP server, plus the local
11
+ // rows that refer to it. The tenant search database holds the full-text index,
12
+ // rebuilt from the mail database, so it never commits together with it.
13
+ export interface SqliteDatabases {
14
+ app: Db<Record<string, unknown>>;
15
+ mail: Db<MessageDataSchema>;
16
+ search: Db<Record<string, unknown>>;
17
+ }
18
+
19
+ export interface SqliteDatabasePaths {
20
+ app: string;
21
+ mail: string;
22
+ search: string;
23
+ }
24
+
25
+ export const SQLITE_DATABASE_ENV = {
26
+ app: "SQLITE_APP_DB_PATH",
27
+ mail: "SQLITE_MAIL_DB_PATH",
28
+ search: "SQLITE_SEARCH_DB_PATH",
29
+ } as const satisfies Record<keyof SqliteDatabasePaths, string>;
30
+
31
+ /**
32
+ * Whether this process is configured for the SQLite composition. The app path
33
+ * is the precondition every composition root checks before importing it.
34
+ */
35
+ export const hasSqliteDatabaseEnv = (
36
+ env: NodeJS.ProcessEnv = process.env,
37
+ ): boolean => Boolean(env[SQLITE_DATABASE_ENV.app]);
38
+
39
+ export const sqliteDatabasePathsFromEnv = (
40
+ env: NodeJS.ProcessEnv = process.env,
41
+ ): SqliteDatabasePaths => {
42
+ const read = (name: string): string => {
43
+ const value = env[name];
44
+ if (!value) throw new Error(`${name} is required`);
45
+ return value;
46
+ };
47
+ return {
48
+ app: read(SQLITE_DATABASE_ENV.app),
49
+ mail: read(SQLITE_DATABASE_ENV.mail),
50
+ search: read(SQLITE_DATABASE_ENV.search),
51
+ };
52
+ };
53
+
54
+ export const openSqliteDatabases = async (
55
+ paths: SqliteDatabasePaths,
56
+ ): Promise<SqliteDatabases & { close: () => Promise<void> }> => {
57
+ const app = await createSqliteDatabase({}, { filename: paths.app });
58
+ const mail = await createSqliteDatabase(messageDataSchema, {
59
+ filename: paths.mail,
60
+ });
61
+ const search = await createSqliteDatabase({}, { filename: paths.search });
62
+ return {
63
+ app: app.db,
64
+ mail: mail.db,
65
+ search: search.db,
66
+ close: async () => {
67
+ await Promise.all([app.close(), mail.close(), search.close()]);
68
+ },
69
+ };
70
+ };
package/src/db.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
2
2
 
3
3
  /**
4
- * A drizzle handle a repository can run on: the top-level database or a
5
- * transaction (or savepoint) bound to one. Both share the query-builder API, so
6
- * a repo constructed with either behaves the same — standalone, or enlisted in a
7
- * unit-of-work transaction.
4
+ * The drizzle handle a repository runs on.
8
5
  *
9
6
  * Internal to this adapter: it names better-sqlite3, so it is not exported from
10
7
  * the package index and never appears on a shared surface.
@@ -0,0 +1,8 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { MailboxAttribute, MessageSystemFlag } from "@remit/domain-enums";
4
+
5
+ test("the generated enums keep the backslash IMAP puts on a system flag and a mailbox attribute", () => {
6
+ assert.equal(MessageSystemFlag.Seen, "\\Seen");
7
+ assert.equal(MailboxAttribute.NoSelect, "\\Noselect");
8
+ });
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { generateBase36Id } from "@remit/drizzle-sqlite-schema";
4
+ import shortUuid from "short-uuid";
5
+
6
+ const base36 = shortUuid.createTranslator(shortUuid.constants.uuid25Base36);
7
+
8
+ test("the schema's id default writes the 25-char base36 v4 uuid that short-uuid reads and writes back unchanged", () => {
9
+ for (let i = 0; i < 5000; i += 1) {
10
+ const id = generateBase36Id();
11
+ assert.match(id, /^[0-9a-z]{25}$/);
12
+ const uuid = base36.toUUID(id);
13
+ assert.match(
14
+ uuid,
15
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
16
+ );
17
+ assert.equal(base36.fromUUID(uuid), id);
18
+ }
19
+ });
package/src/id.test.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, test } from "node:test";
3
3
  import {
4
- bodyPartContentId,
5
4
  bodyPartId,
6
5
  bodyPartParameterId,
7
6
  deterministicBase36Id,
@@ -59,11 +58,4 @@ describe("derived ids are stored primary keys: a changed value orphans every row
59
58
  "5f4e930e-e418-51a2-9402-657822f8688b",
60
59
  );
61
60
  });
62
-
63
- test("bodyPartContentId pins the body_part_content primary key", () => {
64
- assert.equal(
65
- bodyPartContentId(MESSAGE, "bodypart-golden"),
66
- "c0ee5f7d-1894-5957-9f65-d9aafdf306ab",
67
- );
68
- });
69
61
  });
package/src/id.ts CHANGED
@@ -42,9 +42,3 @@ export const envelopeAddressId = (
42
42
  order: number,
43
43
  ): string =>
44
44
  generateDeterministicId(`envelopeaddress:${messageId}:${role}:${order}`);
45
-
46
- export const bodyPartContentId = (
47
- messageId: string,
48
- bodyPartId: string,
49
- ): string =>
50
- generateDeterministicId(`bodypartcontent:${messageId}:${bodyPartId}`);
package/src/index.ts CHANGED
@@ -1,10 +1,18 @@
1
+ export {
2
+ hasSqliteDatabaseEnv,
3
+ openSqliteDatabases,
4
+ SQLITE_DATABASE_ENV,
5
+ type SqliteDatabasePaths,
6
+ type SqliteDatabases,
7
+ sqliteDatabasePathsFromEnv,
8
+ } from "./databases.js";
1
9
  export { CreateFailedConflictError, NotFoundError } from "./error.js";
2
10
  export { CalendarCollectionRepo } from "./repos/calendar-collection.js";
3
11
  export { CalendarEventIndexRepo } from "./repos/calendar-event-index.js";
4
12
  export { CalendarFeedTokenRepo } from "./repos/calendar-feed-token.js";
5
13
  export { CalendarObjectRepo } from "./repos/calendar-object.js";
14
+ export { DrizzleCalendarStore } from "./repos/calendar-store.js";
6
15
  export { CalendarSuggestionRepo } from "./repos/calendar-suggestion.js";
7
- export { DrizzleCalendarUnitOfWork } from "./repos/calendar-unit-of-work.js";
8
16
  export {
9
17
  type CascadeDeleteLogger,
10
18
  type CascadeDeleter,
@@ -14,8 +22,6 @@ export {
14
22
  } from "./repos/cascade-delete.js";
15
23
  export { DrizzleEnvelopeRepository } from "./repos/envelope.js";
16
24
  export { FilterRepo } from "./repos/filter.js";
17
- export { FilterAnchorRepo } from "./repos/filter-anchor.js";
18
- export { DrizzleFilterAnchorTransaction } from "./repos/filter-anchor-transaction.js";
19
25
  export * from "./repos/i4-account.js";
20
26
  export * from "./repos/i4-account-config.js";
21
27
  export * from "./repos/i4-account-export-request.js";
@@ -45,13 +51,12 @@ export { LabelRepo } from "./repos/label.js";
45
51
  export {
46
52
  DrizzleMessageRepository,
47
53
  deleteMessageSubtree,
48
- MESSAGE_REMOVED_EVENT,
49
54
  } from "./repos/message.js";
50
55
  export { DrizzleMessageFlagRepository } from "./repos/message-flag.js";
51
56
  export { MessageLabelRepo } from "./repos/message-label.js";
52
57
  export { QuarantineRepo } from "./repos/quarantine.js";
58
+ export { SyncWarningRepo } from "./repos/sync-warning.js";
53
59
  export { DrizzleThreadMessageRepository } from "./repos/thread-message.js";
54
- export { DrizzleUnitOfWork } from "./repos/unit-of-work.js";
55
60
  export * from "./schema/i4-account-config.js";
56
61
  export * from "./schema/i4-account-export-request.js";
57
62
  export * from "./schema/i4-account-setting.js";
@@ -66,9 +71,14 @@ export * from "./schema/i4-outbox-message.js";
66
71
  export * from "./schema/message-data.js";
67
72
  export { messageDataSchema } from "./schema/message-data.js";
68
73
  export * from "./schema/quarantine.js";
74
+ export * from "./schema/sync-warning.js";
75
+ export { SearchIndex } from "./search/thread-message-search.js";
69
76
  export {
70
77
  createSqliteDatabase,
71
78
  type SqliteClient,
72
79
  type SqliteClientOptions,
73
80
  } from "./sqlite-client.js";
74
- export { runInTransaction, serializeSqliteWrites } from "./tx.js";
81
+ export {
82
+ createSqliteRepositories,
83
+ type SqliteRepositories,
84
+ } from "./sqlite-repositories.js";
@@ -17,11 +17,9 @@ const clientOver = (sqlite: Database.Database): DisplayNameRepairClient => ({
17
17
  });
18
18
 
19
19
  const seed = (sqlite: Database.Database): void => {
20
- sqlite.exec(shippedTableDdl("0000_happy_roland_deschain", "address"));
21
- sqlite.exec(
22
- shippedTableDdl("0000_happy_roland_deschain", "envelope_address"),
23
- );
24
- sqlite.exec(shippedTableDdl("0000_happy_roland_deschain", "thread_message"));
20
+ sqlite.exec(shippedTableDdl("address"));
21
+ sqlite.exec(shippedTableDdl("envelope_address"));
22
+ sqlite.exec(shippedTableDdl("thread_message"));
25
23
 
26
24
  const address = sqlite.prepare(
27
25
  `INSERT INTO address (
@@ -48,6 +48,8 @@ export interface SiteResult {
48
48
  readonly scanned: number;
49
49
  readonly claiming: number;
50
50
  readonly rewritten: number;
51
+ /** The keys of the rows rewritten, so a caller can follow them elsewhere. */
52
+ readonly rewrittenIds: readonly string[];
51
53
  }
52
54
 
53
55
  export interface DisplayNameReport {
@@ -112,7 +114,7 @@ export const sweepDisplayNames = async (
112
114
  for (const site of SITES) {
113
115
  const rows = await candidates(client, site);
114
116
  let claiming = 0;
115
- let rewritten = 0;
117
+ const rewrittenIds: string[] = [];
116
118
 
117
119
  for (const row of rows) {
118
120
  const name = row.name ?? "";
@@ -120,7 +122,9 @@ export const sweepDisplayNames = async (
120
122
  if (stored === name) continue;
121
123
  claiming += 1;
122
124
  if (mode === "repair") {
123
- rewritten += await rewrite(client, site, row, stored);
125
+ if ((await rewrite(client, site, row, stored)) > 0) {
126
+ rewrittenIds.push(row.id);
127
+ }
124
128
  }
125
129
  }
126
130
 
@@ -128,7 +132,8 @@ export const sweepDisplayNames = async (
128
132
  table: site.table,
129
133
  scanned: rows.length,
130
134
  claiming,
131
- rewritten,
135
+ rewritten: rewrittenIds.length,
136
+ rewrittenIds,
132
137
  });
133
138
  }
134
139
 
@@ -1,5 +1,4 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
2
  import { after, before, beforeEach, describe, test } from "node:test";
4
3
  import { composeFolderRoleAppointmentName } from "@remit/data-ports/folder-role";
5
4
  import { CanonicalMailboxRole } from "@remit/domain-enums";
@@ -7,10 +6,7 @@ import Database from "better-sqlite3";
7
6
  import { drizzle } from "drizzle-orm/better-sqlite3";
8
7
  import { AccountSettingRepo } from "../repos/i4-account-setting.js";
9
8
  import { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
10
- import {
11
- applyMigration,
12
- shippedTableDdl,
13
- } from "../test-shipped-sqlite-schema.js";
9
+ import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
14
10
  import {
15
11
  type JunkOnlyRepairClient,
16
12
  type JunkOnlyRepairMode,
@@ -18,8 +14,6 @@ import {
18
14
  sweepJunkOnlyAddresses,
19
15
  } from "./junk-only-address.js";
20
16
 
21
- const DDL_TAG = "0000_happy_roland_deschain";
22
-
23
17
  const clientOver = (sqlite: Database.Database): JunkOnlyRepairClient => ({
24
18
  all: async (sql, params) => sqlite.prepare(sql).all(...params),
25
19
  run: async (sql, params) => sqlite.prepare(sql).run(...params).changes,
@@ -53,11 +47,11 @@ describe("addresses standing only on mail in Junk", () => {
53
47
  sqlite
54
48
  .prepare(
55
49
  `INSERT OR IGNORE INTO account (
56
- account_id, account_config_id, username, email, imap_host,
57
- imap_port, imap_tls, imap_start_tls, smtp_port, is_active,
50
+ account_id, account_config_id, username, email, synced_services,
51
+ imap_host, imap_port, imap_tls, imap_start_tls, smtp_port, is_active,
58
52
  connection_state, created_at, updated_at
59
- ) VALUES (?, 'cfg-1', 'user', 'user@example.com', 'imap.example.com',
60
- 993, 1, 0, 465, 1, 'disconnected', 0, 0)`,
53
+ ) VALUES (?, 'cfg-1', 'user', 'user@example.com', '["Mail"]',
54
+ 'imap.example.com', 993, 1, 0, 465, 1, 'disconnected', 0, 0)`,
61
55
  )
62
56
  .run(accountId);
63
57
  };
@@ -187,32 +181,10 @@ describe("addresses standing only on mail in Junk", () => {
187
181
  before(() => {
188
182
  sqlite = new Database(":memory:");
189
183
  const db = drizzle(sqlite);
190
- specialUse = new MailboxSpecialUseRepo(db as never);
184
+ specialUse = new MailboxSpecialUseRepo(db as never, db as never);
191
185
  accountSetting = new AccountSettingRepo(db as never);
192
- for (const table of [
193
- "address",
194
- "envelope_address",
195
- "message",
196
- "mailbox",
197
- "mailbox_special_use_entry",
198
- "account_setting",
199
- "account",
200
- ]) {
201
- sqlite.exec(shippedTableDdl(DDL_TAG, table));
202
- }
203
- // `mailbox` is read through the repo, which selects every column the
204
- // entity declares — including the rename target added after this DDL.
205
- applyMigration(sqlite, "0028_mailbox_pending_path");
206
- applyMigration(sqlite, "0033_mailbox_sync_failure_reason");
207
- sqlite.exec(
208
- readFileSync(
209
- new URL(
210
- "../../../../npm-scripts/sqlite-address-sightings-index.sql",
211
- import.meta.url,
212
- ),
213
- "utf8",
214
- ),
215
- );
186
+ applyMigrationSet(sqlite, "app");
187
+ applyMigrationSet(sqlite, "mail");
216
188
  });
217
189
 
218
190
  after(() => {
@@ -10,10 +10,7 @@
10
10
  import assert from "node:assert/strict";
11
11
  import { after, before, describe, test } from "node:test";
12
12
  import Database from "better-sqlite3";
13
- import {
14
- applyMigration,
15
- shippedTableDdl,
16
- } from "../test-shipped-sqlite-schema.js";
13
+ import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
17
14
  import {
18
15
  STRANDED_AFTER_MILLIS,
19
16
  type StrandedSentRepairClient,
@@ -82,11 +79,7 @@ describe("outbox rows stranded at sent", () => {
82
79
 
83
80
  before(() => {
84
81
  sqlite = new Database(":memory:");
85
- sqlite.exec(
86
- shippedTableDdl("0000_happy_roland_deschain", "outbox_message"),
87
- );
88
- sqlite.exec(shippedTableDdl("0013_low_harpoon", "outbox_attachment"));
89
- applyMigration(sqlite, "0015_true_meggan");
82
+ applyMigrationSet(sqlite, "mail");
90
83
  });
91
84
 
92
85
  after(() => {
@@ -13,7 +13,7 @@ accountRepositoryConformance({
13
13
  accounts: accountTable,
14
14
  });
15
15
  close = closeDb;
16
- return new AccountRepo(db as never);
16
+ return new AccountRepo(db as never, db as never);
17
17
  },
18
18
  teardown: async () => {
19
19
  await close?.();
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import { provisionDefaultCalendar } from "@remit/calendar-service";
4
- import type { ICalendarUnitOfWork } from "@remit/data-ports";
4
+ import type { ICalendarStore } from "@remit/data-ports";
5
5
  import type { Db } from "../db.js";
6
6
  import {
7
7
  calendarEventIndexTable,
@@ -10,14 +10,14 @@ import {
10
10
  } from "../schema.js";
11
11
  import { createSqliteTestDb } from "../test-db-sqlite.js";
12
12
  import { CalendarCollectionRepo } from "./calendar-collection.js";
13
- import { DrizzleCalendarUnitOfWork } from "./calendar-unit-of-work.js";
13
+ import { DrizzleCalendarStore } from "./calendar-store.js";
14
14
 
15
15
  const ACCOUNT_CONFIG_ID = "account-config-default";
16
16
 
17
17
  describe("provisioning the default calendar", () => {
18
18
  let db: Db<Record<string, unknown>>;
19
19
  let close: () => Promise<void>;
20
- let unitOfWork: ICalendarUnitOfWork;
20
+ let store: ICalendarStore;
21
21
 
22
22
  before(async () => {
23
23
  const created = await createSqliteTestDb({
@@ -27,7 +27,7 @@ describe("provisioning the default calendar", () => {
27
27
  });
28
28
  db = created.db as unknown as Db<Record<string, unknown>>;
29
29
  close = created.close;
30
- unitOfWork = new DrizzleCalendarUnitOfWork(db);
30
+ store = new DrizzleCalendarStore(db, db);
31
31
  });
32
32
 
33
33
  after(() => close());
@@ -35,7 +35,7 @@ describe("provisioning the default calendar", () => {
35
35
  test("leaves one calendar behind when several first reads arrive together", async () => {
36
36
  const provisioned = await Promise.all(
37
37
  Array.from({ length: 8 }, () =>
38
- provisionDefaultCalendar(unitOfWork, ACCOUNT_CONFIG_ID),
38
+ provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID),
39
39
  ),
40
40
  );
41
41
 
@@ -61,7 +61,7 @@ describe("provisioning the default calendar", () => {
61
61
  await repo.bumpSyncSequence(ACCOUNT_CONFIG_ID, before.calendarId);
62
62
 
63
63
  const again = await provisionDefaultCalendar(
64
- unitOfWork,
64
+ store,
65
65
  ACCOUNT_CONFIG_ID,
66
66
  "A different name",
67
67
  );