migrane 1.0.0

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.

Potentially problematic release.


This version of migrane might be problematic. Click here for more details.

package/dist/seeds.js ADDED
@@ -0,0 +1,132 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DEFAULTS } from './defaults.js';
4
+ import { checksumOf, filesIn, indexIn } from './discover.js';
5
+ import { consoleReporter } from './runner.js';
6
+ import { refuse } from './safety.js';
7
+ import { quoteIdent, sqlOver } from './sql.js';
8
+ const ensure = async (db, table) => {
9
+ await db.query(`
10
+ CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (
11
+ name TEXT PRIMARY KEY,
12
+ checksum TEXT NOT NULL DEFAULT '',
13
+ run_at TIMESTAMPTZ NOT NULL DEFAULT now()
14
+ )
15
+ `);
16
+ };
17
+ /**
18
+ * Every directory under the seeds root holding something runnable — the seed
19
+ * half of `discover()`, and the only part of this file that touches a disk.
20
+ *
21
+ * A unit composes exactly like a directory migration, `index.ts` rule included.
22
+ * `discover()` cannot find it because that enforces a `<number>-<slug>` name,
23
+ * and a unit has no number: you pick it by name, so `sequence` is `0` for all
24
+ * of them. A directory holding nothing runnable is skipped, as `discover` does.
25
+ */
26
+ export const unitsIn = (dir) => existsSync(dir)
27
+ ? readdirSync(dir, { withFileTypes: true })
28
+ .filter((entry) => entry.isDirectory())
29
+ .flatMap((entry) => {
30
+ const path = join(dir, entry.name);
31
+ const files = filesIn(path, true);
32
+ if (!files.length)
33
+ return [];
34
+ const index = indexIn(path, files);
35
+ return [
36
+ {
37
+ name: entry.name,
38
+ sequence: 0,
39
+ path,
40
+ run: index ? [index] : files,
41
+ files,
42
+ checksum: checksumOf(files),
43
+ },
44
+ ];
45
+ })
46
+ .sort((a, b) => a.name.localeCompare(b.name))
47
+ : [];
48
+ /**
49
+ * A seed plan from a config and units already loaded — `planFrom`'s twin, and
50
+ * defaulting its table from the same constant for the same reason.
51
+ */
52
+ export const seedPlanFrom = (config, units) => ({
53
+ units,
54
+ table: config.seedTable ?? DEFAULTS.seedTable,
55
+ guard: config.guard,
56
+ });
57
+ /**
58
+ * Picking the named unit out of the plan.
59
+ *
60
+ * The message says *does not exist* rather than naming a directory, because by
61
+ * here there is no directory to name — only the units this plan was handed.
62
+ */
63
+ const chosen = (plan, unit) => {
64
+ const found = plan.units.find(({ name }) => name === unit);
65
+ if (!found)
66
+ throw new Error(`seed unit "${unit}" does not exist.`);
67
+ return found;
68
+ };
69
+ export const seed = async (driver, plan, unit, reporter = consoleReporter) => {
70
+ // Guarded like `reset`, and for the same reason rather than a lesser one: a
71
+ // unit is allowed to truncate before it inserts, so "it only adds rows" is
72
+ // not something the runner can promise about somebody's fixtures.
73
+ await (plan.guard ?? refuse)(driver, 'seed writes fixtures');
74
+ const loaded = chosen(plan, unit);
75
+ await driver.session(async (session) => {
76
+ await ensure(session, plan.table);
77
+ await session.transaction(async (tx) => {
78
+ await loaded.module.up({ sql: sqlOver(tx), db: tx });
79
+ // Upserted, not inserted: seeding the same unit twice is expected, and
80
+ // should move the timestamp rather than fail on the primary key.
81
+ await tx.query(`INSERT INTO ${quoteIdent(plan.table)} (name, checksum) VALUES ($1, $2)
82
+ ON CONFLICT (name) DO UPDATE
83
+ SET checksum = excluded.checksum, run_at = now()`, [unit, loaded.checksum]);
84
+ });
85
+ });
86
+ reporter.line(`seeded ${unit}`);
87
+ };
88
+ export const unseed = async (driver, plan, unit, reporter = consoleReporter) => {
89
+ await (plan.guard ?? refuse)(driver, 'unseed deletes rows a fixture set made');
90
+ const loaded = chosen(plan, unit);
91
+ const revert = loaded.module.down;
92
+ if (!revert)
93
+ throw new Error(`seed unit "${unit}" has no "down".`);
94
+ await driver.session(async (session) => {
95
+ await ensure(session, plan.table);
96
+ await session.transaction(async (tx) => {
97
+ await revert({ sql: sqlOver(tx), db: tx });
98
+ await tx.query(`DELETE FROM ${quoteIdent(plan.table)} WHERE name = $1`, [
99
+ unit,
100
+ ]);
101
+ });
102
+ });
103
+ reporter.line(`unseeded ${unit}`);
104
+ };
105
+ /**
106
+ * What this database was seeded with, and whether that unit still matches disk.
107
+ *
108
+ * Read from the database rather than inferred from a checkout, which is the
109
+ * point: the plan tells you which units exist, only the row tells you which one
110
+ * you are looking at. A unit the plan no longer carries still reports — it is
111
+ * how a database seeded from a deleted branch explains itself, and how an image
112
+ * explains fixtures that were dropped from a later build.
113
+ */
114
+ export const seedStatus = async (driver, plan, reporter = consoleReporter) => {
115
+ await ensure(driver, plan.table);
116
+ const rows = await driver.query(`SELECT name, checksum, run_at FROM ${quoteIdent(plan.table)} ORDER BY run_at DESC`);
117
+ const present = new Map(plan.units.map(({ name, checksum }) => [name, checksum]));
118
+ for (const row of rows) {
119
+ const held = present.get(row.name);
120
+ const state = held === undefined
121
+ ? 'unit is gone'
122
+ : held === row.checksum
123
+ ? 'current'
124
+ : 'changed since';
125
+ const on = new Date(row.run_at)
126
+ .toISOString()
127
+ .slice(0, 16)
128
+ .replace('T', ' ');
129
+ reporter.line(` seeded ${row.name} ${on} (${state})`);
130
+ }
131
+ return rows;
132
+ };
package/dist/sql.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Fragment, Queryable, Sql } from './types.js';
2
+ /**
3
+ * Splice text into a statement verbatim.
4
+ *
5
+ * DDL is mostly things that cannot be bind parameters — table names, types,
6
+ * defaults, whole constraint bodies — so this has to exist. Making it a
7
+ * function call rather than the default behaviour means every unescaped
8
+ * interpolation is visible in a diff.
9
+ */
10
+ export declare const raw: (text: string) => Fragment;
11
+ /**
12
+ * Quote an identifier as a plain string.
13
+ *
14
+ * Separate from {@link id} because the package builds statements of its own —
15
+ * the bookkeeping table — where a `Fragment` would have to be unwrapped again
16
+ * to reach the text.
17
+ */
18
+ export declare const quoteIdent: (name: string) => string;
19
+ /** Quote an identifier. `id('user table')` is `"user table"`. */
20
+ export declare const id: (name: string) => Fragment;
21
+ /** Join fragments — a column list, a set of constraints. */
22
+ export declare const join: (parts: readonly Fragment[], separator?: string) => Fragment;
23
+ export interface Statement {
24
+ text: string;
25
+ params: unknown[];
26
+ }
27
+ /**
28
+ * Turns a template into a statement and its bind parameters.
29
+ *
30
+ * Anything that is not a {@link Fragment} becomes a `$n` placeholder, so the
31
+ * ordinary way to interpolate a value is also the safe one and injecting SQL
32
+ * takes a deliberate `sql.raw`.
33
+ */
34
+ export declare const build: (strings: TemplateStringsArray, values: readonly unknown[]) => Statement;
35
+ /**
36
+ * The tagged template bound to one connection.
37
+ *
38
+ * A template with no interpolated values sends no bind parameters, which is
39
+ * what lets a migration write several statements in one template: PostgreSQL
40
+ * only restricts a request to a single statement once the extended protocol is
41
+ * in play, and that starts at the first parameter.
42
+ */
43
+ export declare const sqlOver: (db: Queryable) => Sql;
package/dist/sql.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The runtime half of {@link Fragment}'s brand.
3
+ *
4
+ * `types.ts` declares the property with a `unique symbol` that has no value, so
5
+ * the brand costs nothing at run time and cannot be forged by writing an object
6
+ * literal. This is the only place that gap is bridged.
7
+ */
8
+ const MARK = Symbol('migrane.fragment');
9
+ const isFragment = (value) => typeof value === 'object' && value !== null && MARK in value;
10
+ /**
11
+ * Splice text into a statement verbatim.
12
+ *
13
+ * DDL is mostly things that cannot be bind parameters — table names, types,
14
+ * defaults, whole constraint bodies — so this has to exist. Making it a
15
+ * function call rather than the default behaviour means every unescaped
16
+ * interpolation is visible in a diff.
17
+ */
18
+ export const raw = (text) => ({ [MARK]: text });
19
+ /**
20
+ * Quote an identifier as a plain string.
21
+ *
22
+ * Separate from {@link id} because the package builds statements of its own —
23
+ * the bookkeeping table — where a `Fragment` would have to be unwrapped again
24
+ * to reach the text.
25
+ */
26
+ export const quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
27
+ /** Quote an identifier. `id('user table')` is `"user table"`. */
28
+ export const id = (name) => raw(quoteIdent(name));
29
+ /** Join fragments — a column list, a set of constraints. */
30
+ export const join = (parts, separator = ', ') => raw(parts.map((part) => part[MARK]).join(separator));
31
+ /**
32
+ * Turns a template into a statement and its bind parameters.
33
+ *
34
+ * Anything that is not a {@link Fragment} becomes a `$n` placeholder, so the
35
+ * ordinary way to interpolate a value is also the safe one and injecting SQL
36
+ * takes a deliberate `sql.raw`.
37
+ */
38
+ export const build = (strings, values) => {
39
+ let text = strings[0] ?? '';
40
+ const params = [];
41
+ for (const [index, value] of values.entries()) {
42
+ if (isFragment(value))
43
+ text += value[MARK];
44
+ else {
45
+ params.push(value);
46
+ text += `$${params.length}`;
47
+ }
48
+ text += strings[index + 1] ?? '';
49
+ }
50
+ return { text, params };
51
+ };
52
+ /**
53
+ * The tagged template bound to one connection.
54
+ *
55
+ * A template with no interpolated values sends no bind parameters, which is
56
+ * what lets a migration write several statements in one template: PostgreSQL
57
+ * only restricts a request to a single statement once the extended protocol is
58
+ * in play, and that starts at the first parameter.
59
+ */
60
+ export const sqlOver = (db) => {
61
+ const tag = (strings, ...values) => {
62
+ const { text, params } = build(strings, values);
63
+ return db.query(text, params.length ? params : undefined);
64
+ };
65
+ return Object.assign(tag, { raw, id, join });
66
+ };
@@ -0,0 +1,52 @@
1
+ import type { Queryable, Row } from './types.js';
2
+ /**
3
+ * The bookkeeping table, and the two questions it answers: what has run, and
4
+ * whether what ran is still what is on disk.
5
+ */
6
+ export interface Applied extends Row {
7
+ name: string;
8
+ checksum: string;
9
+ run_at: string;
10
+ }
11
+ /**
12
+ * A migration was edited after it was applied.
13
+ *
14
+ * A class rather than a plain `Error` so a caller that *knows* its database is
15
+ * disposable — a test lane, a scratch container — can recover from exactly this
16
+ * and nothing else. Matching on the message would catch the next error whose
17
+ * wording happened to look similar, which is how a recovery path ends up
18
+ * dropping a schema it was never meant to touch.
19
+ */
20
+ export declare class ChangedError extends Error {
21
+ readonly migration: string;
22
+ constructor(migration: string);
23
+ }
24
+ /**
25
+ * Written on first contact rather than by a setup command, so a database that
26
+ * has never been migrated needs no preparation — `status` against an empty one
27
+ * answers "everything is pending" instead of failing on a missing table.
28
+ */
29
+ export declare const ensure: (db: Queryable, table: string) => Promise<void>;
30
+ export declare const applied: (db: Queryable, table: string) => Promise<Applied[]>;
31
+ /**
32
+ * Refuses a migration that changed after it was applied.
33
+ *
34
+ * A migration already run is a fact about the database, and editing one makes
35
+ * the file stop describing what the database actually holds — silently, and
36
+ * only on the machines that already ran it. So it is caught before anything
37
+ * else runs.
38
+ *
39
+ * A name recorded but absent from disk is *not* an error: deleting or renaming
40
+ * migrations is what squashing them into one looks like, and refusing that
41
+ * would make a legitimate operation impossible.
42
+ */
43
+ export declare const verify: (rows: readonly Applied[], migrations: readonly {
44
+ name: string;
45
+ checksum: string;
46
+ }[]) => void;
47
+ export declare const record: (db: Queryable, table: string, entry: {
48
+ name: string;
49
+ checksum: string;
50
+ duration: number;
51
+ }) => Promise<void>;
52
+ export declare const forget: (db: Queryable, table: string, name: string) => Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { quoteIdent } from './sql.js';
2
+ /**
3
+ * A migration was edited after it was applied.
4
+ *
5
+ * A class rather than a plain `Error` so a caller that *knows* its database is
6
+ * disposable — a test lane, a scratch container — can recover from exactly this
7
+ * and nothing else. Matching on the message would catch the next error whose
8
+ * wording happened to look similar, which is how a recovery path ends up
9
+ * dropping a schema it was never meant to touch.
10
+ */
11
+ export class ChangedError extends Error {
12
+ migration;
13
+ constructor(migration) {
14
+ super(`"${migration}" changed after it was applied.\n` +
15
+ ` Reset the database if it is disposable, or add a new migration if it is not.`);
16
+ this.migration = migration;
17
+ this.name = 'ChangedError';
18
+ }
19
+ }
20
+ /**
21
+ * Written on first contact rather than by a setup command, so a database that
22
+ * has never been migrated needs no preparation — `status` against an empty one
23
+ * answers "everything is pending" instead of failing on a missing table.
24
+ */
25
+ export const ensure = async (db, table) => {
26
+ await db.query(`
27
+ CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (
28
+ name TEXT PRIMARY KEY,
29
+ checksum TEXT NOT NULL DEFAULT '',
30
+ run_at TIMESTAMPTZ NOT NULL DEFAULT now(),
31
+ duration_ms INTEGER
32
+ )
33
+ `);
34
+ };
35
+ export const applied = async (db, table) => {
36
+ await ensure(db, table);
37
+ return db.query(`SELECT name, checksum, run_at FROM ${quoteIdent(table)} ORDER BY run_at, name`);
38
+ };
39
+ /**
40
+ * Refuses a migration that changed after it was applied.
41
+ *
42
+ * A migration already run is a fact about the database, and editing one makes
43
+ * the file stop describing what the database actually holds — silently, and
44
+ * only on the machines that already ran it. So it is caught before anything
45
+ * else runs.
46
+ *
47
+ * A name recorded but absent from disk is *not* an error: deleting or renaming
48
+ * migrations is what squashing them into one looks like, and refusing that
49
+ * would make a legitimate operation impossible.
50
+ */
51
+ export const verify = (rows, migrations) => {
52
+ const disk = new Map(migrations.map(({ name, checksum }) => [name, checksum]));
53
+ for (const row of rows) {
54
+ const current = disk.get(row.name);
55
+ if (current === undefined || current === row.checksum)
56
+ continue;
57
+ throw new ChangedError(row.name);
58
+ }
59
+ };
60
+ export const record = async (db, table, entry) => {
61
+ await db.query(`INSERT INTO ${quoteIdent(table)} (name, checksum, duration_ms) VALUES ($1, $2, $3)`, [entry.name, entry.checksum, entry.duration]);
62
+ };
63
+ export const forget = async (db, table, name) => {
64
+ await db.query(`DELETE FROM ${quoteIdent(table)} WHERE name = $1`, [name]);
65
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The vocabulary everything downstream is written against. Nothing here names a
3
+ * database, an ORM or an application: a consumer supplies a {@link Driver} and
4
+ * the runner never learns what it is talking to.
5
+ */
6
+ /** A result row. */
7
+ export type Row = Record<string, unknown>;
8
+ /**
9
+ * Anything a statement can be sent to — a pool, a pinned connection, or a
10
+ * connection inside a transaction. A migration body cannot tell them apart.
11
+ *
12
+ * Omitting `values` is meaningful: with no bind parameters PostgreSQL uses the
13
+ * simple query protocol, which is what lets one template carry several
14
+ * statements separated by `;`. Pass a parameter and exactly one is legal.
15
+ */
16
+ export interface Queryable {
17
+ query<R extends Row = Row>(text: string, values?: readonly unknown[]): Promise<R[]>;
18
+ }
19
+ /**
20
+ * One pinned connection.
21
+ *
22
+ * Separate from {@link Driver} because a transaction and a session-level lock
23
+ * are only meaningful on a connection that stays the same between statements,
24
+ * and a pool hands out whichever is free.
25
+ */
26
+ export interface Session extends Queryable {
27
+ transaction<T>(run: (tx: Queryable) => Promise<T>): Promise<T>;
28
+ /**
29
+ * Hold an exclusive lock named by `key` for the length of `run`, releasing it
30
+ * however `run` ends.
31
+ *
32
+ * Must **block** rather than fail when the lock is held: whoever holds it is
33
+ * applying the migrations this process wants applied, so waiting is correct
34
+ * and failing fast turns a wait into a red deploy.
35
+ */
36
+ lock<T>(key: number, run: () => Promise<T>): Promise<T>;
37
+ }
38
+ /**
39
+ * How the runner reaches a database. Implement these and everything else in
40
+ * this package works — `drivers/pg.ts` is the reference, at under a hundred
41
+ * lines.
42
+ */
43
+ export interface Driver extends Queryable {
44
+ /**
45
+ * The machine these coordinates reach: a hostname, an address, or `''` for a
46
+ * unix socket.
47
+ *
48
+ * Required rather than optional, because it is what the destructive commands
49
+ * refuse on. A driver that could omit it would silently opt out of that
50
+ * refusal. State it even when it is `'localhost'`.
51
+ */
52
+ readonly host: string;
53
+ /** Pin one connection for the length of `run`. */
54
+ session<T>(run: (session: Session) => Promise<T>): Promise<T>;
55
+ close(): Promise<void>;
56
+ }
57
+ /**
58
+ * SQL spliced verbatim rather than bound as a parameter.
59
+ *
60
+ * A branded object rather than a string, so reaching for the escape is visible
61
+ * in a diff instead of being what happens by default.
62
+ */
63
+ declare const FRAGMENT: unique symbol;
64
+ export interface Fragment {
65
+ readonly [FRAGMENT]: string;
66
+ }
67
+ /**
68
+ * The tagged template a migration writes against. Interpolations become bind
69
+ * parameters unless they are {@link Fragment}s, so the ordinary way to write a
70
+ * value is also the safe one.
71
+ */
72
+ export interface Sql {
73
+ <R extends Row = Row>(strings: TemplateStringsArray, ...values: readonly unknown[]): Promise<R[]>;
74
+ /** Splice text in unescaped, for DDL the template cannot express. */
75
+ raw: (text: string) => Fragment;
76
+ /** Quote an identifier — `sql.id('user table')` is `"user table"`. */
77
+ id: (name: string) => Fragment;
78
+ /** Join fragments — a column list, a set of constraints. */
79
+ join: (parts: readonly Fragment[], separator?: string) => Fragment;
80
+ }
81
+ /** What every migration and seed part receives. */
82
+ export interface Context {
83
+ sql: Sql;
84
+ /** The connection underneath, for what the template cannot say. */
85
+ db: Queryable;
86
+ }
87
+ /**
88
+ * What a migration file exports.
89
+ *
90
+ * `down` is optional: many migrations have no honest reverse, and requiring one
91
+ * only ever produces an empty body that lies about being reversible.
92
+ */
93
+ export interface Part {
94
+ up: (context: Context) => Promise<void>;
95
+ down?: (context: Context) => Promise<void>;
96
+ }
97
+ /**
98
+ * Export `transaction = false` to opt out of the wrapping transaction, for
99
+ * statements PostgreSQL refuses to run inside one such as
100
+ * `CREATE INDEX CONCURRENTLY`.
101
+ *
102
+ * The cost is stated where it is taken: an untransacted migration that fails
103
+ * half way leaves the schema changed and its row unwritten.
104
+ */
105
+ export interface Transacted {
106
+ transaction?: boolean;
107
+ }
108
+ /** A migration file's module, once loaded. */
109
+ export type Module = Part & Transacted;
110
+ /** One migration, found on disk but not yet loaded. */
111
+ export interface Discovered {
112
+ /** The storage key — the file or directory name, without extension. */
113
+ name: string;
114
+ /** The leading number, which orders it. */
115
+ sequence: number;
116
+ /** Absolute path of the file, or of the directory. */
117
+ path: string;
118
+ /**
119
+ * What actually runs, in order: one entry for a file or a directory with an
120
+ * `index.ts`, every part in path order for a directory without one.
121
+ */
122
+ run: readonly string[];
123
+ /**
124
+ * Every file the migration is made of, including parts reached only through
125
+ * an `index.ts` — the checksum has to cover what it *executes*, not what
126
+ * discovery happened to open.
127
+ */
128
+ files: readonly string[];
129
+ /** Hash of all of {@link files}, so editing an applied migration is refused. */
130
+ checksum: string;
131
+ }
132
+ /**
133
+ * A migration, loaded and ready to run. Carries no paths: the same runner
134
+ * applies it from a laptop and from an image where the files no longer exist.
135
+ */
136
+ export interface Migration extends Omit<Discovered, 'files' | 'path' | 'run'> {
137
+ module: Module;
138
+ }
139
+ /** Run before any pending migration, in declaration order. */
140
+ export type Hook = (context: Context) => Promise<void>;
141
+ /**
142
+ * What every destructive command runs through before it touches anything.
143
+ *
144
+ * **Throws to refuse** — the same shape `refuse()` has, so a policy wanting the
145
+ * default plus one more rule can call it rather than reimplement it:
146
+ *
147
+ * ```ts
148
+ * guard: async (driver, what) => {
149
+ * if (await isDisposable(driver)) return;
150
+ *
151
+ * refuse(driver, what);
152
+ * },
153
+ * ```
154
+ *
155
+ * @param what what the command does, as a clause — `reset drops every table`.
156
+ */
157
+ export type Policy = (driver: Driver, what: string) => void | Promise<void>;
158
+ /** What a consumer's `database/config.ts` exports. */
159
+ export interface Config {
160
+ /**
161
+ * Directories holding migrations, in order. Inside a root, a `.ts` or `.sql`
162
+ * file is one migration and so is a directory.
163
+ */
164
+ dirs: readonly string[];
165
+ /** How to reach the database. A factory, so `status` can skip opening one. */
166
+ driver: () => Driver | Promise<Driver>;
167
+ /** Where seed units live, one directory per unit. Omit for no seeds. */
168
+ seeds?: string;
169
+ /**
170
+ * Where `migrane manifest` writes. Omit it and the command says so rather
171
+ * than inventing a path — `entryFor` writes every specifier relative to this
172
+ * destination, so guessing where it goes would guess what is in it.
173
+ */
174
+ manifest?: string;
175
+ /** Bookkeeping table names. Default to `migrations` and `seeds`. */
176
+ table?: string;
177
+ seedTable?: string;
178
+ /** The schema `reset` drops and recreates. Defaults to `public`. */
179
+ schema?: string;
180
+ /**
181
+ * Who may run a destructive command against this database.
182
+ *
183
+ * Omit it and the built-in refusal applies. Declare one and it replaces both
184
+ * built-in rules, `NODE_ENV=production` included — which is how a deployed
185
+ * environment that can prove it is disposable rebuilds its own fixtures.
186
+ */
187
+ guard?: Policy;
188
+ /**
189
+ * Run once before pending migrations, and even when none are pending. Not
190
+ * before `status` or `reset`, neither of which has business writing DDL.
191
+ *
192
+ * The seam for anything you want true *before* a migration but do not want to
193
+ * write as one: a synced type, a search path, an extension. Also the only
194
+ * place the runner executes code it did not discover.
195
+ */
196
+ before?: readonly Hook[];
197
+ }
198
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The vocabulary everything downstream is written against. Nothing here names a
3
+ * database, an ORM or an application: a consumer supplies a {@link Driver} and
4
+ * the runner never learns what it is talking to.
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "migrane",
3
+ "version": "1.0.0",
4
+ "description": "A SQL migration runner that knows nothing about your application.",
5
+ "keywords": [
6
+ "sql",
7
+ "migration",
8
+ "migrations",
9
+ "migrate",
10
+ "postgres",
11
+ "postgresql",
12
+ "database",
13
+ "schema",
14
+ "seed"
15
+ ],
16
+ "author": {
17
+ "name": "eishexac",
18
+ "url": "https://existin.space",
19
+ "email": "hexac@existin.space"
20
+ },
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/eishexac/migrane.git"
28
+ },
29
+ "homepage": "https://github.com/eishexac/migrane",
30
+ "bugs": "https://github.com/eishexac/migrane/issues",
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "bin": {
34
+ "migrane": "./bin/migrane.js"
35
+ },
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "./drivers/pg": {
42
+ "types": "./dist/drivers/pg.d.ts",
43
+ "default": "./dist/drivers/pg.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "bin",
49
+ "dist",
50
+ "LICENSE",
51
+ "README.md"
52
+ ],
53
+ "peerDependencies": {
54
+ "pg": "^8.23.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "pg": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@arethetypeswrong/cli": "^0.18.5",
63
+ "@changesets/changelog-github": "^1.0.0",
64
+ "@changesets/cli": "^3.0.1",
65
+ "@eslint/js": "^10.0.1",
66
+ "@testcontainers/postgresql": "^12.1.0",
67
+ "@types/node": "^24.10.15",
68
+ "@types/pg": "^8.23.1",
69
+ "@vitest/coverage-v8": "^4.1.9",
70
+ "eslint": "^10.5.0",
71
+ "eslint-config-prettier": "^10.1.8",
72
+ "globals": "^17.6.0",
73
+ "pg": "^8.23.0",
74
+ "prettier": "^3.8.4",
75
+ "publint": "^0.3.24",
76
+ "rimraf": "^6.1.3",
77
+ "typescript": "^6.0.3",
78
+ "typescript-eslint": "^8.61.1",
79
+ "vitest": "^4.1.9"
80
+ },
81
+ "engines": {
82
+ "node": ">=22.18",
83
+ "bun": ">=1.0"
84
+ },
85
+ "scripts": {
86
+ "build": "rimraf dist && tsc -p tsconfig.build.json",
87
+ "format": "prettier --write .",
88
+ "lint": "eslint .",
89
+ "type:check": "tsc --noEmit",
90
+ "test": "vitest run",
91
+ "test:coverage": "vitest run --coverage",
92
+ "pack:check": "pnpm pack --out package.tgz && publint package.tgz && attw package.tgz --profile esm-only && rimraf package.tgz"
93
+ }
94
+ }