@remit/drizzle-service 0.0.102 → 0.0.104

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 (88) hide show
  1. package/package.json +4 -3
  2. package/src/batch.sqlite.test.ts +4 -4
  3. package/src/batch.ts +47 -21
  4. package/src/databases.ts +24 -6
  5. package/src/db.ts +20 -5
  6. package/src/index.ts +12 -0
  7. package/src/repair/junk-only-address.sqlite.test.ts +7 -2
  8. package/src/repos/account.conformance.d1.test.ts +20 -0
  9. package/src/repos/account.conformance.sqlite.test.ts +6 -2
  10. package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
  11. package/src/repos/calendar-collection.ts +2 -2
  12. package/src/repos/calendar-default.d1.test.ts +4 -0
  13. package/src/repos/calendar-default.sqlite.test.ts +3 -72
  14. package/src/repos/calendar-default.suite.ts +68 -0
  15. package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
  16. package/src/repos/calendar-event-index.ts +2 -2
  17. package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
  18. package/src/repos/calendar-feed-token.ts +2 -2
  19. package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
  20. package/src/repos/calendar-object.ts +2 -2
  21. package/src/repos/calendar-put.d1.test.ts +4 -0
  22. package/src/repos/calendar-put.sqlite.test.ts +3 -205
  23. package/src/repos/calendar-put.suite.ts +210 -0
  24. package/src/repos/calendar-store.ts +36 -40
  25. package/src/repos/calendar-suggestion.ts +2 -2
  26. package/src/repos/cascade-delete.test.ts +3 -2
  27. package/src/repos/cascade-delete.ts +12 -8
  28. package/src/repos/envelope.sqlite.test.ts +3 -2
  29. package/src/repos/envelope.test.ts +3 -2
  30. package/src/repos/envelope.ts +14 -9
  31. package/src/repos/filter.d1.test.ts +4 -0
  32. package/src/repos/filter.suite.ts +476 -0
  33. package/src/repos/filter.test.ts +3 -472
  34. package/src/repos/filter.ts +2 -2
  35. package/src/repos/i4-account-config.d1.test.ts +4 -0
  36. package/src/repos/i4-account-config.suite.ts +131 -0
  37. package/src/repos/i4-account-config.test.ts +3 -123
  38. package/src/repos/i4-account-config.ts +8 -10
  39. package/src/repos/i4-account-export-request.d1.test.ts +4 -0
  40. package/src/repos/i4-account-export-request.suite.ts +44 -0
  41. package/src/repos/i4-account-export-request.test.ts +3 -40
  42. package/src/repos/i4-account-export-request.ts +2 -2
  43. package/src/repos/i4-account-setting.d1.test.ts +4 -0
  44. package/src/repos/i4-account-setting.suite.ts +100 -0
  45. package/src/repos/i4-account-setting.test.ts +3 -93
  46. package/src/repos/i4-account-setting.ts +2 -2
  47. package/src/repos/i4-account.test.ts +12 -8
  48. package/src/repos/i4-account.ts +6 -10
  49. package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
  50. package/src/repos/i4-address.ts +12 -2
  51. package/src/repos/i4-config-import.ts +2 -2
  52. package/src/repos/i4-mailbox-lock.ts +2 -2
  53. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
  54. package/src/repos/i4-mailbox-special-use.ts +29 -40
  55. package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
  56. package/src/repos/i4-mailbox.test.ts +15 -14
  57. package/src/repos/i4-mailbox.ts +13 -7
  58. package/src/repos/i4-message-flag-push.ts +2 -2
  59. package/src/repos/i4-message-placement-move.ts +2 -2
  60. package/src/repos/i4-organize-job-request.ts +2 -2
  61. package/src/repos/i4-outbox-attachment.test.ts +7 -2
  62. package/src/repos/i4-outbox-attachment.ts +11 -5
  63. package/src/repos/i4-outbox-message.ts +2 -2
  64. package/src/repos/i4-sender-signer-standing.ts +2 -2
  65. package/src/repos/label.conformance.d1.test.ts +20 -0
  66. package/src/repos/label.ts +2 -2
  67. package/src/repos/message-flag.ts +3 -6
  68. package/src/repos/message-label.ts +2 -2
  69. package/src/repos/message.sqlite.test.ts +57 -3
  70. package/src/repos/message.test.ts +6 -5
  71. package/src/repos/message.ts +81 -10
  72. package/src/repos/quarantine.ts +2 -2
  73. package/src/repos/sync-warning.ts +2 -2
  74. package/src/repos/thread-message-body-muted.sqlite.test.ts +30 -22
  75. package/src/repos/thread-message-field-terms.sqlite.test.ts +16 -11
  76. package/src/repos/thread-message.sqlite.test.ts +13 -7
  77. package/src/repos/thread-message.test.ts +43 -27
  78. package/src/repos/thread-message.ts +26 -14
  79. package/src/repos/thread-search-predicates.ts +36 -63
  80. package/src/schema/message-data.ts +3 -0
  81. package/src/schema-sets/mail.ts +1 -0
  82. package/src/search/thread-message-search.sqlite.test.ts +57 -11
  83. package/src/search/thread-message-search.ts +26 -12
  84. package/src/sqlite-repositories.ts +48 -32
  85. package/src/test-d1.ts +40 -0
  86. package/src/test-db-sqlite.ts +1 -1
  87. package/src/test-shipped-sqlite-schema.ts +8 -6
  88. package/src/test-stores.ts +30 -0
