@coffre/db 0.1.16 → 0.1.17

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/README.md CHANGED
@@ -5,8 +5,9 @@ coffre uses in tests and local development, their migrations and the
5
5
  migrator, the helpers for what the two engines do differently, and the
6
6
  connections, Hyperdrive's included.
7
7
 
8
- A deployment does not import it. `@coffre/server` runs its migrations with
9
- `coffre-server migrate`. The server and vault open the same database with
8
+ A deployment does not import it. Its own CLI applies the migrations,
9
+ `pnpm exec coffre migrate`, before the deploy; `coffre-server migrate` does
10
+ the same for local SQLite and tests. The server and vault open the same database with
10
11
  separate logins, `coffre_runtime` and `coffre_vault_runtime`. The migrations
11
12
  grant each its own rights; only the owner runs migrations. The owner needs
12
13
  `CREATEROLE` and `CREATEDB`, and `ADMIN OPTION` on existing group roles.
package/dist/migrate.d.ts CHANGED
@@ -8,6 +8,22 @@ export declare function restrictDatabase(client: Pick<PoolClient, 'query'>): Pro
8
8
  * in the built package, where the build copies them.
9
9
  */
10
10
  export declare function migrationsFolder(engine: Engine): string;
11
+ /**
12
+ * A database a newer coffre migrated: it has applied every migration this
13
+ * version knows, and more. The ledger keeps no names, so each one it does not
14
+ * know is named by when it was made and its hash.
15
+ */
16
+ export declare class DatabaseAhead extends Error {
17
+ readonly known: string[];
18
+ readonly unknown: {
19
+ createdAt: string;
20
+ hash: string;
21
+ }[];
22
+ constructor(known: string[], unknown: {
23
+ createdAt: string;
24
+ hash: string;
25
+ }[]);
26
+ }
11
27
  /**
12
28
  * A migration run's plan: `applied` of the `total` migrations there before
13
29
  * it, and the tags of the rest, which it applies.
package/dist/migrate.js CHANGED
@@ -47,14 +47,33 @@ async function expectedMigrations(engine) {
47
47
  for (let index = 1; index < expected.length; index += 1) if (BigInt(expected[index - 1].createdAt) >= BigInt(expected[index].createdAt)) throw new Error("Drizzle migration journal timestamps must be strictly increasing");
48
48
  return expected;
49
49
  }
50
+ /**
51
+ * A database a newer coffre migrated: it has applied every migration this
52
+ * version knows, and more. The ledger keeps no names, so each one it does not
53
+ * know is named by when it was made and its hash.
54
+ */
55
+ var DatabaseAhead = class extends Error {
56
+ known;
57
+ unknown;
58
+ constructor(known, unknown) {
59
+ const made = unknown.map(({ createdAt, hash }) => `one made ${new Date(Number(createdAt)).toISOString().slice(0, 16).replace("T", " ")} UTC (${hash.slice(0, 12)})`);
60
+ super(`the database has applied ${unknown.length === 1 ? "a migration" : `${unknown.length} migrations`} this version does not know, after ${known.at(-1) ?? "none"}: ${made.join(", ")}. A newer coffre migrated it`);
61
+ this.name = "DatabaseAhead";
62
+ this.known = known;
63
+ this.unknown = unknown;
64
+ }
65
+ };
50
66
  function verifyHistory(expected, applied, requireComplete) {
51
67
  const rows = applied ?? [];
52
- if (rows.length > expected.length) throw new Error("database contains migrations that are absent from this image");
53
- if (requireComplete && rows.length !== expected.length) throw new Error(`migration history is incomplete: expected ${expected.length}, found ${rows.length}`);
54
- rows.forEach((row, index) => {
68
+ rows.slice(0, expected.length).forEach((row, index) => {
55
69
  const local = expected[index];
56
70
  if (row.created_at !== local.createdAt || row.hash !== local.hash) throw new Error(`migration history diverged at ${local.tag}; applied migrations must never be edited`);
57
71
  });
72
+ if (rows.length > expected.length) throw new DatabaseAhead(expected.map((entry) => entry.tag), rows.slice(expected.length).map((row) => ({
73
+ createdAt: row.created_at,
74
+ hash: row.hash
75
+ })));
76
+ if (requireComplete && rows.length !== expected.length) throw new Error(`migration history is incomplete: expected ${expected.length}, found ${rows.length}`);
58
77
  }
59
78
  async function postgresMigrator(url) {
60
79
  const [{ default: pg }, { drizzle }, { migrate }] = await Promise.all([
@@ -166,4 +185,4 @@ async function migrateDatabase(url, onPlan) {
166
185
  }
167
186
  }
168
187
  //#endregion
169
- export { migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase };
188
+ export { DatabaseAhead, migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/db",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,7 +74,7 @@
74
74
  "drizzle-orm": "0.45.2",
75
75
  "pg": "8.16.3",
76
76
  "pg-connection-string": "2.14.0",
77
- "@coffre/core": "0.1.16"
77
+ "@coffre/core": "0.1.17"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/node": "26.1.1",
package/src/migrate.ts CHANGED
@@ -101,27 +101,47 @@ async function expectedMigrations(engine: Engine): Promise<ExpectedMigration[]>
101
101
  return expected;
102
102
  }
103
103
 
104
+ /**
105
+ * A database a newer coffre migrated: it has applied every migration this
106
+ * version knows, and more. The ledger keeps no names, so each one it does not
107
+ * know is named by when it was made and its hash.
108
+ */
109
+ export class DatabaseAhead extends Error {
110
+ readonly known: string[];
111
+ readonly unknown: { createdAt: string; hash: string }[];
112
+
113
+ constructor(known: string[], unknown: { createdAt: string; hash: string }[]) {
114
+ const made = unknown.map(({ createdAt, hash }) => `one made ${new Date(Number(createdAt)).toISOString().slice(0, 16).replace('T', ' ')} UTC (${hash.slice(0, 12)})`);
115
+ super(
116
+ `the database has applied ${unknown.length === 1 ? 'a migration' : `${unknown.length} migrations`} this version does not know, ` +
117
+ `after ${known.at(-1) ?? 'none'}: ${made.join(', ')}. A newer coffre migrated it`,
118
+ );
119
+ this.name = 'DatabaseAhead';
120
+ this.known = known;
121
+ this.unknown = unknown;
122
+ }
123
+ }
124
+
104
125
  function verifyHistory(
105
126
  expected: ExpectedMigration[],
106
127
  applied: AppliedMigration[] | null,
107
128
  requireComplete: boolean,
108
129
  ): void {
109
130
  const rows = applied ?? [];
110
- if (rows.length > expected.length) {
111
- throw new Error('database contains migrations that are absent from this image');
112
- }
113
- if (requireComplete && rows.length !== expected.length) {
114
- throw new Error(`migration history is incomplete: expected ${expected.length}, found ${rows.length}`);
115
- }
116
-
117
- rows.forEach((row, index) => {
118
- const local = expected[index];
131
+ rows.slice(0, expected.length).forEach((row, index) => {
132
+ const local = expected[index]!;
119
133
  if (row.created_at !== local.createdAt || row.hash !== local.hash) {
120
134
  throw new Error(
121
135
  `migration history diverged at ${local.tag}; applied migrations must never be edited`,
122
136
  );
123
137
  }
124
138
  });
139
+ if (rows.length > expected.length) {
140
+ throw new DatabaseAhead(expected.map((entry) => entry.tag), rows.slice(expected.length).map((row) => ({ createdAt: row.created_at, hash: row.hash })));
141
+ }
142
+ if (requireComplete && rows.length !== expected.length) {
143
+ throw new Error(`migration history is incomplete: expected ${expected.length}, found ${rows.length}`);
144
+ }
125
145
  }
126
146
 
127
147
  async function postgresMigrator(url: string): Promise<Migrator> {