@coffre/db 0.1.10 → 0.1.12
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/dist/connect.d.ts +1 -1
- package/dist/{database-CWpo5aF0.d.ts → database-CNMfHZwF.d.ts} +12 -2
- package/dist/dialect.d.ts +1 -1
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +54 -2
- package/dist/log.d.ts +1 -1
- package/dist/migrations/postgres/0001_remove_syncs.sql +13 -0
- package/dist/migrations/postgres/meta/0001_snapshot.json +1707 -0
- package/dist/migrations/postgres/meta/_journal.json +7 -0
- package/dist/migrations/sqlite/0001_remove_syncs.sql +13 -0
- package/dist/migrations/sqlite/meta/0001_snapshot.json +1575 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{relations-DqgxtlKu.js → relations-BfR3JjT1.js} +2 -22
- package/dist/{schema-Cd4WOnB5.d.ts → schema-AmvM6w0D.d.ts} +2 -362
- package/dist/schema-sqlite.d.ts +1 -369
- package/dist/schema-sqlite.js +5 -59
- package/dist/schema-version.d.ts +5 -3
- package/dist/schema-version.js +4 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +5 -79
- package/package.json +2 -2
- package/src/baseline.ts +11 -19
- package/src/database.ts +62 -1
- package/src/migrations/postgres/0001_remove_syncs.sql +13 -0
- package/src/migrations/postgres/meta/0001_snapshot.json +1707 -0
- package/src/migrations/postgres/meta/_journal.json +7 -0
- package/src/migrations/sqlite/0001_remove_syncs.sql +13 -0
- package/src/migrations/sqlite/meta/0001_snapshot.json +1575 -0
- package/src/migrations/sqlite/meta/_journal.json +7 -0
- package/src/relations.ts +1 -14
- package/src/schema-version.ts +4 -2
- package/src/schema.sqlite.ts +1 -65
- package/src/schema.ts +1 -81
package/src/baseline.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { readFile
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -15,17 +15,15 @@ import * as postgres from './schema.ts';
|
|
|
15
15
|
import * as sqlite from './schema.sqlite.ts';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* engine
|
|
20
|
-
* rather than adding a second. After the first deployment, changes become new
|
|
21
|
-
* migrations and this goes.
|
|
18
|
+
* Applied migrations are immutable. Schema changes append a migration to
|
|
19
|
+
* each engine's journal; custom safety checks belong in that new migration.
|
|
22
20
|
*
|
|
23
21
|
* A baseline is baseline/<engine>.sql with its `-- @schema` line replaced by
|
|
24
22
|
* the tables drizzle-kit generates from that engine's schema. The template
|
|
25
23
|
* holds what a schema cannot say: the audit chain's first rows, and the
|
|
26
24
|
* Postgres runtime role and its grants.
|
|
27
25
|
*
|
|
28
|
-
* `pnpm db:generate` runs this file and
|
|
26
|
+
* `pnpm db:generate` runs this file and appends pending schema changes.
|
|
29
27
|
*/
|
|
30
28
|
|
|
31
29
|
export const ENGINES: readonly Engine[] = ['postgres', 'sqlite'];
|
|
@@ -63,13 +61,13 @@ async function pending(engine: Engine): Promise<string[]> {
|
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
/** Why the
|
|
64
|
+
/** Why the migration tree is out of date: empty when it matches the schema. */
|
|
67
65
|
export async function staleness(engine: Engine): Promise<string[]> {
|
|
68
66
|
const tags = await journal(engine).then(
|
|
69
67
|
(entries) => entries.map((entry) => entry.tag),
|
|
70
68
|
() => [],
|
|
71
69
|
);
|
|
72
|
-
if (tags
|
|
70
|
+
if (tags[0] !== TAG) return [`expected the immutable ${TAG} first`];
|
|
73
71
|
const reasons = await pending(engine);
|
|
74
72
|
const { head, tail } = await template(engine);
|
|
75
73
|
const baseline = await readFile(`${migrationsFolder(engine)}/${TAG}.sql`, 'utf8');
|
|
@@ -77,28 +75,22 @@ export async function staleness(engine: Engine): Promise<string[]> {
|
|
|
77
75
|
return reasons;
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
const folder = migrationsFolder(engine);
|
|
82
|
-
await rm(folder, { recursive: true, force: true });
|
|
78
|
+
function generate(engine: Engine): void {
|
|
83
79
|
execFileSync(
|
|
84
80
|
`${packageRoot}node_modules/.bin/drizzle-kit`,
|
|
85
|
-
['generate', '--config', `drizzle.${engine}.config.ts`, '--name', '
|
|
81
|
+
['generate', '--config', `drizzle.${engine}.config.ts`, '--name', process.argv[2] ?? 'schema'],
|
|
86
82
|
{ cwd: here, stdio: 'inherit' },
|
|
87
83
|
);
|
|
88
|
-
const tables = (await readFile(`${folder}/${TAG}.sql`, 'utf8')).trimEnd();
|
|
89
|
-
const breakpoint = tables.endsWith('--> statement-breakpoint') ? '' : '--> statement-breakpoint';
|
|
90
|
-
const { head, tail } = await template(engine);
|
|
91
|
-
await writeFile(`${folder}/${TAG}.sql`, `${head}${tables}${breakpoint}\n${tail}`);
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
95
87
|
for (const engine of ENGINES) {
|
|
96
88
|
const reasons = await staleness(engine);
|
|
97
89
|
if (reasons.length === 0) {
|
|
98
|
-
console.log(`${engine}: the
|
|
90
|
+
console.log(`${engine}: the migration tree is up to date`);
|
|
99
91
|
continue;
|
|
100
92
|
}
|
|
101
|
-
console.log(`${engine}:
|
|
102
|
-
|
|
93
|
+
console.log(`${engine}: appending a migration (${reasons.length} change${reasons.length === 1 ? '' : 's'})`);
|
|
94
|
+
generate(engine);
|
|
103
95
|
}
|
|
104
96
|
}
|
package/src/database.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
1
3
|
import { getTableName, is, Table } from 'drizzle-orm';
|
|
2
4
|
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
5
|
import type pg from 'pg';
|
|
@@ -27,8 +29,67 @@ export type PoolLike = pg.Pool | {
|
|
|
27
29
|
connect: () => Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }>;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A query made on the database from inside one of its own transactions,
|
|
34
|
+
* rather than through the transaction. With one connection per call, as on
|
|
35
|
+
* Workers, it would wait for the transaction, which waits for it: a call
|
|
36
|
+
* Cloudflare cancels as hung, and nothing says why. So it is refused at
|
|
37
|
+
* once, with this, wherever the database runs.
|
|
38
|
+
*/
|
|
39
|
+
export class QueryOutsideTransaction extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super('query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it');
|
|
42
|
+
this.name = 'QueryOutsideTransaction';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
|
|
47
|
+
const inside = new AsyncLocalStorage<{ pool: PoolLike; open: boolean }>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `pool`, refusing a query or a transaction asked from inside a transaction
|
|
51
|
+
* it is already running. Work from any other flow, a concurrent request's,
|
|
52
|
+
* or what the transaction left running after it ended, goes on as before.
|
|
53
|
+
*/
|
|
54
|
+
function guarded(pool: PoolLike): PoolLike {
|
|
55
|
+
return new Proxy(pool, {
|
|
56
|
+
get(target, key) {
|
|
57
|
+
const value: unknown = Reflect.get(target, key, target);
|
|
58
|
+
if (typeof value !== 'function') return value;
|
|
59
|
+
if (key !== 'query' && key !== 'connect') return value.bind(target);
|
|
60
|
+
return (...args: unknown[]) => {
|
|
61
|
+
const open = inside.getStore();
|
|
62
|
+
if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
|
|
63
|
+
return (value as (...args: unknown[]) => unknown).apply(target, args);
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The refusal `error` is, or was caused by. */
|
|
70
|
+
function brokenRule(error: unknown): QueryOutsideTransaction | undefined {
|
|
71
|
+
for (let cause = error; cause instanceof Error; cause = cause.cause) {
|
|
72
|
+
if (cause instanceof QueryOutsideTransaction) return cause;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
export function createDatabase(pool: PoolLike): Database {
|
|
31
|
-
|
|
78
|
+
const db = drizzle(guarded(pool) as pg.Pool, { schema });
|
|
79
|
+
// Each transaction's work runs knowing which one it is in, until the transaction ends.
|
|
80
|
+
const run = db.transaction.bind(db);
|
|
81
|
+
db.transaction = (async (work, config) => {
|
|
82
|
+
const open = { pool, open: true };
|
|
83
|
+
try {
|
|
84
|
+
return await run((tx) => inside.run(open, () => work(tx)), config);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Drizzle wraps what a query throws: the rule broken is what to say.
|
|
87
|
+
throw brokenRule(error) ?? error;
|
|
88
|
+
} finally {
|
|
89
|
+
open.open = false;
|
|
90
|
+
}
|
|
91
|
+
}) as Database['transaction'];
|
|
92
|
+
return trackCommits(db);
|
|
32
93
|
}
|
|
33
94
|
|
|
34
95
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Stop old app instances before upgrading. Never silently discard a configured
|
|
2
|
+
-- destination, including archived ones. Keep the audit and sealed legacy rows.
|
|
3
|
+
LOCK TABLE syncs, sync_keys IN ACCESS EXCLUSIVE MODE;
|
|
4
|
+
--> statement-breakpoint
|
|
5
|
+
DO $$ BEGIN
|
|
6
|
+
IF EXISTS (SELECT 1 FROM syncs) OR EXISTS (SELECT 1 FROM sync_keys) THEN
|
|
7
|
+
RAISE EXCEPTION 'syncs are removed: migrate destinations to service tokens, back up and clear syncs and sync_keys before upgrading';
|
|
8
|
+
END IF;
|
|
9
|
+
END $$;
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
DROP TABLE sync_keys;
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
DROP TABLE syncs;
|