@@ -1,206 +1,4 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, test } from "node:test";
3
- import {
4
- prepareCalendarObject,
5
- provisionDefaultCalendar,
6
- putCalendarObject,
7
- } from "@remit/calendar-service";
8
- import type {
9
- CalendarRepositories,
10
- CalendarWrite,
11
- ICalendarStore,
12
- } from "@remit/data-ports";
13
- import type { Db } from "../db.js";
14
- import {
15
- calendarEventIndexTable,
16
- calendarObjectTable,
17
- calendarTable,
18
- } from "../schema.js";
19
- import { createSqliteTestDb } from "../test-db-sqlite.js";
20
- import { DrizzleCalendarStore } from "./calendar-store.js";
1
+ import { openCalendarSqliteTestStores } from "../test-stores.js";
2
+ import { calendarPutSuite } from "./calendar-put.suite.js";
21
3
 
22
- const ACCOUNT_CONFIG_ID = "account-config-1";
23
-
24
- const RESOURCE = [
25
- "BEGIN:VCALENDAR",
26
- "VERSION:2.0",
27
- "BEGIN:VEVENT",
28
- "UID:atomic@example.com",
29
- "DTSTART:20260826T090000Z",
30
- "DTEND:20260826T100000Z",
31
- "SUMMARY:Quarterly review",
32
- "RRULE:FREQ=WEEKLY;COUNT=3",
33
- "END:VEVENT",
34
- "END:VCALENDAR",
35
- "",
36
- ].join("\r\n");
37
-
38
- /**
39
- * Appends a write that fails to every commit: a resource put into a collection
40
- * that does not exist, which the store refuses at its last statement. Every
41
- * statement of the real write set has run by then, so a failure there is the
42
- * case that proves the boundary.
43
- */
44
- class FailingLastWriteStore implements ICalendarStore {
45
- constructor(private inner: ICalendarStore) {}
46
-
47
- get repos(): CalendarRepositories {
48
- return this.inner.repos;
49
- }
50
-
51
- async commit(writes: readonly CalendarWrite[]): Promise<void> {
52
- const orphan = await prepareCalendarObject(
53
- { timezone: "" },
54
- {
55
- accountConfigId: ACCOUNT_CONFIG_ID,
56
- calendarId: "no-such-calendar",
57
- resourceName: "orphan.ics",
58
- icalData: RESOURCE,
59
- },
60
- );
61
- assert.ok(orphan.ok);
62
- await this.inner.commit([...writes, orphan.value]);
63
- }
64
- }
65
-
66
- describe("the calendar write path against sqlite", () => {
67
- let db: Db<Record<string, unknown>>;
68
- let close: () => Promise<void>;
69
- let store: ICalendarStore;
70
- let calendarId: string;
71
-
72
- before(async () => {
73
- const created = await createSqliteTestDb({
74
- calendars: calendarTable,
75
- calendarObjects: calendarObjectTable,
76
- calendarEventIndexes: calendarEventIndexTable,
77
- });
78
- db = created.db as unknown as Db<Record<string, unknown>>;
79
- close = created.close;
80
- store = new DrizzleCalendarStore(db, db);
81
- const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
82
- calendarId = collection.calendarId;
83
- });
84
-
85
- after(() => close());
86
-
87
- const objects = () => db.select().from(calendarObjectTable);
88
- const occurrences = () => db.select().from(calendarEventIndexTable);
89
-
90
- test("writes the object, its occurrences and the sequence bump together", async () => {
91
- const result = await putCalendarObject(store, {
92
- accountConfigId: ACCOUNT_CONFIG_ID,
93
- calendarId,
94
- resourceName: "review.ics",
95
- icalData: RESOURCE,
96
- });
97
-
98
- assert.ok(result.ok);
99
- assert.equal(result.value.icalData, RESOURCE);
100
- assert.equal(
101
- (await objects())[0]?.icalData,
102
- RESOURCE,
103
- "the stored row holds the input bytes, CRLF and all",
104
- );
105
- assert.equal(result.value.syncSequence, 1);
106
- assert.equal((await objects()).length, 1);
107
- assert.equal(
108
- (await occurrences()).filter(
109
- (row) => row.calendarObjectId === result.value.calendarObjectId,
110
- ).length,
111
- 3,
112
- );
113
- });
114
-
115
- test("a failing write in the batch leaves no object behind", async () => {
116
- const before = await objects();
117
-
118
- await assert.rejects(
119
- putCalendarObject(new FailingLastWriteStore(store), {
120
- accountConfigId: ACCOUNT_CONFIG_ID,
121
- calendarId,
122
- resourceName: "doomed.ics",
123
- icalData: RESOURCE,
124
- }),
125
- /NOT NULL constraint failed/,
126
- );
127
-
128
- const after = await objects();
129
- assert.equal(after.length, before.length);
130
- assert.equal(
131
- after.some((row) => row.resourceName === "doomed.ics"),
132
- false,
133
- );
134
- });
135
-
136
- test("a failing write in the batch leaves the collection's sequence where it was", async () => {
137
- const [before] = await db.select().from(calendarTable);
138
-
139
- await assert.rejects(
140
- putCalendarObject(new FailingLastWriteStore(store), {
141
- accountConfigId: ACCOUNT_CONFIG_ID,
142
- calendarId,
143
- resourceName: "doomed-again.ics",
144
- icalData: RESOURCE,
145
- }),
146
- /NOT NULL constraint failed/,
147
- );
148
-
149
- const [after] = await db.select().from(calendarTable);
150
- assert.equal(after?.syncSequence, before?.syncSequence);
151
- });
152
-
153
- test("a refused resource writes nothing at all", async () => {
154
- const before = await objects();
155
- const [collectionBefore] = await db.select().from(calendarTable);
156
-
157
- const result = await putCalendarObject(store, {
158
- accountConfigId: ACCOUNT_CONFIG_ID,
159
- calendarId,
160
- resourceName: "backwards.ics",
161
- icalData: [
162
- "BEGIN:VCALENDAR",
163
- "VERSION:2.0",
164
- "BEGIN:VEVENT",
165
- "UID:backwards@example.com",
166
- "DTSTART:20260826T100000Z",
167
- "DTEND:20260826T090000Z",
168
- "END:VEVENT",
169
- "END:VCALENDAR",
170
- "",
171
- ].join("\r\n"),
172
- });
173
-
174
- assert.ok(!result.ok);
175
- assert.equal(result.error.code, "BackwardsEnd");
176
- assert.equal((await objects()).length, before.length);
177
- const [collectionAfter] = await db.select().from(calendarTable);
178
- assert.equal(collectionAfter?.syncSequence, collectionBefore?.syncSequence);
179
- });
180
-
181
- test("rewriting a resource replaces its occurrences rather than adding to them", async () => {
182
- const first = await putCalendarObject(store, {
183
- accountConfigId: ACCOUNT_CONFIG_ID,
184
- calendarId,
185
- resourceName: "shrinking.ics",
186
- icalData: RESOURCE,
187
- });
188
- assert.ok(first.ok);
189
-
190
- const second = await putCalendarObject(store, {
191
- accountConfigId: ACCOUNT_CONFIG_ID,
192
- calendarId,
193
- resourceName: "shrinking.ics",
194
- icalData: RESOURCE.replace("COUNT=3", "COUNT=1"),
195
- });
196
- assert.ok(second.ok);
197
-
198
- assert.equal(second.value.calendarObjectId, first.value.calendarObjectId);
199
- assert.equal(
200
- (await occurrences()).filter(
201
- (row) => row.calendarObjectId === second.value.calendarObjectId,
202
- ).length,
203
- 1,
204
- );
205
- });
206
- });
4
+ calendarPutSuite(openCalendarSqliteTestStores);
@@ -0,0 +1,210 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import {
4
+ prepareCalendarObject,
5
+ provisionDefaultCalendar,
6
+ putCalendarObject,
7
+ } from "@remit/calendar-service";
8
+ import type {
9
+ CalendarRepositories,
10
+ CalendarWrite,
11
+ ICalendarStore,
12
+ } from "@remit/data-ports";
13
+ import type { RepositoryDb } from "../db.js";
14
+ import {
15
+ calendarEventIndexTable,
16
+ calendarObjectTable,
17
+ calendarTable,
18
+ } from "../schema.js";
19
+ import { composeSqliteRepositories } from "../sqlite-repositories.js";
20
+ import type { OpenTestStores } from "../test-stores.js";
21
+
22
+ const ACCOUNT_CONFIG_ID = "account-config-1";
23
+
24
+ const RESOURCE = [
25
+ "BEGIN:VCALENDAR",
26
+ "VERSION:2.0",
27
+ "BEGIN:VEVENT",
28
+ "UID:atomic@example.com",
29
+ "DTSTART:20260826T090000Z",
30
+ "DTEND:20260826T100000Z",
31
+ "SUMMARY:Quarterly review",
32
+ "RRULE:FREQ=WEEKLY;COUNT=3",
33
+ "END:VEVENT",
34
+ "END:VCALENDAR",
35
+ "",
36
+ ].join("\r\n");
37
+
38
+ /**
39
+ * Appends a write that fails to every commit: a resource put into a collection
40
+ * that does not exist, which the store refuses at its last statement. Every
41
+ * statement of the real write set has run by then, so a failure there is the
42
+ * case that proves the boundary.
43
+ */
44
+ class FailingLastWriteStore implements ICalendarStore {
45
+ constructor(private inner: ICalendarStore) {}
46
+
47
+ get repos(): CalendarRepositories {
48
+ return this.inner.repos;
49
+ }
50
+
51
+ async commit(writes: readonly CalendarWrite[]): Promise<void> {
52
+ const orphan = await prepareCalendarObject(
53
+ { timezone: "" },
54
+ {
55
+ accountConfigId: ACCOUNT_CONFIG_ID,
56
+ calendarId: "no-such-calendar",
57
+ resourceName: "orphan.ics",
58
+ icalData: RESOURCE,
59
+ },
60
+ );
61
+ assert.ok(orphan.ok);
62
+ await this.inner.commit([...writes, orphan.value]);
63
+ }
64
+ }
65
+
66
+ export const calendarPutSuite = (open: OpenTestStores): void => {
67
+ describe("the calendar write path against sqlite", () => {
68
+ let db: RepositoryDb<Record<string, unknown>>;
69
+ let close: () => Promise<void>;
70
+ let store: ICalendarStore;
71
+ let calendarId: string;
72
+
73
+ before(async () => {
74
+ const opened = await open();
75
+ db = opened.stores.app.db;
76
+ close = opened.close;
77
+ store = composeSqliteRepositories(opened.stores).calendarStore;
78
+ const collection = await provisionDefaultCalendar(
79
+ store,
80
+ ACCOUNT_CONFIG_ID,
81
+ );
82
+ calendarId = collection.calendarId;
83
+ });
84
+
85
+ after(() => close());
86
+
87
+ const objects = () => db.select().from(calendarObjectTable);
88
+ const occurrences = () => db.select().from(calendarEventIndexTable);
89
+
90
+ test("writes the object, its occurrences and the sequence bump together", async () => {
91
+ const result = await putCalendarObject(store, {
92
+ accountConfigId: ACCOUNT_CONFIG_ID,
93
+ calendarId,
94
+ resourceName: "review.ics",
95
+ icalData: RESOURCE,
96
+ });
97
+
98
+ assert.ok(result.ok);
99
+ assert.equal(result.value.icalData, RESOURCE);
100
+ assert.equal(
101
+ (await objects())[0]?.icalData,
102
+ RESOURCE,
103
+ "the stored row holds the input bytes, CRLF and all",
104
+ );
105
+ assert.equal(result.value.syncSequence, 1);
106
+ assert.equal((await objects()).length, 1);
107
+ assert.equal(
108
+ (await occurrences()).filter(
109
+ (row) => row.calendarObjectId === result.value.calendarObjectId,
110
+ ).length,
111
+ 3,
112
+ );
113
+ });
114
+
115
+ test("a failing write in the batch leaves no object behind", async () => {
116
+ const before = await objects();
117
+
118
+ await assert.rejects(
119
+ putCalendarObject(new FailingLastWriteStore(store), {
120
+ accountConfigId: ACCOUNT_CONFIG_ID,
121
+ calendarId,
122
+ resourceName: "doomed.ics",
123
+ icalData: RESOURCE,
124
+ }),
125
+ /NOT NULL constraint failed/,
126
+ );
127
+
128
+ const after = await objects();
129
+ assert.equal(after.length, before.length);
130
+ assert.equal(
131
+ after.some((row) => row.resourceName === "doomed.ics"),
132
+ false,
133
+ );
134
+ });
135
+
136
+ test("a failing write in the batch leaves the collection's sequence where it was", async () => {
137
+ const [before] = await db.select().from(calendarTable);
138
+
139
+ await assert.rejects(
140
+ putCalendarObject(new FailingLastWriteStore(store), {
141
+ accountConfigId: ACCOUNT_CONFIG_ID,
142
+ calendarId,
143
+ resourceName: "doomed-again.ics",
144
+ icalData: RESOURCE,
145
+ }),
146
+ /NOT NULL constraint failed/,
147
+ );
148
+
149
+ const [after] = await db.select().from(calendarTable);
150
+ assert.equal(after?.syncSequence, before?.syncSequence);
151
+ });
152
+
153
+ test("a refused resource writes nothing at all", async () => {
154
+ const before = await objects();
155
+ const [collectionBefore] = await db.select().from(calendarTable);
156
+
157
+ const result = await putCalendarObject(store, {
158
+ accountConfigId: ACCOUNT_CONFIG_ID,
159
+ calendarId,
160
+ resourceName: "backwards.ics",
161
+ icalData: [
162
+ "BEGIN:VCALENDAR",
163
+ "VERSION:2.0",
164
+ "BEGIN:VEVENT",
165
+ "UID:backwards@example.com",
166
+ "DTSTART:20260826T100000Z",
167
+ "DTEND:20260826T090000Z",
168
+ "END:VEVENT",
169
+ "END:VCALENDAR",
170
+ "",
171
+ ].join("\r\n"),
172
+ });
173
+
174
+ assert.ok(!result.ok);
175
+ assert.equal(result.error.code, "BackwardsEnd");
176
+ assert.equal((await objects()).length, before.length);
177
+ const [collectionAfter] = await db.select().from(calendarTable);
178
+ assert.equal(
179
+ collectionAfter?.syncSequence,
180
+ collectionBefore?.syncSequence,
181
+ );
182
+ });
183
+
184
+ test("rewriting a resource replaces its occurrences rather than adding to them", async () => {
185
+ const first = await putCalendarObject(store, {
186
+ accountConfigId: ACCOUNT_CONFIG_ID,
187
+ calendarId,
188
+ resourceName: "shrinking.ics",
189
+ icalData: RESOURCE,
190
+ });
191
+ assert.ok(first.ok);
192
+
193
+ const second = await putCalendarObject(store, {
194
+ accountConfigId: ACCOUNT_CONFIG_ID,
195
+ calendarId,
196
+ resourceName: "shrinking.ics",
197
+ icalData: RESOURCE.replace("COUNT=3", "COUNT=1"),
198
+ });
199
+ assert.ok(second.ok);
200
+
201
+ assert.equal(second.value.calendarObjectId, first.value.calendarObjectId);
202
+ assert.equal(
203
+ (await occurrences()).filter(
204
+ (row) => row.calendarObjectId === second.value.calendarObjectId,
205
+ ).length,
206
+ 1,
207
+ );
208
+ });
209
+ });
210
+ };
@@ -2,56 +2,67 @@ import type {
2
2
  CalendarRepositories,
3
3
  CalendarWrite,
4
4
  ICalendarStore,
5
+ ICalendarSuggestionRepository,
6
+ SettleCalendarSuggestionWrite,
5
7
  } from "@remit/data-ports";
