@remit/drizzle-service 0.0.103 → 0.0.105

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 (66) hide show
  1. package/package.json +4 -3
  2. package/src/bound-parameters.test.ts +57 -0
  3. package/src/bound-parameters.ts +49 -0
  4. package/src/databases.ts +10 -9
  5. package/src/db.ts +1 -0
  6. package/src/index.ts +4 -0
  7. package/src/repair/junk-only-address.sqlite.test.ts +9 -5
  8. package/src/repos/account.conformance.d1.test.ts +20 -0
  9. package/src/repos/account.conformance.sqlite.test.ts +9 -5
  10. package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
  11. package/src/repos/calendar-default.d1.test.ts +4 -0
  12. package/src/repos/calendar-default.sqlite.test.ts +3 -76
  13. package/src/repos/calendar-default.suite.ts +68 -0
  14. package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
  15. package/src/repos/calendar-event-index.conformance.sqlite.test.ts +3 -1
  16. package/src/repos/calendar-event-index.ts +36 -21
  17. package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
  18. package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
  19. package/src/repos/calendar-put.d1.test.ts +4 -0
  20. package/src/repos/calendar-put.sqlite.test.ts +3 -209
  21. package/src/repos/calendar-put.suite.ts +210 -0
  22. package/src/repos/calendar-store.ts +17 -10
  23. package/src/repos/cascade-delete.test.ts +9 -2
  24. package/src/repos/cascade-delete.ts +3 -2
  25. package/src/repos/envelope.d1.test.ts +66 -0
  26. package/src/repos/envelope.sqlite.test.ts +5 -1
  27. package/src/repos/envelope.test.ts +5 -1
  28. package/src/repos/envelope.ts +40 -31
  29. package/src/repos/filter.d1.test.ts +4 -0
  30. package/src/repos/filter.suite.ts +476 -0
  31. package/src/repos/filter.test.ts +3 -472
  32. package/src/repos/i4-account-config.d1.test.ts +4 -0
  33. package/src/repos/i4-account-config.suite.ts +131 -0
  34. package/src/repos/i4-account-config.test.ts +3 -127
  35. package/src/repos/i4-account-export-request.d1.test.ts +4 -0
  36. package/src/repos/i4-account-export-request.suite.ts +44 -0
  37. package/src/repos/i4-account-export-request.test.ts +3 -40
  38. package/src/repos/i4-account-setting.d1.test.ts +4 -0
  39. package/src/repos/i4-account-setting.suite.ts +100 -0
  40. package/src/repos/i4-account-setting.test.ts +3 -93
  41. package/src/repos/i4-account.test.ts +17 -10
  42. package/src/repos/i4-address-junk-move.sqlite.test.ts +9 -5
  43. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +9 -5
  44. package/src/repos/i4-mailbox.sqlite.test.ts +17 -10
  45. package/src/repos/i4-mailbox.test.ts +17 -10
  46. package/src/repos/i4-outbox-attachment.test.ts +9 -5
  47. package/src/repos/label.conformance.d1.test.ts +20 -0
  48. package/src/repos/message.sqlite.test.ts +59 -1
  49. package/src/repos/message.test.ts +5 -1
  50. package/src/repos/message.ts +66 -0
  51. package/src/repos/sync-warning.ts +35 -22
  52. package/src/repos/thread-message-body-muted.sqlite.test.ts +32 -24
  53. package/src/repos/thread-message-field-terms.sqlite.test.ts +18 -10
  54. package/src/repos/thread-message.sqlite.test.ts +15 -9
  55. package/src/repos/thread-message.test.ts +54 -29
  56. package/src/repos/thread-message.ts +13 -9
  57. package/src/repos/thread-search-predicates.ts +36 -63
  58. package/src/schema/message-data.ts +3 -0
  59. package/src/schema-sets/mail.ts +1 -0
  60. package/src/search/thread-message-search.sqlite.test.ts +62 -14
  61. package/src/search/thread-message-search.ts +27 -21
  62. package/src/sqlite-repositories.ts +6 -3
  63. package/src/test-d1.ts +57 -0
  64. package/src/test-db-sqlite.ts +1 -1
  65. package/src/test-shipped-sqlite-schema.ts +8 -6
  66. package/src/test-stores.ts +43 -0
