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