6
8
  import { deriveCalendarObjectId } from "@remit/data-ports/id";
7
9
  import { and, eq, exists, sql } from "drizzle-orm";
8
- import { runBatch, type SqliteBatchItem } from "../batch.js";
9
- import type { Db } from "../db.js";
10
+ import type { SqliteBatchItem, WriteBatch } from "../batch.js";
11
+ import type { RepositoryDb, SqliteStore } from "../db.js";
10
12
  import {
11
13
  calendarEventIndexTable,
12
14
  calendarFeedTokenTable,
13
15
  calendarObjectTable,
14
- calendarSuggestionTable,
15
16
  calendarTable,
16
17
  } from "../schema.js";
17
18
  import { CalendarCollectionRepo } from "./calendar-collection.js";
18
19
  import { CalendarEventIndexRepo } from "./calendar-event-index.js";
19
20
  import { CalendarFeedTokenRepo } from "./calendar-feed-token.js";
20
21
  import { CalendarObjectRepo } from "./calendar-object.js";
21
- import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
22
22
 
23
- type DB = Db<Record<string, unknown>>;
23
+ type DB = RepositoryDb<Record<string, unknown>>;
24
+
25
+ type CalendarBatchWrite = Exclude<CalendarWrite, SettleCalendarSuggestionWrite>;
26
+
27
+ const isSettlement = (
28
+ write: CalendarWrite,
29
+ ): write is SettleCalendarSuggestionWrite => write.kind === "SettleSuggestion";
30
+
31
+ const isBatchWrite = (write: CalendarWrite): write is CalendarBatchWrite =>
32
+ !isSettlement(write);
24
33
 
