@remit/drizzle-service 0.0.12 → 0.0.13
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/repos/i4-mailbox.sqlite.test.ts +41 -36
- package/src/repos/i4-mailbox.ts +1 -9
- package/src/test-shipped-sqlite-schema.ts +39 -0
- package/src/vps-migrations-drift.sqlite.test.ts +101 -0
- package/src/repos/message-flag-wire-format-migration.sqlite.test.ts +0 -186
- package/src/vps-migrations-drift.test.ts +0 -28
package/package.json
CHANGED
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
3
|
import { after, before, describe, test } from "node:test";
|
|
5
4
|
import Database from "better-sqlite3";
|
|
6
5
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
6
|
import { mailboxTable } from "../schema.js";
|
|
8
7
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
8
|
+
import {
|
|
9
|
+
applyMigration,
|
|
10
|
+
shippedTableDdl,
|
|
11
|
+
} from "../test-shipped-sqlite-schema.js";
|
|
9
12
|
import { MailboxRepo } from "./i4-mailbox.js";
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
* The `mailbox` DDL as it actually ships, read from the committed migration
|
|
13
|
-
* rather than pushed from the drizzle table objects.
|
|
14
|
-
*
|
|
15
|
-
* The two disagree: the table object declares `highest_modseq` as text, the
|
|
16
|
-
* shipped migration still declares it `integer` (reader#73). Every other
|
|
17
|
-
* SQLite test in this package runs against the pushed shape, so none of them
|
|
18
|
-
* has ever exercised the one deployments run on — and SQLite hands a column
|
|
19
|
-
* with numeric affinity back as a number regardless of what the schema says.
|
|
20
|
-
* Reading the committed file keeps this test honest as the migration changes.
|
|
21
|
-
*/
|
|
22
|
-
const shippedMailboxDdl = (): string => {
|
|
23
|
-
const sql = readFileSync(
|
|
24
|
-
new URL(
|
|
25
|
-
"../../../../deploy/vps/migrations-sqlite/entities/0000_happy_roland_deschain.sql",
|
|
26
|
-
import.meta.url,
|
|
27
|
-
),
|
|
28
|
-
"utf8",
|
|
29
|
-
);
|
|
30
|
-
const match = sql.match(/CREATE TABLE `mailbox` \([\s\S]*?\n\);/);
|
|
31
|
-
if (!match)
|
|
32
|
-
throw new Error("mailbox DDL not found in the committed migration");
|
|
33
|
-
return match[0];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
14
|
function makeMailboxInput(accountId: string, fullPath = "INBOX") {
|
|
37
15
|
return {
|
|
38
16
|
accountId,
|
|
@@ -90,13 +68,25 @@ describe("MailboxRepo (sqlite)", () => {
|
|
|
90
68
|
});
|
|
91
69
|
});
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The same repository against the shape a deployment actually runs: the
|
|
73
|
+
* committed migrations applied in order, rather than the schema pushed from the
|
|
74
|
+
* drizzle table objects.
|
|
75
|
+
*
|
|
76
|
+
* The two used to disagree — `highest_modseq` shipped as `integer` while the
|
|
77
|
+
* table object said `text` (reader#73) — and SQLite hands a column with numeric
|
|
78
|
+
* affinity back as a number whatever the declared type, so the repo returned a
|
|
79
|
+
* number where its own type said string. Reading the committed files keeps this
|
|
80
|
+
* honest as the migration set changes.
|
|
81
|
+
*/
|
|
82
|
+
describe("MailboxRepo (sqlite, shipped migrations)", () => {
|
|
94
83
|
let close: () => Promise<void>;
|
|
95
84
|
let repo: MailboxRepo;
|
|
96
85
|
|
|
97
|
-
before(
|
|
86
|
+
before(() => {
|
|
98
87
|
const sqlite = new Database(":memory:");
|
|
99
|
-
sqlite.exec(
|
|
88
|
+
sqlite.exec(shippedTableDdl("0000_happy_roland_deschain", "mailbox"));
|
|
89
|
+
applyMigration(sqlite, "0002_highest_modseq_text");
|
|
100
90
|
const db = drizzle(sqlite, { schema: { mailbox: mailboxTable } });
|
|
101
91
|
repo = new MailboxRepo(db as never);
|
|
102
92
|
close = async () => {
|
|
@@ -108,11 +98,14 @@ describe("MailboxRepo (sqlite, shipped column shape)", () => {
|
|
|
108
98
|
await close();
|
|
109
99
|
});
|
|
110
100
|
|
|
111
|
-
test("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
test("declares highest_modseq as text", () => {
|
|
102
|
+
assert.match(
|
|
103
|
+
shippedTableDdl("0002_highest_modseq_text", "__new_mailbox"),
|
|
104
|
+
/`highest_modseq` text NOT NULL/,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("reads a plain-digit cursor back as a string", async () => {
|
|
116
109
|
const accountId = randomUUID();
|
|
117
110
|
const created = await repo.create({
|
|
118
111
|
...makeMailboxInput(accountId),
|
|
@@ -123,10 +116,9 @@ describe("MailboxRepo (sqlite, shipped column shape)", () => {
|
|
|
123
116
|
|
|
124
117
|
const fetched = await repo.get(accountId, created.mailboxId);
|
|
125
118
|
assert.strictEqual(fetched.highestModseq, "900");
|
|
126
|
-
assert.strictEqual(fetched.highestModseq === "900", true);
|
|
127
119
|
});
|
|
128
120
|
|
|
129
|
-
test("keeps a resumable cursor intact
|
|
121
|
+
test("keeps a resumable cursor intact", async () => {
|
|
130
122
|
const accountId = randomUUID();
|
|
131
123
|
const created = await repo.create({
|
|
132
124
|
...makeMailboxInput(accountId, "Archive"),
|
|
@@ -136,4 +128,17 @@ describe("MailboxRepo (sqlite, shipped column shape)", () => {
|
|
|
136
128
|
const fetched = await repo.get(accountId, created.mailboxId);
|
|
137
129
|
assert.strictEqual(fetched.highestModseq, "900:149");
|
|
138
130
|
});
|
|
131
|
+
|
|
132
|
+
test("round-trips a cursor above 2^53 with its exact digits", async () => {
|
|
133
|
+
const accountId = randomUUID();
|
|
134
|
+
const modseq = "18446744073709551615";
|
|
135
|
+
const created = await repo.create({
|
|
136
|
+
...makeMailboxInput(accountId, "Sent"),
|
|
137
|
+
highestModseq: modseq,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(created.highestModseq, modseq);
|
|
141
|
+
const fetched = await repo.get(accountId, created.mailboxId);
|
|
142
|
+
assert.strictEqual(fetched.highestModseq, modseq);
|
|
143
|
+
});
|
|
139
144
|
});
|
package/src/repos/i4-mailbox.ts
CHANGED
|
@@ -33,15 +33,7 @@ export function rowToMailbox(
|
|
|
33
33
|
fullPath: row.fullPath,
|
|
34
34
|
uidValidity: row.uidValidity,
|
|
35
35
|
uidNext: row.uidNext,
|
|
36
|
-
|
|
37
|
-
// schema declares, and the shipped self-host migration still declares
|
|
38
|
-
// this one `integer` (reader#73). A cursor read back as a number is not
|
|
39
|
-
// merely awkward to parse: `"900" === 900` is false, so code comparing
|
|
40
|
-
// the value it just wrote against the value it read would conclude
|
|
41
|
-
// nothing had changed — which is how a stalled cursor goes unreported.
|
|
42
|
-
// Normalising here means every consumer sees the declared type instead
|
|
43
|
-
// of each one guarding separately.
|
|
44
|
-
highestModseq: String(row.highestModseq),
|
|
36
|
+
highestModseq: row.highestModseq,
|
|
45
37
|
messageCount: row.messageCount,
|
|
46
38
|
unseenCount: row.unseenCount,
|
|
47
39
|
deletedCount: row.deletedCount,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type Database from "better-sqlite3";
|
|
3
|
+
|
|
4
|
+
// Read the committed SQLite entity migrations — the DDL a self-host deployment
|
|
5
|
+
// actually runs — so a test can exercise the shipped shape instead of the one
|
|
6
|
+
// `pushSQLiteSchema` derives from the drizzle table objects. The two are
|
|
7
|
+
// generated from the same entities but only the pushed one is regenerated on
|
|
8
|
+
// every run, so drift between them is invisible to any test that pushes
|
|
9
|
+
// (reader#73). Reading the files means a test fails when they drift, and
|
|
10
|
+
// tracks them when they change.
|
|
11
|
+
|
|
12
|
+
const MIGRATIONS_DIR = new URL(
|
|
13
|
+
"../../../deploy/vps/migrations-sqlite/entities/",
|
|
14
|
+
import.meta.url,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const migrationSql = (tag: string): string =>
|
|
18
|
+
readFileSync(new URL(`${tag}.sql`, MIGRATIONS_DIR), "utf8");
|
|
19
|
+
|
|
20
|
+
/** The `CREATE TABLE` block for one table, as that migration declares it. */
|
|
21
|
+
export const shippedTableDdl = (tag: string, table: string): string => {
|
|
22
|
+
const match = migrationSql(tag).match(
|
|
23
|
+
new RegExp(`CREATE TABLE \`${table}\` \\([\\s\\S]*?\\n\\);`),
|
|
24
|
+
);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`${table} DDL not found in migration ${tag}`);
|
|
27
|
+
}
|
|
28
|
+
return match[0];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Run every statement of a committed migration against an open database. */
|
|
32
|
+
export const applyMigration = (
|
|
33
|
+
sqlite: Database.Database,
|
|
34
|
+
tag: string,
|
|
35
|
+
): void => {
|
|
36
|
+
for (const statement of migrationSql(tag).split("--> statement-breakpoint")) {
|
|
37
|
+
sqlite.exec(statement);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import {
|
|
5
|
+
generateSQLiteDrizzleJson,
|
|
6
|
+
generateSQLiteMigration,
|
|
7
|
+
} from "drizzle-kit/api";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every committed SQLite migration set must describe the schema its drizzle
|
|
11
|
+
* source declares.
|
|
12
|
+
*
|
|
13
|
+
* Nothing else checked this. The previous guard shelled out to
|
|
14
|
+
* `npm-scripts/check-vps-migrations.mjs`, which is stripped from this tree, so
|
|
15
|
+
* it skipped on every run. Every other SQLite test pushes its schema from the
|
|
16
|
+
* drizzle table objects, so a migration set that has fallen behind those
|
|
17
|
+
* objects still passes the whole suite while deployments run the stale shape —
|
|
18
|
+
* which is how `mailbox.highest_modseq` shipped as `integer` for as long as it
|
|
19
|
+
* did (reader#73). SQLite column types are affinity rather than constraint, so
|
|
20
|
+
* a wrong declaration corrupts values instead of rejecting them.
|
|
21
|
+
*
|
|
22
|
+
* This is the same diff `drizzle-kit generate` takes, run in-process against
|
|
23
|
+
* each set's latest committed snapshot. A non-empty result means someone
|
|
24
|
+
* changed a schema without regenerating:
|
|
25
|
+
*
|
|
26
|
+
* npx drizzle-kit generate --config <the config named below>
|
|
27
|
+
*
|
|
28
|
+
* The schema and output paths come from the configs themselves, so a set stays
|
|
29
|
+
* covered when either moves.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = new URL("../../../", import.meta.url);
|
|
33
|
+
|
|
34
|
+
const CONFIGS = [
|
|
35
|
+
"deploy/vps/migrate/drizzle.entities.sqlite.config.ts",
|
|
36
|
+
"deploy/vps/migrate/drizzle.auth.sqlite.config.ts",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
type DrizzleConfig = { schema: string; out: string };
|
|
40
|
+
|
|
41
|
+
const loadConfig = async (path: string): Promise<DrizzleConfig> =>
|
|
42
|
+
(
|
|
43
|
+
(await import(new URL(path, REPO_ROOT).href)) as {
|
|
44
|
+
default: DrizzleConfig;
|
|
45
|
+
}
|
|
46
|
+
).default;
|
|
47
|
+
|
|
48
|
+
const latestSnapshot = (out: string): Record<string, unknown> => {
|
|
49
|
+
const dir = new URL(`${out}/`, REPO_ROOT);
|
|
50
|
+
const journal = JSON.parse(
|
|
51
|
+
readFileSync(new URL("meta/_journal.json", dir), "utf8"),
|
|
52
|
+
) as { entries: Array<{ idx: number }> };
|
|
53
|
+
const idx = Math.max(...journal.entries.map((entry) => entry.idx));
|
|
54
|
+
return JSON.parse(
|
|
55
|
+
readFileSync(
|
|
56
|
+
new URL(`meta/${String(idx).padStart(4, "0")}_snapshot.json`, dir),
|
|
57
|
+
"utf8",
|
|
58
|
+
),
|
|
59
|
+
) as Record<string, unknown>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("committed sqlite migrations", () => {
|
|
63
|
+
for (const configPath of CONFIGS) {
|
|
64
|
+
test(`${configPath} — the set matches its schema`, async () => {
|
|
65
|
+
const config = await loadConfig(configPath);
|
|
66
|
+
const schema = (await import(
|
|
67
|
+
new URL(config.schema, REPO_ROOT).href
|
|
68
|
+
)) as Record<string, unknown>;
|
|
69
|
+
|
|
70
|
+
const drift = await generateSQLiteMigration(
|
|
71
|
+
latestSnapshot(config.out) as unknown as Parameters<
|
|
72
|
+
typeof generateSQLiteMigration
|
|
73
|
+
>[0],
|
|
74
|
+
await generateSQLiteDrizzleJson(schema),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
drift,
|
|
79
|
+
[],
|
|
80
|
+
`the committed migrations in ${config.out} no longer match ${config.schema} — regenerate them with drizzle-kit generate`,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test("declare mailbox.highest_modseq as text", async () => {
|
|
86
|
+
const { out } = await loadConfig(CONFIGS[0]);
|
|
87
|
+
const snapshot = latestSnapshot(out) as {
|
|
88
|
+
tables: Record<
|
|
89
|
+
string,
|
|
90
|
+
{ columns: Record<string, { type: string; notNull: boolean }> }
|
|
91
|
+
>;
|
|
92
|
+
};
|
|
93
|
+
const column = snapshot.tables.mailbox.columns.highest_modseq;
|
|
94
|
+
|
|
95
|
+
// A mod-sequence is an unsigned 63-bit value carried as decimal digits and
|
|
96
|
+
// parsed to BigInt, and the stored cursor also takes a `<group>:<uid>`
|
|
97
|
+
// form. Numeric affinity would hand both back as numbers.
|
|
98
|
+
assert.equal(column.type, "text");
|
|
99
|
+
assert.equal(column.notNull, true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { afterEach, beforeEach, describe, test } from "node:test";
|
|
4
|
-
import { MessageSystemFlag } from "@remit/domain-enums";
|
|
5
|
-
import type Database from "better-sqlite3";
|
|
6
|
-
import {
|
|
7
|
-
type MessageDataSchema,
|
|
8
|
-
messageDataSchema,
|
|
9
|
-
} from "../schema/message-data.js";
|
|
10
|
-
import { createSqliteTestDb, type SqliteTestDb } from "../test-db-sqlite.js";
|
|
11
|
-
import { DrizzleMessageFlagRepository } from "./message-flag.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The one-time `message_flag.flag_name` rename shipped for issue #64, run
|
|
15
|
-
* against a real database rather than a hand-copied twin: the SQL is read
|
|
16
|
-
* from the committed migration so this test fails if that file drifts.
|
|
17
|
-
*
|
|
18
|
-
* Before the generated-enum fix, `MessageSystemFlag.Seen` held `Seen` and
|
|
19
|
-
* every row landed under the unprefixed spelling. `hasFlag` is an exact
|
|
20
|
-
* string match, so once the corrected code queries `\Seen` those rows go
|
|
21
|
-
* invisible — which is a re-star on unstar, and a silent no-op on
|
|
22
|
-
* mark-as-unread.
|
|
23
|
-
*/
|
|
24
|
-
const MIGRATION_SQL = readFileSync(
|
|
25
|
-
new URL(
|
|
26
|
-
"../../../../deploy/vps/migrations-sqlite/entities/0002_system_flag_wire_format.sql",
|
|
27
|
-
import.meta.url,
|
|
28
|
-
),
|
|
29
|
-
"utf8",
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const applyMigration = (sqlite: Database.Database): void => {
|
|
33
|
-
for (const statement of MIGRATION_SQL.split("--> statement-breakpoint")) {
|
|
34
|
-
sqlite.exec(statement);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const MESSAGE_ID = "00000000-0000-0000-6464-000000000001";
|
|
39
|
-
const OTHER_MESSAGE_ID = "00000000-0000-0000-6464-000000000002";
|
|
40
|
-
|
|
41
|
-
describe("message_flag wire-format migration (issue #64, sqlite)", () => {
|
|
42
|
-
let db: SqliteTestDb<MessageDataSchema>;
|
|
43
|
-
let sqlite: Database.Database;
|
|
44
|
-
let close: () => Promise<void>;
|
|
45
|
-
let repo: DrizzleMessageFlagRepository;
|
|
46
|
-
|
|
47
|
-
const insertLegacyRow = (messageId: string, flagName: string): void => {
|
|
48
|
-
sqlite
|
|
49
|
-
.prepare(
|
|
50
|
-
`INSERT INTO message_flag
|
|
51
|
-
(message_flag_id, message_id, flag_name, set_at, created_at, updated_at)
|
|
52
|
-
VALUES (?, ?, ?, 1000, 1000, 1000)`,
|
|
53
|
-
)
|
|
54
|
-
.run(`${messageId}:${flagName}`, messageId, flagName);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const flagNames = (messageId: string): string[] =>
|
|
58
|
-
(
|
|
59
|
-
sqlite
|
|
60
|
-
.prepare(
|
|
61
|
-
"SELECT flag_name FROM message_flag WHERE message_id = ? ORDER BY flag_name",
|
|
62
|
-
)
|
|
63
|
-
.all(messageId) as Array<{ flag_name: string }>
|
|
64
|
-
).map((r) => r.flag_name);
|
|
65
|
-
|
|
66
|
-
beforeEach(async () => {
|
|
67
|
-
({ db, sqlite, close } = await createSqliteTestDb(messageDataSchema));
|
|
68
|
-
repo = new DrizzleMessageFlagRepository(
|
|
69
|
-
db as unknown as ConstructorParameters<
|
|
70
|
-
typeof DrizzleMessageFlagRepository
|
|
71
|
-
>[0],
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
afterEach(async () => {
|
|
76
|
-
await close();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("a row written under the old spelling is found by the corrected enum", async () => {
|
|
80
|
-
insertLegacyRow(MESSAGE_ID, "Flagged");
|
|
81
|
-
assert.equal(
|
|
82
|
-
await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Flagged),
|
|
83
|
-
false,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
applyMigration(sqlite);
|
|
87
|
-
|
|
88
|
-
assert.equal(
|
|
89
|
-
await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Flagged),
|
|
90
|
-
true,
|
|
91
|
-
);
|
|
92
|
-
assert.deepEqual(flagNames(MESSAGE_ID), ["\\Flagged"]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("unstarring a migrated message removes the star instead of re-adding it", async () => {
|
|
96
|
-
insertLegacyRow(MESSAGE_ID, "Flagged");
|
|
97
|
-
applyMigration(sqlite);
|
|
98
|
-
|
|
99
|
-
// The toggleFlagged decision: hasFlag true => operation "remove".
|
|
100
|
-
const hadFlag = await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Flagged);
|
|
101
|
-
assert.equal(hadFlag, true, "pre-migration row must read as starred");
|
|
102
|
-
|
|
103
|
-
await repo.removeFlag(MESSAGE_ID, MessageSystemFlag.Flagged);
|
|
104
|
-
assert.equal(
|
|
105
|
-
await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Flagged),
|
|
106
|
-
false,
|
|
107
|
-
);
|
|
108
|
-
assert.deepEqual(flagNames(MESSAGE_ID), []);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("mark-as-unread on a migrated message clears the read state", async () => {
|
|
112
|
-
insertLegacyRow(MESSAGE_ID, "Seen");
|
|
113
|
-
applyMigration(sqlite);
|
|
114
|
-
|
|
115
|
-
assert.equal(await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Seen), true);
|
|
116
|
-
await repo.removeFlag(MESSAGE_ID, MessageSystemFlag.Seen);
|
|
117
|
-
assert.equal(await repo.hasFlag(MESSAGE_ID, MessageSystemFlag.Seen), false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("renames every RFC 9051 system flag", () => {
|
|
121
|
-
for (const name of ["Seen", "Answered", "Flagged", "Deleted", "Draft"]) {
|
|
122
|
-
insertLegacyRow(MESSAGE_ID, name);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
applyMigration(sqlite);
|
|
126
|
-
|
|
127
|
-
assert.deepEqual(
|
|
128
|
-
flagNames(MESSAGE_ID).sort(),
|
|
129
|
-
Object.values(MessageSystemFlag).slice().sort(),
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("leaves keyword and custom flags untouched", () => {
|
|
134
|
-
insertLegacyRow(MESSAGE_ID, "$Forwarded");
|
|
135
|
-
insertLegacyRow(MESSAGE_ID, "$Junk");
|
|
136
|
-
insertLegacyRow(MESSAGE_ID, "project-invoices");
|
|
137
|
-
|
|
138
|
-
applyMigration(sqlite);
|
|
139
|
-
|
|
140
|
-
assert.deepEqual(flagNames(MESSAGE_ID), [
|
|
141
|
-
"$Forwarded",
|
|
142
|
-
"$Junk",
|
|
143
|
-
"project-invoices",
|
|
144
|
-
]);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("is idempotent — a second and third run change nothing", () => {
|
|
148
|
-
insertLegacyRow(MESSAGE_ID, "Seen");
|
|
149
|
-
insertLegacyRow(OTHER_MESSAGE_ID, "Flagged");
|
|
150
|
-
insertLegacyRow(OTHER_MESSAGE_ID, "$Forwarded");
|
|
151
|
-
|
|
152
|
-
applyMigration(sqlite);
|
|
153
|
-
const afterFirst = [
|
|
154
|
-
...flagNames(MESSAGE_ID),
|
|
155
|
-
...flagNames(OTHER_MESSAGE_ID),
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
applyMigration(sqlite);
|
|
159
|
-
applyMigration(sqlite);
|
|
160
|
-
|
|
161
|
-
assert.deepEqual(
|
|
162
|
-
[...flagNames(MESSAGE_ID), ...flagNames(OTHER_MESSAGE_ID)],
|
|
163
|
-
afterFirst,
|
|
164
|
-
);
|
|
165
|
-
assert.deepEqual(afterFirst, ["\\Seen", "$Forwarded", "\\Flagged"]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("collapses a message already carrying both spellings to one row", () => {
|
|
169
|
-
insertLegacyRow(MESSAGE_ID, "Seen");
|
|
170
|
-
insertLegacyRow(MESSAGE_ID, "\\Seen");
|
|
171
|
-
|
|
172
|
-
applyMigration(sqlite);
|
|
173
|
-
|
|
174
|
-
assert.deepEqual(flagNames(MESSAGE_ID), ["\\Seen"]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("does not touch other messages' rows", () => {
|
|
178
|
-
insertLegacyRow(MESSAGE_ID, "Seen");
|
|
179
|
-
insertLegacyRow(OTHER_MESSAGE_ID, "Seen");
|
|
180
|
-
|
|
181
|
-
applyMigration(sqlite);
|
|
182
|
-
|
|
183
|
-
assert.deepEqual(flagNames(MESSAGE_ID), ["\\Seen"]);
|
|
184
|
-
assert.deepEqual(flagNames(OTHER_MESSAGE_ID), ["\\Seen"]);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
9
|
-
|
|
10
|
-
// The migration-check script is stripped from the open-core tree; skip there and
|
|
11
|
-
// run where it ships.
|
|
12
|
-
const hasCheckScript = existsSync(
|
|
13
|
-
resolve(repoRoot, "npm-scripts/check-vps-migrations.mjs"),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
// Fails when the committed VPS migrations (deploy/vps/migrations/*) no longer
|
|
17
|
-
// produce the schema drizzle would generate from the entity + auth schemas.
|
|
18
|
-
// See npm-scripts/check-vps-migrations.mjs for the mechanism.
|
|
19
|
-
test("committed VPS migrations match the drizzle schema", {
|
|
20
|
-
skip: !hasCheckScript,
|
|
21
|
-
}, () => {
|
|
22
|
-
assert.doesNotThrow(() => {
|
|
23
|
-
execFileSync("node", ["npm-scripts/check-vps-migrations.mjs", "--check"], {
|
|
24
|
-
cwd: repoRoot,
|
|
25
|
-
stdio: "inherit",
|
|
26
|
-
});
|
|
27
|
-
}, "committed VPS migrations are stale — run `npm run migrations:generate`");
|
|
28
|
-
});
|