@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.
- package/package.json +4 -3
- package/src/batch.sqlite.test.ts +4 -4
- package/src/batch.ts +47 -21
- package/src/databases.ts +24 -6
- package/src/db.ts +20 -5
- package/src/index.ts +12 -0
- package/src/repair/junk-only-address.sqlite.test.ts +7 -2
- package/src/repos/account.conformance.d1.test.ts +20 -0
- package/src/repos/account.conformance.sqlite.test.ts +6 -2
- package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-collection.ts +2 -2
- package/src/repos/calendar-default.d1.test.ts +4 -0
- package/src/repos/calendar-default.sqlite.test.ts +3 -72
- package/src/repos/calendar-default.suite.ts +68 -0
- package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-event-index.ts +2 -2
- package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-feed-token.ts +2 -2
- package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-object.ts +2 -2
- package/src/repos/calendar-put.d1.test.ts +4 -0
- package/src/repos/calendar-put.sqlite.test.ts +3 -205
- package/src/repos/calendar-put.suite.ts +210 -0
- package/src/repos/calendar-store.ts +36 -40
- package/src/repos/calendar-suggestion.ts +2 -2
- package/src/repos/cascade-delete.test.ts +3 -2
- package/src/repos/cascade-delete.ts +12 -8
- package/src/repos/envelope.sqlite.test.ts +3 -2
- package/src/repos/envelope.test.ts +3 -2
- package/src/repos/envelope.ts +14 -9
- package/src/repos/filter.d1.test.ts +4 -0
- package/src/repos/filter.suite.ts +476 -0
- package/src/repos/filter.test.ts +3 -472
- package/src/repos/filter.ts +2 -2
- package/src/repos/i4-account-config.d1.test.ts +4 -0
- package/src/repos/i4-account-config.suite.ts +131 -0
- package/src/repos/i4-account-config.test.ts +3 -123
- package/src/repos/i4-account-config.ts +8 -10
- package/src/repos/i4-account-export-request.d1.test.ts +4 -0
- package/src/repos/i4-account-export-request.suite.ts +44 -0
- package/src/repos/i4-account-export-request.test.ts +3 -40
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.d1.test.ts +4 -0
- package/src/repos/i4-account-setting.suite.ts +100 -0
- package/src/repos/i4-account-setting.test.ts +3 -93
- package/src/repos/i4-account-setting.ts +2 -2
- package/src/repos/i4-account.test.ts +12 -8
- package/src/repos/i4-account.ts +6 -10
- package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
- package/src/repos/i4-address.ts +12 -2
- package/src/repos/i4-config-import.ts +2 -2
- package/src/repos/i4-mailbox-lock.ts +2 -2
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox-special-use.ts +29 -40
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
- package/src/repos/i4-mailbox.test.ts +15 -14
- package/src/repos/i4-mailbox.ts +13 -7
- package/src/repos/i4-message-flag-push.ts +2 -2
- package/src/repos/i4-message-placement-move.ts +2 -2
- package/src/repos/i4-organize-job-request.ts +2 -2
- package/src/repos/i4-outbox-attachment.test.ts +7 -2
- package/src/repos/i4-outbox-attachment.ts +11 -5
- package/src/repos/i4-outbox-message.ts +2 -2
- package/src/repos/i4-sender-signer-standing.ts +2 -2
- package/src/repos/label.conformance.d1.test.ts +20 -0
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +3 -6
- package/src/repos/message-label.ts +2 -2
- package/src/repos/message.sqlite.test.ts +57 -3
- package/src/repos/message.test.ts +6 -5
- package/src/repos/message.ts +81 -10
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/sync-warning.ts +2 -2
- package/src/repos/thread-message-body-muted.sqlite.test.ts +30 -22
- package/src/repos/thread-message-field-terms.sqlite.test.ts +16 -11
- package/src/repos/thread-message.sqlite.test.ts +13 -7
- package/src/repos/thread-message.test.ts +43 -27
- package/src/repos/thread-message.ts +26 -14
- package/src/repos/thread-search-predicates.ts +36 -63
- package/src/schema/message-data.ts +3 -0
- package/src/schema-sets/mail.ts +1 -0
- package/src/search/thread-message-search.sqlite.test.ts +57 -11
- package/src/search/thread-message-search.ts +26 -12
- package/src/sqlite-repositories.ts +48 -32
- package/src/test-d1.ts +40 -0
- package/src/test-db-sqlite.ts +1 -1
- package/src/test-shipped-sqlite-schema.ts +8 -6
- package/src/test-stores.ts +30 -0
|
@@ -1,206 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
9
|
-
import type {
|
|
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 =
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
(write) => write
|
|
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:
|
|
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 {
|
|
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 =
|
|
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 {
|
|
1
|
+
import type { SqliteBatchItem } from "../batch.js";
|
|
2
2
|
import {
|
|
3
3
|
openSqliteDatabases,
|
|
4
4
|
type SqliteDatabasePaths,
|
|
5
|
-
type
|
|
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 +
|
|
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
|
-
|
|
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
|
|
221
|
-
await
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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 =
|
|
22
|
+
repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
|
|
22
23
|
|
|
23
24
|
await repo.upsertEnvelope({
|
|
24
25
|
envelopeId: ENVELOPE_ID,
|