@remit/drizzle-service 0.0.99 → 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 +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
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
|
+
import * as entities from "@remit/drizzle-sqlite-schema";
|
|
4
5
|
import {
|
|
5
6
|
generateSQLiteDrizzleJson,
|
|
6
7
|
generateSQLiteMigration,
|
|
7
8
|
} from "drizzle-kit/api";
|
|
9
|
+
import { getTableName, is } from "drizzle-orm";
|
|
10
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Every committed SQLite migration set must describe the schema its drizzle
|
|
@@ -31,13 +34,14 @@ import {
|
|
|
31
34
|
|
|
32
35
|
const REPO_ROOT = new URL("../../../", import.meta.url);
|
|
33
36
|
|
|
34
|
-
const CONFIGS =
|
|
35
|
-
"packages/migrate/drizzle.
|
|
36
|
-
"packages/migrate/drizzle.
|
|
37
|
-
"packages/migrate/drizzle.
|
|
38
|
-
|
|
37
|
+
const CONFIGS = {
|
|
38
|
+
app: "packages/migrate/drizzle.app.sqlite.config.ts",
|
|
39
|
+
mail: "packages/migrate/drizzle.mail.sqlite.config.ts",
|
|
40
|
+
search: "packages/migrate/drizzle.search.sqlite.config.ts",
|
|
41
|
+
mcp: "packages/migrate/drizzle.mcp.sqlite.config.ts",
|
|
42
|
+
};
|
|
39
43
|
|
|
40
|
-
type DrizzleConfig = { schema: string; out: string };
|
|
44
|
+
type DrizzleConfig = { schema: string[]; out: string };
|
|
41
45
|
|
|
42
46
|
const loadConfig = async (path: string): Promise<DrizzleConfig> =>
|
|
43
47
|
(
|
|
@@ -46,11 +50,38 @@ const loadConfig = async (path: string): Promise<DrizzleConfig> =>
|
|
|
46
50
|
}
|
|
47
51
|
).default;
|
|
48
52
|
|
|
53
|
+
const loadSchema = async (
|
|
54
|
+
config: DrizzleConfig,
|
|
55
|
+
): Promise<Record<string, unknown>> =>
|
|
56
|
+
Object.assign(
|
|
57
|
+
{},
|
|
58
|
+
...(await Promise.all(
|
|
59
|
+
config.schema.map(
|
|
60
|
+
(path) =>
|
|
61
|
+
import(new URL(path, REPO_ROOT).href) as Promise<
|
|
62
|
+
Record<string, unknown>
|
|
63
|
+
>,
|
|
64
|
+
),
|
|
65
|
+
)),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const tableNames = (schema: Record<string, unknown>): string[] =>
|
|
69
|
+
Object.values(schema)
|
|
70
|
+
.filter((value) => is(value, SQLiteTable))
|
|
71
|
+
.map((table) => getTableName(table as SQLiteTable));
|
|
72
|
+
|
|
73
|
+
interface Journal {
|
|
74
|
+
entries: Array<{ idx: number; tag: string }>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const journalOf = (out: string): Journal =>
|
|
78
|
+
JSON.parse(
|
|
79
|
+
readFileSync(new URL(`${out}/meta/_journal.json`, REPO_ROOT), "utf8"),
|
|
80
|
+
) as Journal;
|
|
81
|
+
|
|
49
82
|
const latestSnapshot = (out: string): Record<string, unknown> => {
|
|
50
83
|
const dir = new URL(`${out}/`, REPO_ROOT);
|
|
51
|
-
const journal =
|
|
52
|
-
readFileSync(new URL("meta/_journal.json", dir), "utf8"),
|
|
53
|
-
) as { entries: Array<{ idx: number }> };
|
|
84
|
+
const journal = journalOf(out);
|
|
54
85
|
const idx = Math.max(...journal.entries.map((entry) => entry.idx));
|
|
55
86
|
return JSON.parse(
|
|
56
87
|
readFileSync(
|
|
@@ -61,12 +92,10 @@ const latestSnapshot = (out: string): Record<string, unknown> => {
|
|
|
61
92
|
};
|
|
62
93
|
|
|
63
94
|
describe("committed sqlite migrations", () => {
|
|
64
|
-
for (const configPath of CONFIGS) {
|
|
95
|
+
for (const configPath of Object.values(CONFIGS)) {
|
|
65
96
|
test(`${configPath} — the set matches its schema`, async () => {
|
|
66
97
|
const config = await loadConfig(configPath);
|
|
67
|
-
const schema =
|
|
68
|
-
new URL(config.schema, REPO_ROOT).href
|
|
69
|
-
)) as Record<string, unknown>;
|
|
98
|
+
const schema = await loadSchema(config);
|
|
70
99
|
|
|
71
100
|
const drift = await generateSQLiteMigration(
|
|
72
101
|
latestSnapshot(config.out) as unknown as Parameters<
|
|
@@ -83,8 +112,45 @@ describe("committed sqlite migrations", () => {
|
|
|
83
112
|
});
|
|
84
113
|
}
|
|
85
114
|
|
|
115
|
+
for (const configPath of Object.values(CONFIGS)) {
|
|
116
|
+
test(`${configPath} — the journal names every file on disk once, in order`, async () => {
|
|
117
|
+
const { out } = await loadConfig(configPath);
|
|
118
|
+
const { entries } = journalOf(out);
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
entries.map((entry) => entry.idx),
|
|
121
|
+
entries.map((_, index) => index),
|
|
122
|
+
);
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
entries.map((entry) => `${entry.tag}.sql`).sort(),
|
|
125
|
+
readdirSync(new URL(`${out}/`, REPO_ROOT))
|
|
126
|
+
.filter((name) => name.endsWith(".sql"))
|
|
127
|
+
.sort(),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A new TypeSpec entity has to be placed in a database: one that is in
|
|
133
|
+
// neither set is never migrated anywhere, and one in both is two tables.
|
|
134
|
+
test("place every generated entity table in exactly one database", async () => {
|
|
135
|
+
const [app, mail] = await Promise.all(
|
|
136
|
+
[CONFIGS.app, CONFIGS.mail].map(async (path) =>
|
|
137
|
+
tableNames(await loadSchema(await loadConfig(path))),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
const placed = [...app, ...mail].sort();
|
|
141
|
+
assert.deepEqual(placed, [...new Set(placed)]);
|
|
142
|
+
for (const table of tableNames(entities)) {
|
|
143
|
+
assert.ok(placed.includes(table), `${table} is in no database`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("keep the search set free of drizzle tables", async () => {
|
|
148
|
+
const config = await loadConfig(CONFIGS.search);
|
|
149
|
+
assert.deepEqual(tableNames(await loadSchema(config)), []);
|
|
150
|
+
});
|
|
151
|
+
|
|
86
152
|
test("declare mailbox.highest_modseq as text", async () => {
|
|
87
|
-
const { out } = await loadConfig(CONFIGS
|
|
153
|
+
const { out } = await loadConfig(CONFIGS.mail);
|
|
88
154
|
const snapshot = latestSnapshot(out) as {
|
|
89
155
|
tables: Record<
|
|
90
156
|
string,
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, test } from "node:test";
|
|
3
|
-
import Database from "better-sqlite3";
|
|
4
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
5
|
-
import { AccountRepo } from "./repos/i4-account.js";
|
|
6
|
-
import { accountTable } from "./schema.js";
|
|
7
|
-
import {
|
|
8
|
-
applyMigration,
|
|
9
|
-
migrationJournal,
|
|
10
|
-
} from "./test-shipped-sqlite-schema.js";
|
|
11
|
-
|
|
12
|
-
const MIGRATION = "0032_account_granted_scopes";
|
|
13
|
-
|
|
14
|
-
const atPredecessor = (): Database.Database => {
|
|
15
|
-
const sqlite = new Database(":memory:");
|
|
16
|
-
for (const entry of migrationJournal()) {
|
|
17
|
-
if (entry.tag === MIGRATION) return sqlite;
|
|
18
|
-
applyMigration(sqlite, entry.tag);
|
|
19
|
-
}
|
|
20
|
-
throw new Error(`${MIGRATION} is not in the journal`);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const migrateOnward = (sqlite: Database.Database): void => {
|
|
24
|
-
const tags = migrationJournal().map((entry) => entry.tag);
|
|
25
|
-
for (const tag of tags.slice(tags.indexOf(MIGRATION))) {
|
|
26
|
-
applyMigration(sqlite, tag);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const insertAccount = (
|
|
31
|
-
sqlite: Database.Database,
|
|
32
|
-
accountId: string,
|
|
33
|
-
authType: string,
|
|
34
|
-
): void => {
|
|
35
|
-
sqlite
|
|
36
|
-
.prepare(
|
|
37
|
-
`INSERT INTO account (
|
|
38
|
-
account_id, account_config_id, username, email, auth_type,
|
|
39
|
-
imap_host, imap_port, imap_tls, imap_start_tls, smtp_port,
|
|
40
|
-
is_active, connection_state, created_at, updated_at
|
|
41
|
-
) VALUES (?, 'config', 'person', 'person@example.com', ?,
|
|
42
|
-
'outlook.office365.com', 993, 1, 0, 587, 1,
|
|
43
|
-
'not_authenticated', 0, 0)`,
|
|
44
|
-
)
|
|
45
|
-
.run(accountId, authType);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
describe("an account written before granted scopes were recorded", () => {
|
|
49
|
-
test("a Microsoft account holds the mail grant its consent always asked for", async () => {
|
|
50
|
-
const sqlite = atPredecessor();
|
|
51
|
-
insertAccount(sqlite, "microsoft", "oauthMicrosoft");
|
|
52
|
-
insertAccount(sqlite, "password", "password");
|
|
53
|
-
|
|
54
|
-
migrateOnward(sqlite);
|
|
55
|
-
|
|
56
|
-
const repo = new AccountRepo(
|
|
57
|
-
drizzle(sqlite, { schema: { accounts: accountTable } }) as never,
|
|
58
|
-
);
|
|
59
|
-
assert.deepEqual((await repo.get("microsoft")).grantedScopes, [
|
|
60
|
-
"https://outlook.office.com/IMAP.AccessAsUser.All",
|
|
61
|
-
"https://outlook.office.com/SMTP.Send",
|
|
62
|
-
]);
|
|
63
|
-
assert.deepEqual((await repo.get("password")).grantedScopes, []);
|
|
64
|
-
sqlite.close();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, test } from "node:test";
|
|
3
|
-
import { AccountService } from "@remit/domain-enums";
|
|
4
|
-
import Database from "better-sqlite3";
|
|
5
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
6
|
-
import { AccountRepo } from "./repos/i4-account.js";
|
|
7
|
-
import { accountTable } from "./schema.js";
|
|
8
|
-
import {
|
|
9
|
-
applyMigration,
|
|
10
|
-
migrationJournal,
|
|
11
|
-
} from "./test-shipped-sqlite-schema.js";
|
|
12
|
-
|
|
13
|
-
const MIGRATION = "0029_account_synced_services";
|
|
14
|
-
|
|
15
|
-
const atPredecessor = (): Database.Database => {
|
|
16
|
-
const sqlite = new Database(":memory:");
|
|
17
|
-
for (const entry of migrationJournal()) {
|
|
18
|
-
if (entry.tag === MIGRATION) return sqlite;
|
|
19
|
-
applyMigration(sqlite, entry.tag);
|
|
20
|
-
}
|
|
21
|
-
throw new Error(`${MIGRATION} is not in the journal`);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const migrateOnward = (sqlite: Database.Database): void => {
|
|
25
|
-
const tags = migrationJournal().map((entry) => entry.tag);
|
|
26
|
-
for (const tag of tags.slice(tags.indexOf(MIGRATION))) {
|
|
27
|
-
applyMigration(sqlite, tag);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe("an account written before the service selection existed", () => {
|
|
32
|
-
test("comes out of the migration syncing mail only", async () => {
|
|
33
|
-
const sqlite = atPredecessor();
|
|
34
|
-
sqlite
|
|
35
|
-
.prepare(
|
|
36
|
-
`INSERT INTO account (
|
|
37
|
-
account_id, account_config_id, username, email, auth_type,
|
|
38
|
-
imap_host, imap_port, imap_tls, imap_start_tls, smtp_port,
|
|
39
|
-
is_active, connection_state, created_at, updated_at
|
|
40
|
-
) VALUES ('existing', 'config', 'person', 'person@example.com',
|
|
41
|
-
'password', 'imap.example.com', 993, 1, 0, 587, 1,
|
|
42
|
-
'not_authenticated', 0, 0)`,
|
|
43
|
-
)
|
|
44
|
-
.run();
|
|
45
|
-
|
|
46
|
-
migrateOnward(sqlite);
|
|
47
|
-
|
|
48
|
-
const repo = new AccountRepo(
|
|
49
|
-
drizzle(sqlite, { schema: { accounts: accountTable } }) as never,
|
|
50
|
-
);
|
|
51
|
-
assert.deepEqual((await repo.get("existing")).syncedServices, [
|
|
52
|
-
AccountService.Mail,
|
|
53
|
-
]);
|
|
54
|
-
sqlite.close();
|
|
55
|
-
});
|
|
56
|
-
});
|