@remit/drizzle-service 0.0.81 → 0.0.82

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.81",
3
+ "version": "0.0.82",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,152 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { after, describe, test } from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+ import Database from "better-sqlite3";
8
+ import { drizzle } from "drizzle-orm/better-sqlite3";
9
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
10
+
11
+ /**
12
+ * A release's `schemaVersion` and an instance's `currentSchemaVersion` have to
13
+ * be the same quantity, because the self-update consent screen subtracts one
14
+ * from the other: `schemaVersion > currentSchemaVersion` is the whole of
15
+ * "installing this release runs a migration" (#279).
16
+ *
17
+ * They are computed by two different programs from two different sources. The
18
+ * manifest sums the drizzle journals (npm-scripts/lib/update-manifest.mjs); the
19
+ * wrapper counts rows in the `__drizzle_migrations_*` tables on the live
20
+ * database (deploy/vps/remit). Nothing tied the two together, and a run that
21
+ * reported ten against a manifest's nine inverted the derivation with no test
22
+ * to catch it.
23
+ *
24
+ * This applies the shipped migration sets with the shipped migrator and asserts
25
+ * the two quantities agree: one applied row per journal entry, in exactly the
26
+ * tables the wrapper counts.
27
+ */
28
+
29
+ const REPO_ROOT = new URL("../../../", import.meta.url);
30
+
31
+ const MIGRATIONS_ROOT = new URL("deploy/vps/migrations-sqlite/", REPO_ROOT);
32
+
33
+ const read = (path: string): string =>
34
+ readFileSync(new URL(path, REPO_ROOT), "utf8");
35
+
36
+ interface MigrationSet {
37
+ set: string;
38
+ table: string;
39
+ }
40
+
41
+ /**
42
+ * The sets the migrate one-shot applies, taken from the entrypoint itself
43
+ * rather than restated here: a set added, renamed or dropped there is one this
44
+ * test then migrates and counts, instead of one it silently stops covering.
45
+ * The folder is the path inside the image, where the migrations are staged at
46
+ * the working directory; in this tree they are under deploy/vps.
47
+ */
48
+ const migrationSets = (): MigrationSet[] => {
49
+ const source = read("packages/migrate/src/run-migrate.ts");
50
+ const sets = [
51
+ ...source.matchAll(
52
+ /migrationsFolder:\s*"migrations-sqlite\/(\w+)",\s*migrationsTable:\s*"(\w+)",/g,
53
+ ),
54
+ ].map(([, set, table]) => ({ set, table }));
55
+ assert.ok(
56
+ sets.length > 0,
57
+ "no sqlite migration sets found in the migrate entrypoint",
58
+ );
59
+ return sets;
60
+ };
61
+
62
+ const journalEntries = (set: string): unknown[] => {
63
+ const journal = JSON.parse(
64
+ readFileSync(new URL(`${set}/meta/_journal.json`, MIGRATIONS_ROOT), "utf8"),
65
+ ) as { entries: unknown[] };
66
+ return journal.entries;
67
+ };
68
+
69
+ /** The tables `read_schema_version` in the wrapper sums on the live database. */
70
+ const wrapperCountedTables = (): string[] => {
71
+ const match = read("deploy/vps/remit").match(
72
+ /for t in ((?:__drizzle_migrations_\w+ ?)+); do/,
73
+ );
74
+ assert.ok(match, "the wrapper no longer sums any __drizzle_migrations table");
75
+ return match[1].trim().split(/\s+/);
76
+ };
77
+
78
+ describe("schema version accounting", () => {
79
+ const dir = mkdtempSync(join(tmpdir(), "remit-schema-version-"));
80
+ const sets = migrationSets();
81
+ const sqlite = new Database(join(dir, "remit.db"));
82
+ sqlite.pragma("journal_mode = WAL");
83
+ sqlite.pragma("foreign_keys = ON");
84
+ const db = drizzle(sqlite);
85
+ for (const { set, table } of sets) {
86
+ migrate(db, {
87
+ migrationsFolder: fileURLToPath(new URL(set, MIGRATIONS_ROOT)),
88
+ migrationsTable: table,
89
+ });
90
+ }
91
+
92
+ after(() => {
93
+ sqlite.close();
94
+ rmSync(dir, { recursive: true, force: true });
95
+ });
96
+
97
+ const rows = (table: string): number =>
98
+ (
99
+ sqlite.prepare(`SELECT count(*) AS n FROM ${table}`).get() as {
100
+ n: number;
101
+ }
102
+ ).n;
103
+
104
+ test("a fully migrated database holds one row per journal entry", () => {
105
+ for (const { set, table } of sets) {
106
+ assert.equal(
107
+ rows(table),
108
+ journalEntries(set).length,
109
+ `${table} does not hold one row per entry in the ${set} journal`,
110
+ );
111
+ }
112
+ });
113
+
114
+ // The sum is the quantity both sides publish, and the manifest derives it
115
+ // from these same journals — so this is `deriveSchemaVersion` against the
116
+ // count the wrapper reads back off a migrated instance.
117
+ test("the total equals the schema version the manifest derives", () => {
118
+ const applied = sets.reduce((total, { table }) => total + rows(table), 0);
119
+ const derived = sets.reduce(
120
+ (total, { set }) => total + journalEntries(set).length,
121
+ 0,
122
+ );
123
+ assert.equal(applied, derived);
124
+ });
125
+
126
+ // Same number, same tables. A set the migrator writes and the wrapper does
127
+ // not count is a version that reads low forever, which inverts the consent
128
+ // screen's comparison rather than failing it.
129
+ test("the wrapper counts exactly the tables the migrator writes", () => {
130
+ assert.deepEqual(
131
+ wrapperCountedTables().sort(),
132
+ sets.map(({ table }) => table).sort(),
133
+ );
134
+ });
135
+
136
+ // Forward-only: the migrator is run on every boot, and a second pass that
137
+ // re-recorded an applied migration would lift the count above the journal
138
+ // sum — the shape of the drift #279 reported.
139
+ test("a second migrate run records nothing further", () => {
140
+ const before = sets.reduce((total, { table }) => total + rows(table), 0);
141
+ for (const { set, table } of sets) {
142
+ migrate(db, {
143
+ migrationsFolder: fileURLToPath(new URL(set, MIGRATIONS_ROOT)),
144
+ migrationsTable: table,
145
+ });
146
+ }
147
+ assert.equal(
148
+ sets.reduce((total, { table }) => total + rows(table), 0),
149
+ before,
150
+ );
151
+ });
152
+ });