@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.
- package/package.json +1 -1
- package/src/batch.sqlite.test.ts +78 -0
- package/src/batch.ts +45 -0
- package/src/databases.ts +70 -0
- package/src/db.ts +1 -4
- package/src/generated-enum.test.ts +8 -0
- package/src/generated-id.test.ts +19 -0
- package/src/id.test.ts +0 -8
- package/src/id.ts +0 -6
- package/src/index.ts +16 -6
- package/src/repair/address-display-name.sqlite.test.ts +3 -5
- package/src/repair/address-display-name.ts +8 -3
- package/src/repair/junk-only-address.sqlite.test.ts +8 -36
- package/src/repair/stranded-sent-outbox.sqlite.test.ts +2 -9
- package/src/repos/account.conformance.sqlite.test.ts +1 -1
- package/src/repos/calendar-default.sqlite.test.ts +6 -6
- package/src/repos/calendar-put.sqlite.test.ts +40 -39
- package/src/repos/calendar-store.ts +273 -0
- package/src/repos/cascade-delete.sqlite.test.ts +7 -9
- package/src/repos/cascade-delete.test.ts +6 -13
- package/src/repos/cascade-delete.ts +127 -97
- package/src/repos/envelope.test.ts +1 -17
- package/src/repos/envelope.ts +83 -67
- package/src/repos/filter.test.ts +22 -2
- package/src/repos/filter.ts +1 -5
- package/src/repos/i4-account-config.test.ts +2 -2
- package/src/repos/i4-account-config.ts +5 -2
- package/src/repos/i4-account.test.ts +6 -2
- package/src/repos/i4-account.ts +12 -56
- package/src/repos/i4-address-junk-move.sqlite.test.ts +1 -1
- package/src/repos/i4-address-merge-flags.sqlite.test.ts +1 -2
- package/src/repos/i4-address.ts +58 -90
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +6 -2
- package/src/repos/i4-mailbox-special-use.ts +9 -6
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -14
- package/src/repos/i4-mailbox.test.ts +17 -62
- package/src/repos/i4-mailbox.ts +121 -94
- package/src/repos/i4-organize-job-request.ts +0 -4
- package/src/repos/i4-outbox-attachment.ts +62 -33
- package/src/repos/mappers.ts +0 -16
- package/src/repos/message.sqlite.test.ts +62 -102
- package/src/repos/message.test.ts +6 -167
- package/src/repos/message.ts +227 -218
- package/src/repos/sync-warning.ts +76 -0
- package/src/repos/thread-message-body-muted.sqlite.test.ts +5 -10
- package/src/repos/thread-message-field-terms.sqlite.test.ts +5 -1
- package/src/repos/thread-message.sqlite.test.ts +2 -1
- package/src/repos/thread-message.test.ts +17 -4
- package/src/repos/thread-message.ts +502 -285
- package/src/repos/thread-search-predicates.ts +24 -26
- package/src/schema/message-data.ts +4 -8
- package/src/schema/sync-warning.ts +3 -0
- package/src/schema-sets/app.ts +17 -0
- package/src/schema-sets/mail.ts +32 -0
- package/src/schema-sets/search.ts +5 -0
- package/src/schema-version-accounting.sqlite.test.ts +56 -43
- package/src/schema.ts +3 -4
- package/src/search/thread-message-search.sqlite.test.ts +131 -0
- package/src/search/thread-message-search.ts +271 -0
- package/src/sqlite-client.ts +5 -7
- package/src/sqlite-repositories.ts +75 -0
- package/src/test-db-sqlite.ts +5 -13
- package/src/test-shipped-sqlite-schema.ts +46 -45
- package/src/test-sqlite.ts +35 -11
- package/src/vps-migrations-drift.sqlite.test.ts +81 -15
- package/src/account-granted-scopes-migration.sqlite.test.ts +0 -66
- package/src/account-synced-services-migration.sqlite.test.ts +0 -56
- package/src/mailbox-path-dedupe.sqlite.test.ts +0 -462
- package/src/mailbox-sync-status-backfill.sqlite.test.ts +0 -118
- package/src/outbox-created-cleanup.sqlite.test.ts +0 -83
- package/src/repair/duplicate-mailbox-mail.ts +0 -98
- package/src/repair/thread-message-category-contract.ts +0 -505
- package/src/repair/thread-message-category.sqlite.test.ts +0 -100
- package/src/repair/thread-message-category.ts +0 -415
- package/src/repos/calendar-migrations.sqlite.test.ts +0 -345
- package/src/repos/calendar-unit-of-work.ts +0 -31
- package/src/repos/config-import-atomicity.sqlite.test.ts +0 -255
- package/src/repos/filter-anchor-transaction.test.ts +0 -136
- package/src/repos/filter-anchor-transaction.ts +0 -45
- package/src/repos/filter-anchor.test.ts +0 -131
- package/src/repos/filter-anchor.ts +0 -105
- package/src/repos/i4-mailbox-special-use.structure.test.ts +0 -27
- package/src/repos/message-placement-backfill.sqlite.test.ts +0 -190
- package/src/repos/search-index-shape.test.ts +0 -82
- package/src/repos/search-index-shape.ts +0 -56
- package/src/repos/serialized-writes.sqlite.test.ts +0 -199
- package/src/repos/unit-of-work.sqlite.test.ts +0 -138
- package/src/repos/unit-of-work.test.ts +0 -109
- package/src/repos/unit-of-work.ts +0 -31
- package/src/schema/outbox.ts +0 -46
- package/src/schema-full-sqlite.ts +0 -13
- package/src/tx.sqlite.test.ts +0 -83
- package/src/tx.ts +0 -233
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
prepareCalendarObject,
|
|
4
5
|
provisionDefaultCalendar,
|
|
5
6
|
putCalendarObject,
|
|
6
7
|
} from "@remit/calendar-service";
|
|
7
8
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
CalendarRepositories,
|
|
10
|
+
CalendarWrite,
|
|
11
|
+
ICalendarStore,
|
|
10
12
|
} from "@remit/data-ports";
|
|
11
13
|
import type { Db } from "../db.js";
|
|
12
14
|
import {
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
calendarTable,
|
|
16
18
|
} from "../schema.js";
|
|
17
19
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
18
|
-
import {
|
|
20
|
+
import { DrizzleCalendarStore } from "./calendar-store.js";
|
|
19
21
|
|
|
20
22
|
const ACCOUNT_CONFIG_ID = "account-config-1";
|
|
21
23
|
|
|
@@ -34,35 +36,37 @@ const RESOURCE = [
|
|
|
34
36
|
].join("\r\n");
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
41
43
|
*/
|
|
42
|
-
class
|
|
43
|
-
constructor(private inner:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
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
|
+
},
|
|
58
60
|
);
|
|
61
|
+
assert.ok(orphan.ok);
|
|
62
|
+
await this.inner.commit([...writes, orphan.value]);
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
describe("the calendar write path against sqlite", () => {
|
|
63
67
|
let db: Db<Record<string, unknown>>;
|
|
64
68
|
let close: () => Promise<void>;
|
|
65
|
-
let
|
|
69
|
+
let store: ICalendarStore;
|
|
66
70
|
let calendarId: string;
|
|
67
71
|
|
|
68
72
|
before(async () => {
|
|
@@ -73,11 +77,8 @@ describe("the calendar write path against sqlite", () => {
|
|
|
73
77
|
});
|
|
74
78
|
db = created.db as unknown as Db<Record<string, unknown>>;
|
|
75
79
|
close = created.close;
|
|
76
|
-
|
|
77
|
-
const collection = await provisionDefaultCalendar(
|
|
78
|
-
unitOfWork,
|
|
79
|
-
ACCOUNT_CONFIG_ID,
|
|
80
|
-
);
|
|
80
|
+
store = new DrizzleCalendarStore(db, db);
|
|
81
|
+
const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
81
82
|
calendarId = collection.calendarId;
|
|
82
83
|
});
|
|
83
84
|
|
|
@@ -87,7 +88,7 @@ describe("the calendar write path against sqlite", () => {
|
|
|
87
88
|
const occurrences = () => db.select().from(calendarEventIndexTable);
|
|
88
89
|
|
|
89
90
|
test("writes the object, its occurrences and the sequence bump together", async () => {
|
|
90
|
-
const result = await putCalendarObject(
|
|
91
|
+
const result = await putCalendarObject(store, {
|
|
91
92
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
92
93
|
calendarId,
|
|
93
94
|
resourceName: "review.ics",
|
|
@@ -111,17 +112,17 @@ describe("the calendar write path against sqlite", () => {
|
|
|
111
112
|
);
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
test("a failing
|
|
115
|
+
test("a failing write in the batch leaves no object behind", async () => {
|
|
115
116
|
const before = await objects();
|
|
116
117
|
|
|
117
118
|
await assert.rejects(
|
|
118
|
-
putCalendarObject(new
|
|
119
|
+
putCalendarObject(new FailingLastWriteStore(store), {
|
|
119
120
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
120
121
|
calendarId,
|
|
121
122
|
resourceName: "doomed.ics",
|
|
122
123
|
icalData: RESOURCE,
|
|
123
124
|
}),
|
|
124
|
-
/
|
|
125
|
+
/NOT NULL constraint failed/,
|
|
125
126
|
);
|
|
126
127
|
|
|
127
128
|
const after = await objects();
|
|
@@ -132,17 +133,17 @@ describe("the calendar write path against sqlite", () => {
|
|
|
132
133
|
);
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
test("a failing
|
|
136
|
+
test("a failing write in the batch leaves the collection's sequence where it was", async () => {
|
|
136
137
|
const [before] = await db.select().from(calendarTable);
|
|
137
138
|
|
|
138
139
|
await assert.rejects(
|
|
139
|
-
putCalendarObject(new
|
|
140
|
+
putCalendarObject(new FailingLastWriteStore(store), {
|
|
140
141
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
141
142
|
calendarId,
|
|
142
143
|
resourceName: "doomed-again.ics",
|
|
143
144
|
icalData: RESOURCE,
|
|
144
145
|
}),
|
|
145
|
-
/
|
|
146
|
+
/NOT NULL constraint failed/,
|
|
146
147
|
);
|
|
147
148
|
|
|
148
149
|
const [after] = await db.select().from(calendarTable);
|
|
@@ -153,7 +154,7 @@ describe("the calendar write path against sqlite", () => {
|
|
|
153
154
|
const before = await objects();
|
|
154
155
|
const [collectionBefore] = await db.select().from(calendarTable);
|
|
155
156
|
|
|
156
|
-
const result = await putCalendarObject(
|
|
157
|
+
const result = await putCalendarObject(store, {
|
|
157
158
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
158
159
|
calendarId,
|
|
159
160
|
resourceName: "backwards.ics",
|
|
@@ -178,7 +179,7 @@ describe("the calendar write path against sqlite", () => {
|
|
|
178
179
|
});
|
|
179
180
|
|
|
180
181
|
test("rewriting a resource replaces its occurrences rather than adding to them", async () => {
|
|
181
|
-
const first = await putCalendarObject(
|
|
182
|
+
const first = await putCalendarObject(store, {
|
|
182
183
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
183
184
|
calendarId,
|
|
184
185
|
resourceName: "shrinking.ics",
|
|
@@ -186,7 +187,7 @@ describe("the calendar write path against sqlite", () => {
|
|
|
186
187
|
});
|
|
187
188
|
assert.ok(first.ok);
|
|
188
189
|
|
|
189
|
-
const second = await putCalendarObject(
|
|
190
|
+
const second = await putCalendarObject(store, {
|
|
190
191
|
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
191
192
|
calendarId,
|
|
192
193
|
resourceName: "shrinking.ics",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CalendarRepositories,
|
|
3
|
+
CalendarWrite,
|
|
4
|
+
ICalendarStore,
|
|
5
|
+
} from "@remit/data-ports";
|
|
6
|
+
import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
7
|
+
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 {
|
|
11
|
+
calendarEventIndexTable,
|
|
12
|
+
calendarFeedTokenTable,
|
|
13
|
+
calendarObjectTable,
|
|
14
|
+
calendarSuggestionTable,
|
|
15
|
+
calendarTable,
|
|
16
|
+
} from "../schema.js";
|
|
17
|
+
import { CalendarCollectionRepo } from "./calendar-collection.js";
|
|
18
|
+
import { CalendarEventIndexRepo } from "./calendar-event-index.js";
|
|
19
|
+
import { CalendarFeedTokenRepo } from "./calendar-feed-token.js";
|
|
20
|
+
import { CalendarObjectRepo } from "./calendar-object.js";
|
|
21
|
+
import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
|
|
22
|
+
|
|
23
|
+
type DB = Db<Record<string, unknown>>;
|
|
24
|
+
|
|
25
|
+
export class DrizzleCalendarStore implements ICalendarStore {
|
|
26
|
+
readonly repos: CalendarRepositories;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private db: DB,
|
|
30
|
+
private mail: DB,
|
|
31
|
+
) {
|
|
32
|
+
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),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
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)),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private collection(accountConfigId: string, calendarId: string) {
|
|
58
|
+
return and(
|
|
59
|
+
eq(calendarTable.accountConfigId, accountConfigId),
|
|
60
|
+
eq(calendarTable.calendarId, calendarId),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The sequence the bump just wrote, read inside the batch. A collection that
|
|
66
|
+
* is gone yields NULL, which the NOT NULL column refuses, so the whole batch
|
|
67
|
+
* fails rather than storing an orphan.
|
|
68
|
+
*/
|
|
69
|
+
private bumpedSequence(accountConfigId: string, calendarId: string) {
|
|
70
|
+
return sql<number>`(${this.db
|
|
71
|
+
.select({ syncSequence: calendarTable.syncSequence })
|
|
72
|
+
.from(calendarTable)
|
|
73
|
+
.where(this.collection(accountConfigId, calendarId))})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private bump(accountConfigId: string, calendarId: string): SqliteBatchItem {
|
|
77
|
+
return this.db
|
|
78
|
+
.update(calendarTable)
|
|
79
|
+
.set({
|
|
80
|
+
syncSequence: sql`${calendarTable.syncSequence} + 1`,
|
|
81
|
+
updatedAt: Date.now(),
|
|
82
|
+
})
|
|
83
|
+
.where(this.collection(accountConfigId, calendarId));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private clearOccurrences(
|
|
87
|
+
calendarId: string,
|
|
88
|
+
calendarObjectId: string,
|
|
89
|
+
): SqliteBatchItem {
|
|
90
|
+
return this.db
|
|
91
|
+
.delete(calendarEventIndexTable)
|
|
92
|
+
.where(
|
|
93
|
+
and(
|
|
94
|
+
eq(calendarEventIndexTable.calendarId, calendarId),
|
|
95
|
+
eq(calendarEventIndexTable.calendarObjectId, calendarObjectId),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private statements(write: CalendarWrite): SqliteBatchItem[] {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
switch (write.kind) {
|
|
103
|
+
case "PutObject": {
|
|
104
|
+
const { calendarId, resourceName } = write.object;
|
|
105
|
+
const calendarObjectId = deriveCalendarObjectId(
|
|
106
|
+
calendarId,
|
|
107
|
+
resourceName,
|
|
108
|
+
);
|
|
109
|
+
const syncSequence = this.bumpedSequence(
|
|
110
|
+
write.accountConfigId,
|
|
111
|
+
calendarId,
|
|
112
|
+
);
|
|
113
|
+
const items: SqliteBatchItem[] = [
|
|
114
|
+
this.bump(write.accountConfigId, calendarId),
|
|
115
|
+
this.db
|
|
116
|
+
.insert(calendarObjectTable)
|
|
117
|
+
.values({
|
|
118
|
+
...write.object,
|
|
119
|
+
calendarObjectId,
|
|
120
|
+
syncSequence,
|
|
121
|
+
createdAt: now,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
})
|
|
124
|
+
.onConflictDoUpdate({
|
|
125
|
+
target: calendarObjectTable.calendarObjectId,
|
|
126
|
+
set: { ...write.object, syncSequence, updatedAt: now },
|
|
127
|
+
}),
|
|
128
|
+
this.clearOccurrences(calendarId, calendarObjectId),
|
|
129
|
+
];
|
|
130
|
+
if (write.occurrences.length === 0) return items;
|
|
131
|
+
return [
|
|
132
|
+
...items,
|
|
133
|
+
this.db.insert(calendarEventIndexTable).values(
|
|
134
|
+
write.occurrences.map((occurrence) => ({
|
|
135
|
+
...occurrence,
|
|
136
|
+
calendarId,
|
|
137
|
+
calendarObjectId,
|
|
138
|
+
createdAt: now,
|
|
139
|
+
updatedAt: now,
|
|
140
|
+
})),
|
|
141
|
+
),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
case "ReprojectObject": {
|
|
145
|
+
const { calendarId, resourceName } = write.object;
|
|
146
|
+
const calendarObjectId = deriveCalendarObjectId(
|
|
147
|
+
calendarId,
|
|
148
|
+
resourceName,
|
|
149
|
+
);
|
|
150
|
+
// Every statement reads the object it rewrites at the version the
|
|
151
|
+
// caller read, so a resource edited or deleted since writes nothing.
|
|
152
|
+
const asRead = and(
|
|
153
|
+
eq(calendarObjectTable.calendarObjectId, calendarObjectId),
|
|
154
|
+
eq(calendarObjectTable.etag, write.readEtag),
|
|
155
|
+
);
|
|
156
|
+
const unchanged = exists(
|
|
157
|
+
this.db
|
|
158
|
+
.select({ id: calendarObjectTable.calendarObjectId })
|
|
159
|
+
.from(calendarObjectTable)
|
|
160
|
+
.where(asRead),
|
|
161
|
+
);
|
|
162
|
+
const value = <T>(field: T, name: string) => sql<T>`${field}`.as(name);
|
|
163
|
+
return [
|
|
164
|
+
this.bump(write.accountConfigId, calendarId),
|
|
165
|
+
this.db
|
|
166
|
+
.update(calendarObjectTable)
|
|
167
|
+
.set({
|
|
168
|
+
...write.object,
|
|
169
|
+
syncSequence: this.bumpedSequence(
|
|
170
|
+
write.accountConfigId,
|
|
171
|
+
calendarId,
|
|
172
|
+
),
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
})
|
|
175
|
+
.where(asRead),
|
|
176
|
+
this.db
|
|
177
|
+
.delete(calendarEventIndexTable)
|
|
178
|
+
.where(
|
|
179
|
+
and(
|
|
180
|
+
eq(calendarEventIndexTable.calendarId, calendarId),
|
|
181
|
+
eq(calendarEventIndexTable.calendarObjectId, calendarObjectId),
|
|
182
|
+
unchanged,
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
...write.occurrences.map((occurrence) =>
|
|
186
|
+
this.db.insert(calendarEventIndexTable).select(
|
|
187
|
+
this.db
|
|
188
|
+
.select({
|
|
189
|
+
calendarId: value(calendarId, "calendar_id"),
|
|
190
|
+
calendarObjectId: value(
|
|
191
|
+
calendarObjectId,
|
|
192
|
+
"calendar_object_id",
|
|
193
|
+
),
|
|
194
|
+
recurrenceId: value(occurrence.recurrenceId, "recurrence_id"),
|
|
195
|
+
startAt: value(occurrence.startAt, "start_at"),
|
|
196
|
+
endAt: value(occurrence.endAt, "end_at"),
|
|
197
|
+
allDay: value(occurrence.allDay ? 1 : 0, "all_day"),
|
|
198
|
+
summary: value(occurrence.summary, "summary"),
|
|
199
|
+
status: value(occurrence.status, "status"),
|
|
200
|
+
transparency: value(occurrence.transparency, "transparency"),
|
|
201
|
+
createdAt: value(now, "created_at"),
|
|
202
|
+
updatedAt: value(now, "updated_at"),
|
|
203
|
+
})
|
|
204
|
+
.from(calendarObjectTable)
|
|
205
|
+
.where(asRead),
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
case "DeleteObject":
|
|
211
|
+
return [
|
|
212
|
+
this.clearOccurrences(write.calendarId, write.calendarObjectId),
|
|
213
|
+
this.db
|
|
214
|
+
.delete(calendarObjectTable)
|
|
215
|
+
.where(
|
|
216
|
+
and(
|
|
217
|
+
eq(calendarObjectTable.calendarId, write.calendarId),
|
|
218
|
+
eq(
|
|
219
|
+
calendarObjectTable.calendarObjectId,
|
|
220
|
+
write.calendarObjectId,
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
this.bump(write.accountConfigId, write.calendarId),
|
|
225
|
+
];
|
|
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
|
+
case "UpdateCollection":
|
|
242
|
+
return [
|
|
243
|
+
this.db
|
|
244
|
+
.update(calendarTable)
|
|
245
|
+
.set({ ...write.patch, updatedAt: now })
|
|
246
|
+
.where(this.collection(write.accountConfigId, write.calendarId)),
|
|
247
|
+
];
|
|
248
|
+
case "DeleteCollection":
|
|
249
|
+
return [
|
|
250
|
+
this.db
|
|
251
|
+
.delete(calendarEventIndexTable)
|
|
252
|
+
.where(eq(calendarEventIndexTable.calendarId, write.calendarId)),
|
|
253
|
+
this.db
|
|
254
|
+
.delete(calendarObjectTable)
|
|
255
|
+
.where(eq(calendarObjectTable.calendarId, write.calendarId)),
|
|
256
|
+
this.db
|
|
257
|
+
.delete(calendarFeedTokenTable)
|
|
258
|
+
.where(
|
|
259
|
+
and(
|
|
260
|
+
eq(
|
|
261
|
+
calendarFeedTokenTable.accountConfigId,
|
|
262
|
+
write.accountConfigId,
|
|
263
|
+
),
|
|
264
|
+
eq(calendarFeedTokenTable.calendarId, write.calendarId),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
this.db
|
|
268
|
+
.delete(calendarTable)
|
|
269
|
+
.where(this.collection(write.accountConfigId, write.calendarId)),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
mailboxTable,
|
|
13
13
|
messageTable,
|
|
14
14
|
quarantineTable,
|
|
15
|
+
syncWarningTable,
|
|
15
16
|
} from "../schema.js";
|
|
16
17
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
17
18
|
import {
|
|
@@ -30,6 +31,7 @@ const CASCADE_SCHEMA = {
|
|
|
30
31
|
account: accountTable,
|
|
31
32
|
mailbox: mailboxTable,
|
|
32
33
|
quarantine: quarantineTable,
|
|
34
|
+
syncWarning: syncWarningTable,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
const log = { info: () => {} };
|
|
@@ -131,7 +133,11 @@ describe("createSqliteCascadeDeleter", () => {
|
|
|
131
133
|
{ entityType: "Account", key: { accountId: ACCOUNT_ID } },
|
|
132
134
|
];
|
|
133
135
|
|
|
134
|
-
const deleter = await createSqliteCascadeDeleter(
|
|
136
|
+
const deleter = await createSqliteCascadeDeleter({
|
|
137
|
+
app: filename,
|
|
138
|
+
mail: filename,
|
|
139
|
+
search: filename,
|
|
140
|
+
});
|
|
135
141
|
await deleter(entities, log);
|
|
136
142
|
|
|
137
143
|
const sqlite = new Database(filename);
|
|
@@ -150,14 +156,6 @@ describe("createSqliteCascadeDeleter", () => {
|
|
|
150
156
|
);
|
|
151
157
|
assert.equal(count("mailbox", "mailbox_id", MAILBOX_ID), 0, "mailbox gone");
|
|
152
158
|
assert.equal(count("account", "account_id", ACCOUNT_ID), 0, "account gone");
|
|
153
|
-
|
|
154
|
-
const outboxRows = (
|
|
155
|
-
sqlite.prepare("SELECT COUNT(*) AS n FROM outbox").get() as { n: number }
|
|
156
|
-
).n;
|
|
157
|
-
assert.ok(
|
|
158
|
-
outboxRows >= 1,
|
|
159
|
-
"a message-removed outbox row must be emitted for search-index cleanup",
|
|
160
|
-
);
|
|
161
159
|
sqlite.close();
|
|
162
160
|
});
|
|
163
161
|
});
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
messagePlacementMoveTable,
|
|
16
16
|
messageTable,
|
|
17
17
|
outboxMessageTable,
|
|
18
|
-
outboxTable,
|
|
19
18
|
quarantineTable,
|
|
20
19
|
threadMessageTable,
|
|
21
20
|
} from "../schema.js";
|
|
@@ -299,7 +298,11 @@ describe("runDrizzleCascadeDelete", () => {
|
|
|
299
298
|
];
|
|
300
299
|
|
|
301
300
|
test("deletes every enumerated row across all tables", async () => {
|
|
302
|
-
await runDrizzleCascadeDelete(
|
|
301
|
+
await runDrizzleCascadeDelete(
|
|
302
|
+
{ app: db, mail: db, search: db } as never,
|
|
303
|
+
entities,
|
|
304
|
+
log,
|
|
305
|
+
);
|
|
303
306
|
|
|
304
307
|
assert.equal(
|
|
305
308
|
(
|
|
@@ -431,21 +434,11 @@ describe("runDrizzleCascadeDelete", () => {
|
|
|
431
434
|
);
|
|
432
435
|
});
|
|
433
436
|
|
|
434
|
-
test("emits a message.removed outbox row for search-index cleanup", async () => {
|
|
435
|
-
const rows = await db
|
|
436
|
-
.select()
|
|
437
|
-
.from(outboxTable)
|
|
438
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
439
|
-
assert.equal(rows.length, 1);
|
|
440
|
-
assert.equal(rows[0].event, "message.removed");
|
|
441
|
-
assert.equal(rows[0].processedAt, null);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
437
|
test("rejects an unknown entity type without deleting", async () => {
|
|
445
438
|
await assert.rejects(
|
|
446
439
|
() =>
|
|
447
440
|
runDrizzleCascadeDelete(
|
|
448
|
-
db as never,
|
|
441
|
+
{ app: db, mail: db, search: db } as never,
|
|
449
442
|
[{ entityType: "Nonsense", key: { id: "x" } }],
|
|
450
443
|
log,
|
|
451
444
|
),
|