25
34
  export class DrizzleCalendarStore implements ICalendarStore {
26
35
  readonly repos: CalendarRepositories;
27
36
 
37
+ private readonly db: DB;
38
+ private readonly batch: WriteBatch;
39
+
28
40
  constructor(
29
- private db: DB,
30
- private mail: DB,
41
+ store: SqliteStore<Record<string, unknown>>,
42
+ calendarSuggestion: ICalendarSuggestionRepository,
31
43
  ) {
44
+ this.db = store.db;
45
+ this.batch = store.batch;
32
46
  this.repos = {
33
- calendarCollection: new CalendarCollectionRepo(db),
34
- calendarObject: new CalendarObjectRepo(db),
35
- calendarEventIndex: new CalendarEventIndexRepo(db),
36
- calendarSuggestion: new CalendarSuggestionRepo(mail),
37
- calendarFeedToken: new CalendarFeedTokenRepo(db),
47
+ calendarCollection: new CalendarCollectionRepo(store.db),
48
+ calendarObject: new CalendarObjectRepo(store.db),
49
+ calendarEventIndex: new CalendarEventIndexRepo(store.db),
50
+ calendarSuggestion,
51
+ calendarFeedToken: new CalendarFeedTokenRepo(store.db),
38
52
  };
39
53
  }
40
54
 
41
55
  async commit(writes: readonly CalendarWrite[]): Promise<void> {
42
- const settlements = writes.filter(
43
- (write) => write.kind === "SettleSuggestion",
44
- );
45
- await runBatch(
46
- this.db,
47
- writes
48
- .filter((write) => write.kind !== "SettleSuggestion")
49
- .flatMap((write) => this.statements(write)),
50
- );
51
- await runBatch(
52
- this.mail,
53
- settlements.flatMap((write) => this.statements(write)),
56
+ await this.batch(
57
+ writes.filter(isBatchWrite).flatMap((write) => this.statements(write)),
54
58
  );
59
+ for (const write of writes.filter(isSettlement)) {
60
+ await this.repos.calendarSuggestion.settle(
61
+ write.accountConfigId,
62
+ write.suggestionId,
63
+ write.settle,
64
+ );
65
+ }
55
66
  }
56
67
 
57
68
  private collection(accountConfigId: string, calendarId: string) {
@@ -97,7 +108,7 @@ export class DrizzleCalendarStore implements ICalendarStore {
97
108
  );
98
109
  }
99
110
 
100
- private statements(write: CalendarWrite): SqliteBatchItem[] {
111
+ private statements(write: CalendarBatchWrite): SqliteBatchItem[] {
101
112
  const now = Date.now();
102
113
  switch (write.kind) {
103
114
  case "PutObject": {
@@ -223,21 +234,6 @@ export class DrizzleCalendarStore implements ICalendarStore {
223
234
  ),
224
235
  this.bump(write.accountConfigId, write.calendarId),
225
236
  ];
226
- case "SettleSuggestion":
227
- return [
228
- this.mail
229
- .update(calendarSuggestionTable)
230
- .set({ ...write.settle, updatedAt: now })
231
- .where(
232
- and(
233
- eq(
234
- calendarSuggestionTable.accountConfigId,
235
- write.accountConfigId,
236
- ),
237
- eq(calendarSuggestionTable.suggestionId, write.suggestionId),
238
- ),
239
- ),
240
- ];
241
237
  case "UpdateCollection":
242
238
  return [
243
239
  this.db
@@ -8,13 +8,13 @@ import type {
8
8
  import { deriveCalendarSuggestionId } from "@remit/data-ports/id";
9
9
  import { CalendarSuggestionState } from "@remit/domain-enums";
10
10
  import { and, asc, desc, eq, gte, lt, or } from "drizzle-orm";
11
- import type { Db } from "../db.js";
11
+ import type { RepositoryDb } from "../db.js";
12
12
  import { NotFoundError } from "../error.js";
13
13
  import { decodeToken, resultList } from "../pagination.js";
14
14
  import { calendarSuggestionTable } from "../schema.js";
15
15
  import { inList } from "./in-list.js";
16
16
 
17
- type DB = Db<Record<string, unknown>>;
17
+ type DB = RepositoryDb<Record<string, unknown>>;
18
18
 
19
19
  function rowToCalendarSuggestion(
20
20
  row: typeof calendarSuggestionTable.$inferSelect,
@@ -2,6 +2,7 @@ import assert from "node:assert";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import { MailboxCursorState } from "@remit/domain-enums";
4
4
  import { eq } from "drizzle-orm";
5
+ import { syncStores } from "../databases.js";
5
6
  import {
6
7
  accountSettingTable,
7
8
  accountTable,
@@ -299,7 +300,7 @@ describe("runDrizzleCascadeDelete", () => {
299
300
 
300
301
  test("deletes every enumerated row across all tables", async () => {
301
302
  await runDrizzleCascadeDelete(
302
- { app: db, mail: db, search: db } as never,
303
+ syncStores({ app: db, mail: db, search: db } as never),
303
304
  entities,
304
305
  log,
305
306
  );
@@ -438,7 +439,7 @@ describe("runDrizzleCascadeDelete", () => {
438
439
  await assert.rejects(
439
440
  () =>
440
441
  runDrizzleCascadeDelete(
441
- { app: db, mail: db, search: db } as never,
442
+ syncStores({ app: db, mail: db, search: db } as never),
442
443
  [{ entityType: "Nonsense", key: { id: "x" } }],
443
444
  log,
444
445
  ),
@@ -1,8 +1,9 @@
1
- import { runBatch, type SqliteBatchItem } from "../batch.js";
1
+ import type { SqliteBatchItem } from "../batch.js";
2
2
  import {
3
3
  openSqliteDatabases,
4
4
  type SqliteDatabasePaths,
5
- type SqliteDatabases,
5
+ type SqliteStores,
6
+ syncStores,
6
7
  } from "../databases.js";
7
8
  import { accountTable } from "../schema/i4-account-config.js";
8
9
  import { accountSettingTable } from "../schema/i4-account-setting.js";
@@ -76,16 +77,18 @@ const groupByType = (
76
77
  * first, then the app rows, then the search documents of the deleted thread
77
78
  * messages. Every statement is keyed, so a rerun after a failure between two
78
79
  * batches deletes what is left. Message subtrees
79
- * (message + nine child tables) are deleted set-wise by message id; the remaining
80
+ * (message + ten child tables) are deleted set-wise by message id; the remaining
80
81
  * entity types are deleted by their enumerated primary keys. AccountConfig is
81
82
  * never deleted here — the caller removes it last through its repository, the
82
83
  * cascade-in-progress marker.
83
84
  */
84
85
  export const runDrizzleCascadeDelete = async (
85
- { app, mail, search }: SqliteDatabases,
86
+ stores: SqliteStores,
86
87
  entities: CascadeEntity[],
87
88
  log: CascadeDeleteLogger,
88
89
  ): Promise<void> => {
90
+ const app = stores.app.db;
91
+ const mail = stores.mail.db;
89
92
  const grouped = groupByType(entities);
90
93
 
91
94
  for (const entityType of grouped.keys()) {
@@ -217,9 +220,9 @@ export const runDrizzleCascadeDelete = async (
217
220
  ),
218
221
  );
219
222
  }
220
- await runBatch(mail, items);
221
- await runBatch(app, appItems);
222
- await new SearchIndex(mail, search).sync(threadMessageIds);
223
+ await stores.mail.batch(items);
224
+ await stores.app.batch(appItems);
225
+ await new SearchIndex(mail, stores.search).sync(threadMessageIds);
223
226
 
224
227
  log.info({ entityCount: entities.length }, "Drizzle cascade delete complete");
225
228
  };
@@ -238,5 +241,6 @@ export const createSqliteCascadeDeleter = async (
238
241
  paths: SqliteDatabasePaths,
239
242
  ): Promise<CascadeDeleter> => {
240
243
  const databases = await openSqliteDatabases(paths);
241
- return (entities, log) => runDrizzleCascadeDelete(databases, entities, log);
244
+ const stores = syncStores(databases);
245
+ return (entities, log) => runDrizzleCascadeDelete(stores, entities, log);
242
246
  };
@@ -4,8 +4,9 @@ import {
4
4
  type MessageDataSchema,
5
5
  messageDataSchema,
6
6
  } from "../schema/message-data.js";
7
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
7
8
  import { createSqliteTestDb, type SqliteTestDb } from "../test-db-sqlite.js";
8
- import { DrizzleEnvelopeRepository } from "./envelope.js";
9
+ import type { DrizzleEnvelopeRepository } from "./envelope.js";
9
10
 
10
11
  // Envelope repo on sqlite (RFC 036 D1): the onConflictDoUpdate upsert path and
11
12
  // the transactional body-part upsert both run on better-sqlite3 unchanged.
@@ -19,7 +20,7 @@ describe("DrizzleEnvelopeRepository (sqlite)", () => {
19
20
 
20
21
  before(async () => {
21
22
  ({ db, close } = await createSqliteTestDb(messageDataSchema));
22
- repo = new DrizzleEnvelopeRepository(db);
23
+ repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
23
24
  });
24
25
 
25
26
  after(async () => {
@@ -2,7 +2,8 @@ import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import type { BodyPartUpsertInput } from "@remit/data-ports";
4
4
  import { envelopeId as deriveEnvelopeId } from "../id.js";
5
- import { DrizzleEnvelopeRepository } from "./envelope.js";
5
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
6
+ import type { DrizzleEnvelopeRepository } from "./envelope.js";
6
7
  import { createTestDb, type TestDb } from "./test-helpers.js";
7
8
 
8
9
  describe("DrizzleEnvelopeRepository", () => {
@@ -18,7 +19,7 @@ describe("DrizzleEnvelopeRepository", () => {
18
19
 
19
20
  before(async () => {
20
21
  ({ db, stop } = await createTestDb());
21
- repo = new DrizzleEnvelopeRepository(db);
22
+ repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
22
23
 
23
24
  await repo.upsertEnvelope({
24
25
  envelopeId: ENVELOPE_ID,