@@ -1,210 +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 { createSqliteRepositories } from "../sqlite-repositories.js";
20
- import { createSqliteTestDb } from "../test-db-sqlite.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 = createSqliteRepositories({
81
- app: db,
82
- mail: db,
83
- search: db,
84
- }).calendarStore;
85
- const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
86
- calendarId = collection.calendarId;
87
- });
88
-
89
- after(() => close());
90
-
91
- const objects = () => db.select().from(calendarObjectTable);
92
- const occurrences = () => db.select().from(calendarEventIndexTable);
93
-
94
- test("writes the object, its occurrences and the sequence bump together", async () => {
95
- const result = await putCalendarObject(store, {
96
- accountConfigId: ACCOUNT_CONFIG_ID,
97
- calendarId,
98
- resourceName: "review.ics",
99
- icalData: RESOURCE,
100
- });
101
-
102
- assert.ok(result.ok);
103
- assert.equal(result.value.icalData, RESOURCE);
104
- assert.equal(
105
- (await objects())[0]?.icalData,
106
- RESOURCE,
107
- "the stored row holds the input bytes, CRLF and all",
108
- );
109
- assert.equal(result.value.syncSequence, 1);
110
- assert.equal((await objects()).length, 1);
111
- assert.equal(
112
- (await occurrences()).filter(
113
- (row) => row.calendarObjectId === result.value.calendarObjectId,
114
- ).length,
115
- 3,
116
- );
117
- });
118
-
119
- test("a failing write in the batch leaves no object behind", async () => {
120
- const before = await objects();
121
-
122
- await assert.rejects(
123
- putCalendarObject(new FailingLastWriteStore(store), {
124
- accountConfigId: ACCOUNT_CONFIG_ID,
125
- calendarId,
126
- resourceName: "doomed.ics",
127
- icalData: RESOURCE,
128
- }),
129
- /NOT NULL constraint failed/,
130
- );
131
-
132
- const after = await objects();
133
- assert.equal(after.length, before.length);
134
- assert.equal(
135
- after.some((row) => row.resourceName === "doomed.ics"),
136
- false,
137
- );
138
- });
139
-
140
- test("a failing write in the batch leaves the collection's sequence where it was", async () => {
141
- const [before] = await db.select().from(calendarTable);
142
-
143
- await assert.rejects(
144
- putCalendarObject(new FailingLastWriteStore(store), {
145
- accountConfigId: ACCOUNT_CONFIG_ID,
146
- calendarId,
147
- resourceName: "doomed-again.ics",
148
- icalData: RESOURCE,
149
- }),
150
- /NOT NULL constraint failed/,
151
- );
152
-
153
- const [after] = await db.select().from(calendarTable);
154
- assert.equal(after?.syncSequence, before?.syncSequence);
155
- });
156
-
157
- test("a refused resource writes nothing at all", async () => {
158
- const before = await objects();
159
- const [collectionBefore] = await db.select().from(calendarTable);
160
-
161
- const result = await putCalendarObject(store, {
162
- accountConfigId: ACCOUNT_CONFIG_ID,
163
- calendarId,
164
- resourceName: "backwards.ics",
165
- icalData: [
166
- "BEGIN:VCALENDAR",
167
- "VERSION:2.0",
168
- "BEGIN:VEVENT",
169
- "UID:backwards@example.com",
170
- "DTSTART:20260826T100000Z",
171
- "DTEND:20260826T090000Z",
172
- "END:VEVENT",
173
- "END:VCALENDAR",
174
- "",
175
- ].join("\r\n"),
176
- });
177
-
178
- assert.ok(!result.ok);
179
- assert.equal(result.error.code, "BackwardsEnd");
180
- assert.equal((await objects()).length, before.length);
181
- const [collectionAfter] = await db.select().from(calendarTable);
182
- assert.equal(collectionAfter?.syncSequence, collectionBefore?.syncSequence);
183
- });
184
-
185
- test("rewriting a resource replaces its occurrences rather than adding to them", async () => {
186
- const first = await putCalendarObject(store, {
187
- accountConfigId: ACCOUNT_CONFIG_ID,
188
- calendarId,
189
- resourceName: "shrinking.ics",
190
- icalData: RESOURCE,
191
- });
192
- assert.ok(first.ok);
193
-
194
- const second = await putCalendarObject(store, {
195
- accountConfigId: ACCOUNT_CONFIG_ID,
196
- calendarId,
197
- resourceName: "shrinking.ics",
198
- icalData: RESOURCE.replace("COUNT=3", "COUNT=1"),
199
- });
200
- assert.ok(second.ok);
201
-
202
- assert.equal(second.value.calendarObjectId, first.value.calendarObjectId);
203
- assert.equal(
204
- (await occurrences()).filter(
205
- (row) => row.calendarObjectId === second.value.calendarObjectId,
206
- ).length,
207
- 1,
208
- );
209
- });
210
- });
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
+ };
@@ -8,6 +8,7 @@ import type {
8
8
  import { deriveCalendarObjectId } from "@remit/data-ports/id";
9
9
  import { and, eq, exists, sql } from "drizzle-orm";
10
10
  import type { SqliteBatchItem, WriteBatch } from "../batch.js";
11
+ import { chunkRows } from "../bound-parameters.js";
11
12
  import type { RepositoryDb, SqliteStore } from "../db.js";
12
13
  import {
13
14
  calendarEventIndexTable,
@@ -36,6 +37,7 @@ export class DrizzleCalendarStore implements ICalendarStore {
36
37
 
37
38
  private readonly db: DB;
38
39
  private readonly batch: WriteBatch;
40
+ private readonly maxBoundParameters: number;
39
41
 
40
42
  constructor(
41
43
  store: SqliteStore<Record<string, unknown>>,
@@ -43,10 +45,11 @@ export class DrizzleCalendarStore implements ICalendarStore {
43
45
  ) {
44
46
  this.db = store.db;
45
47
  this.batch = store.batch;
48
+ this.maxBoundParameters = store.maxBoundParameters;
46
49
  this.repos = {
47
50
  calendarCollection: new CalendarCollectionRepo(store.db),
48
51
  calendarObject: new CalendarObjectRepo(store.db),
49
- calendarEventIndex: new CalendarEventIndexRepo(store.db),
52
+ calendarEventIndex: new CalendarEventIndexRepo(store),
50
53
  calendarSuggestion,
51
54
  calendarFeedToken: new CalendarFeedTokenRepo(store.db),
52
55
  };
@@ -138,17 +141,21 @@ export class DrizzleCalendarStore implements ICalendarStore {
138
141
  }),
139
142
  this.clearOccurrences(calendarId, calendarObjectId),
140
143
  ];
141
- if (write.occurrences.length === 0) return items;
144
+ const occurrences = write.occurrences.map((occurrence) => ({
145
+ ...occurrence,
146
+ calendarId,
147
+ calendarObjectId,
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ }));
142
151
  return [
143
152
  ...items,
144
- this.db.insert(calendarEventIndexTable).values(
145
- write.occurrences.map((occurrence) => ({
146
- ...occurrence,
147
- calendarId,
148
- calendarObjectId,
149
- createdAt: now,
150
- updatedAt: now,
151
- })),
153
+ ...chunkRows(
154
+ calendarEventIndexTable,
155
+ occurrences,
156
+ this.maxBoundParameters,
157
+ ).map((slice) =>
158
+ this.db.insert(calendarEventIndexTable).values(slice),
152
159
  ),
153
160
  ];
154
161
  }
@@ -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 { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
5
6
  import { syncStores } from "../databases.js";
6
7
  import {
7
8
  accountSettingTable,
@@ -300,7 +301,10 @@ describe("runDrizzleCascadeDelete", () => {
300
301
 
301
302
  test("deletes every enumerated row across all tables", async () => {
302
303
  await runDrizzleCascadeDelete(
303
- syncStores({ app: db, mail: db, search: db } as never),
304
+ syncStores(
305
+ { app: db, mail: db, search: db } as never,
306
+ BETTER_SQLITE3_LIMITS,
307
+ ),
304
308
  entities,
305
309
  log,
306
310
  );
@@ -439,7 +443,10 @@ describe("runDrizzleCascadeDelete", () => {
439
443
  await assert.rejects(
440
444
  () =>
441
445
  runDrizzleCascadeDelete(
442
- syncStores({ app: db, mail: db, search: db } as never),
446
+ syncStores(
447
+ { app: db, mail: db, search: db } as never,
448
+ BETTER_SQLITE3_LIMITS,
449
+ ),
443
450
  [{ entityType: "Nonsense", key: { id: "x" } }],
444
451
  log,
445
452
  ),
@@ -1,4 +1,5 @@
1
1
  import type { SqliteBatchItem } from "../batch.js";
2
+ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
2
3
  import {
3
4
  openSqliteDatabases,
4
5
  type SqliteDatabasePaths,
@@ -77,7 +78,7 @@ const groupByType = (
77
78
  * first, then the app rows, then the search documents of the deleted thread
78
79
  * messages. Every statement is keyed, so a rerun after a failure between two
79
80
  * batches deletes what is left. Message subtrees
80
- * (message + nine child tables) are deleted set-wise by message id; the remaining
81
+ * (message + ten child tables) are deleted set-wise by message id; the remaining
81
82
  * entity types are deleted by their enumerated primary keys. AccountConfig is
82
83
  * never deleted here — the caller removes it last through its repository, the
83
84
  * cascade-in-progress marker.
@@ -241,6 +242,6 @@ export const createSqliteCascadeDeleter = async (
241
242
  paths: SqliteDatabasePaths,
242
243
  ): Promise<CascadeDeleter> => {
243
244
  const databases = await openSqliteDatabases(paths);
244
- const stores = syncStores(databases);
245
+ const stores = syncStores(databases, BETTER_SQLITE3_LIMITS);
245
246
  return (entities, log) => runDrizzleCascadeDelete(stores, entities, log);
246
247
  };
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import type { BodyPartUpsertInput } from "@remit/data-ports";
4
+ import { ReferenceType } from "@remit/domain-enums";
5
+ import { openD1TestStore } from "../test-d1.js";
6
+ import { DrizzleEnvelopeRepository } from "./envelope.js";
7
+
8
+ const MESSAGE_ID = "00000000-0000-0000-4444-000000000d01";
9
+
10
+ const bodyPart = (index: number): BodyPartUpsertInput => ({
11
+ partPath: `${index + 1}`,
12
+ parentPartPath: null,
13
+ mediaType: "TEXT",
14
+ mediaSubtype: "plain",
15
+ transferEncoding: "7BIT",
16
+ sizeOctets: 128,
17
+ isMultipart: false,
18
+ parameters: [{ parameterName: "charset", parameterValue: "utf-8" }],
19
+ });
20
+
21
+ describe("DrizzleEnvelopeRepository on D1's 100 bound parameter limit", () => {
22
+ let repo: DrizzleEnvelopeRepository;
23
+ let close: () => Promise<void>;
24
+
25
+ before(async () => {
26
+ const opened = await openD1TestStore("mail");
27
+ close = opened.close;
28
+ repo = new DrizzleEnvelopeRepository(opened.store);
29
+ await repo.upsertEnvelope({
30
+ envelopeId: "ignored-derived",
31
+ messageId: MESSAGE_ID,
32
+ dateValue: 1700000000000,
33
+ dateRaw: "Mon, 14 Nov 2023 08:00:00 +0000",
34
+ subject: "Thirty parts",
35
+ });
36
+ });
37
+
38
+ after(async () => {
39
+ await close();
40
+ });
41
+
42
+ test("upsertBodyParts writes thirty parts and their parameters", async () => {
43
+ await repo.upsertBodyParts(
44
+ MESSAGE_ID,
45
+ Array.from({ length: 30 }, (_, index) => bodyPart(index)),
46
+ );
47
+
48
+ const data = await repo.getMessageData(MESSAGE_ID);
49
+ assert.equal(data.bodyPart.length, 30);
50
+ assert.equal(data.bodyPartParameter.length, 30);
51
+ });
52
+
53
+ test("replaceMessageReferences writes thirty references", async () => {
54
+ await repo.replaceMessageReferences(
55
+ MESSAGE_ID,
56
+ Array.from({ length: 30 }, (_, index) => ({
57
+ messageIdValue: `<ref-${index}@example.com>`,
58
+ referenceType: ReferenceType.References,
59
+ referenceOrder: index,
60
+ })),
61
+ );
62
+
63
+ const data = await repo.getMessageData(MESSAGE_ID);
64
+ assert.equal(data.messageReference.length, 30);
65
+ });
66
+ });
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
+ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
3
4
  import {
4
5
  type MessageDataSchema,
5
6
  messageDataSchema,
@@ -20,7 +21,10 @@ describe("DrizzleEnvelopeRepository (sqlite)", () => {
20
21
 
21
22
  before(async () => {
22
23
  ({ db, close } = await createSqliteTestDb(messageDataSchema));
23
- repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
24
+ repo = createSqliteRepositories(
25
+ { app: db, mail: db, search: db },
26
+ BETTER_SQLITE3_LIMITS,
27
+ ).envelope;
24
28
  });
25
29
 
26
30
  after(async () => {
@@ -1,6 +1,7 @@
1
1
  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
+ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
4
5
  import { envelopeId as deriveEnvelopeId } from "../id.js";
5
6
  import { createSqliteRepositories } from "../sqlite-repositories.js";
6
7
  import type { DrizzleEnvelopeRepository } from "./envelope.js";
@@ -19,7 +20,10 @@ describe("DrizzleEnvelopeRepository", () => {
19
20
 
20
21
  before(async () => {
21
22
  ({ db, stop } = await createTestDb());
22
- repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
23
+ repo = createSqliteRepositories(
24
+ { app: db, mail: db, search: db },
25
+ BETTER_SQLITE3_LIMITS,
26
+ ).envelope;
23
27
 
24
28
  await repo.upsertEnvelope({
25
29
  envelopeId: ENVELOPE_ID,