kern-migrations 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +37 -2
- package/dist/cli.d.mts +27 -0
- package/dist/cli.mjs +369 -0
- package/dist/error-BOZbdvKf.d.mts +19 -0
- package/dist/index.d.mts +185 -0
- package/dist/index.mjs +309 -0
- package/dist/migrate-CxWPJJmr.mjs +238 -0
- package/dist/target-BHaRgZJG.mjs +386 -0
- package/dist/target-iyJah0hO.d.mts +83 -0
- package/dist/testing.d.mts +60 -0
- package/dist/testing.mjs +557 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilya Semenov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kern-migrations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Migrations of PostgreSQL schemas for [kern-orm](https://github.com/IlyaSemenov/kern-orm) and its database layer `kern-db`: write each change of the database with helpers, SQL, or code, and apply the pending ones to a schema, such as the schema of each tenant.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install it beside `kern-orm` or `kern-db`:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install kern-migrations
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/migrations/0001_create_form.ts
|
|
17
|
+
import { createIndex, createTable, defineMigration } from "kern-migrations"
|
|
18
|
+
|
|
19
|
+
export default defineMigration([
|
|
20
|
+
createTable("form", {
|
|
21
|
+
id: "uuid PRIMARY KEY DEFAULT gen_random_uuid()",
|
|
22
|
+
name: "text NOT NULL",
|
|
23
|
+
}),
|
|
24
|
+
createIndex("form", ["name"]),
|
|
25
|
+
])
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { defineMigrations, migrate } from "kern-migrations"
|
|
30
|
+
|
|
31
|
+
const migrations = defineMigrations({
|
|
32
|
+
"0001_create_form": () => import("./migrations/0001_create_form.ts"),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await migrate(db, migrations)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See [Migrations](https://ilyasemenov.github.io/kern-orm/migrations/) for the helpers, steps of SQL and code, listing migrations, tenants, rolling back, the command line, and testing migrations.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { i as MigrationHistory, t as MigrationTarget } from "./target-iyJah0hO.mjs";
|
|
2
|
+
/** Options of the list module that the command `new` writes. */
|
|
3
|
+
interface MigrationListModuleOptions {
|
|
4
|
+
/** Path of the module, absolute or relative to the working directory, or its `file:` URL. */
|
|
5
|
+
readonly path: string | URL;
|
|
6
|
+
/** Whether import specifiers keep the extension of the migration modules, such as `./0001_create_form.ts`; `true` by default. */
|
|
7
|
+
readonly extensions?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Options of `runMigrationCli`. */
|
|
10
|
+
interface MigrationCliOptions {
|
|
11
|
+
/** Directory of the migration modules, where the command `new` creates one: a path, absolute or relative to the working directory, or a `file:` URL. */
|
|
12
|
+
readonly directory?: string | URL;
|
|
13
|
+
/** Module that the command `new` writes with the loaders of the migration modules of `directory`, for runtimes without `import.meta.glob`: its path or URL, or its options. */
|
|
14
|
+
readonly listModule?: string | URL | MigrationListModuleOptions;
|
|
15
|
+
/** Returns the text of a new migration module by its key, such as `0003_add_form_slug`. */
|
|
16
|
+
readonly template?: (key: string) => string;
|
|
17
|
+
/** Arguments of the command, `process.argv.slice(2)` by default. */
|
|
18
|
+
readonly args?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs the command of the arguments of the process on `target` with the migrations of `history`, such as `migrate` or `rollback 2`, and prints its result.
|
|
22
|
+
*
|
|
23
|
+
* It never rejects: it prints an error to stderr and sets `process.exitCode` to 1, so that the script closes the database after it.
|
|
24
|
+
* It does not close the database, which belongs to the script.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runMigrationCli(target: MigrationTarget, history: MigrationHistory, options?: MigrationCliOptions): Promise<void>;
|
|
27
|
+
export type { MigrationCliOptions, MigrationListModuleOptions };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { d as moduleExtensions, i as isMigrationTargetDb, l as getMigrationNumber, s as compareMigrationNumbers, w as getErrorMessage } from "./target-BHaRgZJG.mjs";
|
|
2
|
+
import { i as rollback, n as migrate, r as redo, t as getMigrationStatus } from "./migrate-CxWPJJmr.mjs";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
7
|
+
// src/cli/args.ts
|
|
8
|
+
/** Error of the arguments of the command line, which the command line reports with a hint to `--help`. */
|
|
9
|
+
var CliUsageError = class extends Error {
|
|
10
|
+
name = "CliUsageError";
|
|
11
|
+
};
|
|
12
|
+
/** Text of `--help`. */
|
|
13
|
+
const usage = `Usage: <script> <command> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
migrate [count] Apply the pending migrations, or the first count of them
|
|
17
|
+
rollback [count | all] Roll back the last applied migration, the last count of them, or all
|
|
18
|
+
redo [count] Roll back the last applied migration, or the last count of them, and apply them again
|
|
19
|
+
status List the applied and the pending migrations
|
|
20
|
+
new <name> Create the module of the next migration
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-s, --schema <name> PostgreSQL schema to migrate instead of that of the database
|
|
24
|
+
-h, --help Show this help`;
|
|
25
|
+
/**
|
|
26
|
+
* Parses the arguments of the command line, such as `["rollback", "2", "--schema", "tenant_acme"]`.
|
|
27
|
+
*
|
|
28
|
+
* It returns `help` for `--help` and for arguments without a command, and throws a `CliUsageError` for arguments of another form.
|
|
29
|
+
*/
|
|
30
|
+
function parseCliArgs(args) {
|
|
31
|
+
const { values, positionals } = parseOptions(args);
|
|
32
|
+
if (values.help) return {
|
|
33
|
+
name: "help",
|
|
34
|
+
requested: true
|
|
35
|
+
};
|
|
36
|
+
const command = positionals[0];
|
|
37
|
+
const commandArgs = positionals.slice(1);
|
|
38
|
+
const schemaName = values.schema;
|
|
39
|
+
if (schemaName === "") throw new CliUsageError("--schema requires the name of a PostgreSQL schema");
|
|
40
|
+
switch (command) {
|
|
41
|
+
case void 0: return {
|
|
42
|
+
name: "help",
|
|
43
|
+
requested: false
|
|
44
|
+
};
|
|
45
|
+
case "migrate":
|
|
46
|
+
case "redo": {
|
|
47
|
+
const arg = getCountArg(command, commandArgs);
|
|
48
|
+
return {
|
|
49
|
+
name: command,
|
|
50
|
+
count: arg === void 0 ? void 0 : parseCount(command, arg, false),
|
|
51
|
+
schemaName
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case "rollback": {
|
|
55
|
+
const arg = getCountArg(command, commandArgs);
|
|
56
|
+
return {
|
|
57
|
+
name: command,
|
|
58
|
+
count: arg === void 0 || arg === "all" ? arg : parseCount(command, arg, true),
|
|
59
|
+
schemaName
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "status":
|
|
63
|
+
if (commandArgs.length > 0) throw new CliUsageError("status takes no arguments");
|
|
64
|
+
return {
|
|
65
|
+
name: command,
|
|
66
|
+
schemaName
|
|
67
|
+
};
|
|
68
|
+
case "new":
|
|
69
|
+
if (schemaName !== void 0) throw new CliUsageError("new takes no --schema");
|
|
70
|
+
return {
|
|
71
|
+
name: command,
|
|
72
|
+
migrationName: parseMigrationName(commandArgs)
|
|
73
|
+
};
|
|
74
|
+
default: throw new CliUsageError(`Unknown command ${command}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Parses the options and the positional arguments, throwing a `CliUsageError` for an unknown option or an option without its value. */
|
|
78
|
+
function parseOptions(args) {
|
|
79
|
+
try {
|
|
80
|
+
return parseArgs({
|
|
81
|
+
args: [...args],
|
|
82
|
+
options: {
|
|
83
|
+
schema: {
|
|
84
|
+
type: "string",
|
|
85
|
+
short: "s"
|
|
86
|
+
},
|
|
87
|
+
help: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
short: "h"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
allowPositionals: true
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof TypeError && String(Reflect.get(error, "code")).startsWith("ERR_PARSE_ARGS_")) throw new CliUsageError(error.message);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Returns the optional count argument of `command`, rejecting more arguments. */
|
|
100
|
+
function getCountArg(command, args) {
|
|
101
|
+
if (args.length > 1) throw new CliUsageError(`${command} takes one count at most`);
|
|
102
|
+
return args[0];
|
|
103
|
+
}
|
|
104
|
+
/** Parses the count argument of `command`: decimal digits of a positive safe integer, so that `0x10`, `1e3`, and `02` are not counts. */
|
|
105
|
+
function parseCount(command, arg, allowsAll) {
|
|
106
|
+
const count = Number(arg);
|
|
107
|
+
if (!/^[1-9]\d*$/.test(arg) || !Number.isSafeInteger(count)) throw new CliUsageError(`The count of ${command} must be a positive integer${allowsAll ? " or all" : ""}, not ${arg}`);
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
/** Parses the name of a new migration, the `<name>` of its key `<number>_<name>`. */
|
|
111
|
+
function parseMigrationName(args) {
|
|
112
|
+
const name = args[0];
|
|
113
|
+
if (name === void 0 || args.length > 1) throw new CliUsageError("new takes the name of the migration, such as new add_form_slug");
|
|
114
|
+
if (!/^[\p{L}\p{N}_-]+$/u.test(name)) throw new CliUsageError(`The name of a migration must have only letters, digits, _, and -, such as add_form_slug, not ${name}`);
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
// src/cli/new.ts
|
|
118
|
+
/** First line of the list module, by which `new` recognizes a module that it may write. */
|
|
119
|
+
const listModuleHeader = "// Written by the new command of kern-migrations";
|
|
120
|
+
/**
|
|
121
|
+
* Text of a new migration module: `createTable` for the first migration and `addColumn` for a later one.
|
|
122
|
+
*
|
|
123
|
+
* Their names are blank, with comments that name them, and fail to load until they are filled in, so that a migration left as created never applies.
|
|
124
|
+
*/
|
|
125
|
+
function getDefaultTemplate(_key, isFirst) {
|
|
126
|
+
return isFirst ? `import { createTable, defineMigration } from "kern-migrations"
|
|
127
|
+
|
|
128
|
+
export default defineMigration([
|
|
129
|
+
createTable(/* table */ "", {
|
|
130
|
+
id: "uuid PRIMARY KEY DEFAULT gen_random_uuid()",
|
|
131
|
+
}),
|
|
132
|
+
])
|
|
133
|
+
` : `import { addColumn, defineMigration } from "kern-migrations"
|
|
134
|
+
|
|
135
|
+
export default defineMigration([
|
|
136
|
+
addColumn(/* table */ "", /* column */ "", "text"),
|
|
137
|
+
])
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Creates the module of the next migration named `name` in the directory, and then writes the list module, if any; returns the paths that it wrote.
|
|
142
|
+
*
|
|
143
|
+
* The number of the migration follows the greatest number of `keys` and of the modules of the directory, with as many digits as the key of that number.
|
|
144
|
+
* It checks the list module and builds its text before it creates the migration, so that a list module written by hand or a module that the list cannot import fails without writing anything.
|
|
145
|
+
* It writes the list from the modules of the directory listed again after it creates the migration, so that the list keeps a migration that another `new` has created meanwhile, and two migrations of one number fail.
|
|
146
|
+
*/
|
|
147
|
+
async function createMigration(keys, name, options) {
|
|
148
|
+
const { directory, listModule } = options;
|
|
149
|
+
if (listModule !== void 0) await checkListModule(listModule.path);
|
|
150
|
+
const modules = await listMigrationModules(directory);
|
|
151
|
+
const existingKeys = [...keys, ...modules.map((module) => module.key)];
|
|
152
|
+
const key = getNextKey(existingKeys, name);
|
|
153
|
+
const fileName = `${key}.ts`;
|
|
154
|
+
if (listModule !== void 0) formatListModule([...modules, {
|
|
155
|
+
key,
|
|
156
|
+
number: getMigrationNumber(key),
|
|
157
|
+
fileName
|
|
158
|
+
}], directory, listModule);
|
|
159
|
+
await mkdir(directory, { recursive: true });
|
|
160
|
+
const path = join(directory, fileName);
|
|
161
|
+
await writeFile(path, options.template(key, existingKeys.length === 0), { flag: "wx" });
|
|
162
|
+
if (listModule === void 0) return [path];
|
|
163
|
+
const text = formatListModule(await listMigrationModules(directory), directory, listModule);
|
|
164
|
+
await mkdir(dirname(listModule.path), { recursive: true });
|
|
165
|
+
await writeFile(listModule.path, text);
|
|
166
|
+
return [path, listModule.path];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Lists the migration modules of `directory` in the order of their numbers, and none of a missing directory.
|
|
170
|
+
*
|
|
171
|
+
* A module is a file with the extension of a module whose name without the extension is a key, such as `0001_create_form.ts`; other files, such as `index.ts` and migration tests `*.migration-test.ts`, are not.
|
|
172
|
+
* It rejects two modules of one key or one number, such as of two branches that took one number.
|
|
173
|
+
*/
|
|
174
|
+
async function listMigrationModules(directory) {
|
|
175
|
+
let entries;
|
|
176
|
+
try {
|
|
177
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (Reflect.get(Object(error), "code") === "ENOENT") return [];
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
const modules = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const extension = extname(entry.name);
|
|
185
|
+
if (!entry.isFile() || !moduleExtensions.has(extension)) continue;
|
|
186
|
+
const key = entry.name.slice(0, -extension.length);
|
|
187
|
+
const number = getMigrationNumber(key);
|
|
188
|
+
if (number !== void 0) modules.push({
|
|
189
|
+
key,
|
|
190
|
+
number,
|
|
191
|
+
fileName: entry.name
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
modules.sort((left, right) => compareMigrationNumbers(left.number, right.number) || (left.fileName < right.fileName ? -1 : left.fileName > right.fileName ? 1 : 0));
|
|
195
|
+
for (let index = 1; index < modules.length; index++) {
|
|
196
|
+
const previous = modules[index - 1];
|
|
197
|
+
const module = modules[index];
|
|
198
|
+
if (previous.number === module.number) throw new Error(`Migrations ${previous.fileName} and ${module.fileName} have the same number: give each migration its own number`);
|
|
199
|
+
}
|
|
200
|
+
return modules;
|
|
201
|
+
}
|
|
202
|
+
/** Returns the key of the migration named `name` that follows the greatest number of `keys`, such as `0010_add_form_slug` after `0009_create_tag`, or `0001_…` without keys. */
|
|
203
|
+
function getNextKey(keys, name) {
|
|
204
|
+
let last;
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
const number = getMigrationNumber(key);
|
|
207
|
+
if (number !== void 0 && (last === void 0 || number > last.number)) last = {
|
|
208
|
+
key,
|
|
209
|
+
number
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (last === void 0) return `0001_${name}`;
|
|
213
|
+
const digits = last.key.indexOf("_");
|
|
214
|
+
return `${String(last.number + 1n).padStart(digits, "0")}_${name}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Rejects a list module that `new` must not write: one named as a migration module, which would replace the migration of its key, and one that exists without the header line, so that `new` never overwrites a module written by hand.
|
|
218
|
+
*
|
|
219
|
+
* It reads the header line without the `\r` of a line ending converted to CRLF, such as by a checkout on Windows.
|
|
220
|
+
*/
|
|
221
|
+
async function checkListModule(path) {
|
|
222
|
+
const fileName = basename(path);
|
|
223
|
+
if (getMigrationNumber(fileName.slice(0, fileName.length - extname(fileName).length)) !== void 0) throw new Error(`Cannot write the list of migrations to ${path}: name it other than a migration, such as index.ts`);
|
|
224
|
+
let text;
|
|
225
|
+
try {
|
|
226
|
+
text = await readFile(path, "utf8");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (Reflect.get(Object(error), "code") === "ENOENT") return;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
if (text.split("\n", 1)[0].replace(/\r$/, "") !== listModuleHeader) throw new Error(`Cannot write the list of migrations to ${path}: the new command writes only a module that starts with the line "${listModuleHeader}"; add the line at the top of the module to let the command write it`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Formats the list module: the default export of the loaders of `modules` by key, with import specifiers relative to the list module.
|
|
235
|
+
*
|
|
236
|
+
* It rejects a specifier with `#`, `?`, or `%` rather than escaping it, since Node.js reads a specifier as a URL, and Bun and bundlers read it as a path.
|
|
237
|
+
* It rejects a module that has no relative path from the list module, such as on a different Windows drive.
|
|
238
|
+
*/
|
|
239
|
+
function formatListModule(modules, directory, listModule) {
|
|
240
|
+
const lines = [listModuleHeader, "export default {"];
|
|
241
|
+
for (const module of modules) {
|
|
242
|
+
const fileName = listModule.extensions ? module.fileName : module.fileName.slice(0, -extname(module.fileName).length);
|
|
243
|
+
const relativePath = relative(dirname(listModule.path), join(directory, fileName));
|
|
244
|
+
if (isAbsolute(relativePath)) throw new Error(`Cannot import ${relativePath} from the list of migrations at ${listModule.path}: put the list module on the drive of the migrations`);
|
|
245
|
+
const path = relativePath.split(sep).join("/");
|
|
246
|
+
const specifier = path.startsWith("../") ? path : `./${path}`;
|
|
247
|
+
if (/[#?%]/.test(specifier)) throw new Error(`Cannot import ${specifier} from the list of migrations: an import specifier must not have #, ?, or %`);
|
|
248
|
+
lines.push(` ${JSON.stringify(module.key)}: () => import(${JSON.stringify(specifier)}),`);
|
|
249
|
+
}
|
|
250
|
+
lines.push("}", "");
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
253
|
+
// src/cli/run.ts
|
|
254
|
+
/**
|
|
255
|
+
* Runs the command of the arguments of the process on `target` with the migrations of `history`, such as `migrate` or `rollback 2`, and prints its result.
|
|
256
|
+
*
|
|
257
|
+
* It never rejects: it prints an error to stderr and sets `process.exitCode` to 1, so that the script closes the database after it.
|
|
258
|
+
* It does not close the database, which belongs to the script.
|
|
259
|
+
*/
|
|
260
|
+
async function runMigrationCli(target, history, options = {}) {
|
|
261
|
+
const exitCode = await runCli(target, history, options, {
|
|
262
|
+
log: (text) => console.log(text),
|
|
263
|
+
error: (value) => console.error(value)
|
|
264
|
+
});
|
|
265
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
266
|
+
}
|
|
267
|
+
/** Runs the command of `options.args` and prints its result to `output`; resolves to the exit code. */
|
|
268
|
+
async function runCli(target, history, options, output) {
|
|
269
|
+
try {
|
|
270
|
+
const command = parseCliArgs(options.args ?? process.argv.slice(2));
|
|
271
|
+
switch (command.name) {
|
|
272
|
+
case "help":
|
|
273
|
+
if (!command.requested) {
|
|
274
|
+
output.error(usage);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
output.log(usage);
|
|
278
|
+
return 0;
|
|
279
|
+
case "migrate": {
|
|
280
|
+
const { database, schemaName } = resolveTarget(target, command.schemaName);
|
|
281
|
+
logKeys(output, "Applied", await migrate(database, history, {
|
|
282
|
+
count: command.count,
|
|
283
|
+
schemaName
|
|
284
|
+
}), "No pending migrations");
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
case "rollback": {
|
|
288
|
+
const { database, schemaName } = resolveTarget(target, command.schemaName);
|
|
289
|
+
logKeys(output, "Rolled back", await rollback(database, history, {
|
|
290
|
+
count: command.count,
|
|
291
|
+
schemaName
|
|
292
|
+
}), "No applied migrations");
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
case "redo": {
|
|
296
|
+
const { database, schemaName } = resolveTarget(target, command.schemaName);
|
|
297
|
+
const keys = await redo(database, history, {
|
|
298
|
+
count: command.count,
|
|
299
|
+
schemaName
|
|
300
|
+
});
|
|
301
|
+
logKeys(output, "Rolled back", keys.toReversed(), "No applied migrations");
|
|
302
|
+
logKeys(output, "Applied", keys);
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
case "status": {
|
|
306
|
+
const { database, schemaName } = resolveTarget(target, command.schemaName);
|
|
307
|
+
const status = await getMigrationStatus(database, history, { schemaName });
|
|
308
|
+
const applied = new Set(status.applied);
|
|
309
|
+
output.log(`Migrations of schema "${status.schemaName}":`);
|
|
310
|
+
for (const key of history.keys) output.log(` ${applied.has(key) ? "applied" : "pending"} ${key}`);
|
|
311
|
+
for (const key of status.unknown) output.log(` unknown ${key}`);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
case "new": {
|
|
315
|
+
if (options.directory === void 0) throw new Error("The command new requires the directory option of runMigrationCli");
|
|
316
|
+
const listModule = typeof options.listModule === "string" || options.listModule instanceof URL ? { path: options.listModule } : options.listModule;
|
|
317
|
+
const paths = await createMigration(history.keys, command.migrationName, {
|
|
318
|
+
directory: resolvePath(options.directory),
|
|
319
|
+
listModule: listModule === void 0 ? void 0 : {
|
|
320
|
+
path: resolvePath(listModule.path),
|
|
321
|
+
extensions: listModule.extensions ?? true
|
|
322
|
+
},
|
|
323
|
+
template: options.template ?? getDefaultTemplate
|
|
324
|
+
});
|
|
325
|
+
output.log(`Created ${relative(process.cwd(), paths[0])}`);
|
|
326
|
+
if (paths[1] !== void 0) output.log(`Wrote ${relative(process.cwd(), paths[1])}`);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
output.error(error instanceof CliUsageError ? `${error.message}\nRun with --help to list the commands.` : error);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Resolves the database and the PostgreSQL schema of a command: the schema of `--schema`, otherwise that of a `Db`, or `undefined` for `current_schema()`.
|
|
337
|
+
*
|
|
338
|
+
* It reads `$schemaName` of a `Db` once, and the command passes the calls the database with that schema, so that no call reads it again.
|
|
339
|
+
* When the function of the schema throws, such as of the current tenant, which a command line has no context for, the error names `--schema`.
|
|
340
|
+
*/
|
|
341
|
+
function resolveTarget(target, schemaName) {
|
|
342
|
+
if (!isMigrationTargetDb(target)) return {
|
|
343
|
+
database: target,
|
|
344
|
+
schemaName
|
|
345
|
+
};
|
|
346
|
+
const database = target.$database;
|
|
347
|
+
if (schemaName !== void 0) return {
|
|
348
|
+
database,
|
|
349
|
+
schemaName
|
|
350
|
+
};
|
|
351
|
+
try {
|
|
352
|
+
return {
|
|
353
|
+
database,
|
|
354
|
+
schemaName: target.$schemaName
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw new Error(`Cannot resolve the PostgreSQL schema of the target: ${getErrorMessage(error)}; pass --schema`, { cause: error });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/** Prints a line of `verb` for each key, or `empty` without keys. */
|
|
361
|
+
function logKeys(output, verb, keys, empty) {
|
|
362
|
+
if (keys.length === 0 && empty !== void 0) output.log(empty);
|
|
363
|
+
for (const key of keys) output.log(`${verb} ${key}`);
|
|
364
|
+
}
|
|
365
|
+
/** Resolves a path of an option, absolute or relative to the working directory, or a `file:` URL. */
|
|
366
|
+
function resolvePath(path) {
|
|
367
|
+
return resolve(path instanceof URL ? fileURLToPath(path) : path);
|
|
368
|
+
}
|
|
369
|
+
export { runMigrationCli };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Direction of a migration: `up` applies it, and `down` rolls it back. */
|
|
2
|
+
type MigrationDirection = "up" | "down";
|
|
3
|
+
/** Error of a migration: of loading its module, or of a step that failed, with the original error in `cause`. */
|
|
4
|
+
declare class MigrationError extends Error {
|
|
5
|
+
readonly name = "MigrationError";
|
|
6
|
+
/** Key of the migration. */
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** Number of the step, from 1, or `undefined` for an error of the migration as a whole, such as of loading its module. */
|
|
9
|
+
readonly step: number | undefined;
|
|
10
|
+
/** Direction of the step that failed, or `undefined` for an error of the migration as a whole or of a step that is not valid. */
|
|
11
|
+
readonly direction: MigrationDirection | undefined;
|
|
12
|
+
constructor(message: string, details: {
|
|
13
|
+
key: string;
|
|
14
|
+
step?: number;
|
|
15
|
+
direction?: MigrationDirection;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export { MigrationError as n, MigrationDirection as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { n as MigrationError, t as MigrationDirection } from "./error-BOZbdvKf.mjs";
|
|
2
|
+
import { a as MigrationHistoryOptions, c as Migration, d as MigrationStep, f as defineMigration, i as MigrationHistory, l as MigrationChange, n as MigrationTargetDb, o as MigrationLoader, r as MigrationTargetOptions, s as defineMigrations, t as MigrationTarget, u as MigrationDatabase } from "./target-iyJah0hO.mjs";
|
|
3
|
+
/** Builds a step that adds a column of a SQL definition, such as `"text NOT NULL DEFAULT ''"`, to a table, and drops it on rollback. */
|
|
4
|
+
export declare function addColumn(table: string, column: string, definition: string): MigrationStep;
|
|
5
|
+
/**
|
|
6
|
+
* Builds a step that drops a column of a table, and adds it again on rollback as `addColumn` of the same arguments does.
|
|
7
|
+
*
|
|
8
|
+
* Rollback restores neither the values of the column nor the indexes and constraints on it that its definition does not describe.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dropColumn(table: string, column: string, definition: string): MigrationStep;
|
|
11
|
+
/** Builds a step that renames a column of a table, and renames it back on rollback. */
|
|
12
|
+
export declare function renameColumn(table: string, from: string, to: string): MigrationStep;
|
|
13
|
+
/** State of a column that `changeColumn` changes from or to: the properties that change, each given in both states. */
|
|
14
|
+
interface ColumnState {
|
|
15
|
+
/** SQL type, such as `"integer"`. */
|
|
16
|
+
type?: string;
|
|
17
|
+
/** SQL expression that converts the values of the column to `type`, such as `"position::integer"`, for a change to this state. */
|
|
18
|
+
using?: string;
|
|
19
|
+
notNull?: boolean;
|
|
20
|
+
/** SQL expression of the default, such as `"0"`, or `null` for no default. */
|
|
21
|
+
default?: string | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Builds a step that changes the type, the nullability, or the default of a column from state `from` to state `to`, and changes it back on rollback.
|
|
25
|
+
*
|
|
26
|
+
* A property that changes must be in both states, and `using` of a state requires a change of type: `up` converts the values with `using` of `to`, and `down` with that of `from`.
|
|
27
|
+
* It throws when the states change nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function changeColumn(table: string, column: string, from: ColumnState, to: ColumnState): MigrationStep;
|
|
30
|
+
/** Builds a step that adds a constraint of a SQL definition, such as `"CHECK (price > 0)"`, to a table, and drops it on rollback. */
|
|
31
|
+
export declare function addConstraint(table: string, name: string, definition: string): MigrationStep;
|
|
32
|
+
/** Builds a step that drops a constraint of a table, and adds it again on rollback as `addConstraint` of the same arguments does. */
|
|
33
|
+
export declare function dropConstraint(table: string, name: string, definition: string): MigrationStep;
|
|
34
|
+
/** Builds a step that replaces the definition of a constraint of a table, keeping its name, and restores it on rollback. */
|
|
35
|
+
export declare function changeConstraint(table: string, name: string, from: string, to: string): MigrationStep;
|
|
36
|
+
/** Options of `createExtension`. */
|
|
37
|
+
interface ExtensionOptions {
|
|
38
|
+
/** PostgreSQL schema of the objects of the extension, such as `"extensions"`, which the step creates when it is missing; without it, the first schema of the search path, which is the target schema of the migration. */
|
|
39
|
+
schema?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds a step that creates a PostgreSQL extension unless it exists, with its schema when the schema is missing, and does nothing on rollback, since an extension and its schema belong to the whole database, such as to every tenant.
|
|
43
|
+
*
|
|
44
|
+
* It sends no notice for an existing extension, unlike `CREATE EXTENSION IF NOT EXISTS`, which postgres.js would log at every target.
|
|
45
|
+
* Migrations of different targets that create a missing extension at once wait for each other instead of failing on the duplicate.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createExtension(name: string, options?: ExtensionOptions): MigrationStep;
|
|
48
|
+
/** Options of `createIndex` and `dropIndex`. */
|
|
49
|
+
interface IndexOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Name of the index.
|
|
52
|
+
*
|
|
53
|
+
* Without it, the name is the one that PostgreSQL gives an index without a name, `<table>_<columns>_idx` of the columns and the `include` columns, such as `form_field_form_id_idx`.
|
|
54
|
+
* The name is required when an element is not a lowercase column name, with an optional ordering such as `DESC`, when a column repeats, or when the name would be longer than 63 bytes.
|
|
55
|
+
*/
|
|
56
|
+
name?: string;
|
|
57
|
+
unique?: boolean;
|
|
58
|
+
/** Treats `NULL` values as equal when a unique index compares keys. */
|
|
59
|
+
nullsNotDistinct?: boolean;
|
|
60
|
+
/** Index method, such as `"gin"`. */
|
|
61
|
+
using?: string;
|
|
62
|
+
/** Columns that the index stores without searching by them, each a SQL reference to a column, such as `"label"`. */
|
|
63
|
+
include?: readonly string[];
|
|
64
|
+
/** SQL condition of a partial index, such as `"deleted_at IS NULL"`. */
|
|
65
|
+
where?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Builds and drops the index without blocking writes to the table, with `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY`, when `migrate`, `rollback`, or `redo` runs outside a transaction; inside a transaction, the step builds and drops the index as without this option.
|
|
68
|
+
*
|
|
69
|
+
* A build after a failed one first drops the index that the failed build left not valid; a valid index of the name on the table counts as built.
|
|
70
|
+
* PostgreSQL rejects it for a partitioned table.
|
|
71
|
+
*/
|
|
72
|
+
concurrently?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Builds a step that creates an index of a table, and drops it on rollback.
|
|
76
|
+
*
|
|
77
|
+
* `columns` are the SQL elements of the index: columns or expressions, with their ordering, such as `["form_id", "position DESC"]`, `["lower(email)"]`, or `["(true)"]`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createIndex(table: string, columns: readonly string[], options?: IndexOptions): MigrationStep;
|
|
80
|
+
/**
|
|
81
|
+
* Builds a step that drops an index of a table, and creates it again on rollback as `createIndex` of the same arguments does.
|
|
82
|
+
*
|
|
83
|
+
* It drops the index of the name that `createIndex` gives it, and with `concurrently`, drops and builds it as `createIndex` does.
|
|
84
|
+
*/
|
|
85
|
+
export declare function dropIndex(table: string, columns: readonly string[], options?: IndexOptions): MigrationStep;
|
|
86
|
+
/**
|
|
87
|
+
* Inverts a step: its `down` becomes `up`, and its `up` becomes `down`, also of its changes outside a transaction, such as to build a helper that drops what another helper creates.
|
|
88
|
+
*
|
|
89
|
+
* It throws for a step without `down`, which gives nothing to apply, and for a value that is not a step, such as one with a misspelled `down`, which the inverted step would hide.
|
|
90
|
+
*/
|
|
91
|
+
export declare function invertStep(step: MigrationStep): MigrationStep;
|
|
92
|
+
/**
|
|
93
|
+
* Combines steps into one step: its `up` runs the `up` of the steps in order, and its `down` runs the `down` of those that have one in reverse order.
|
|
94
|
+
*
|
|
95
|
+
* A direction whose changes are all SQL scripts is one script of them; otherwise, it is a function that runs them in order.
|
|
96
|
+
* The step has no `down` when no step has one.
|
|
97
|
+
* It throws for a value that is not a step, such as one with a misspelled `down`, which the combined step would hide, and for a step with changes outside a transaction, which runs alone in its migration.
|
|
98
|
+
*/
|
|
99
|
+
export declare function combineSteps(...steps: readonly MigrationStep[]): MigrationStep;
|
|
100
|
+
/** Options of `createTable` and `dropTable`. */
|
|
101
|
+
interface TableOptions {
|
|
102
|
+
/** Constraints of the table by name, each a SQL definition, such as `{ form_field_position_key: "UNIQUE (form_id, position)" }`. */
|
|
103
|
+
constraints?: Readonly<Record<string, string>>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Builds a step that creates a table, and drops it on rollback.
|
|
107
|
+
*
|
|
108
|
+
* `columns` gives the SQL definition of each column by its name, such as `{ name: "text NOT NULL" }`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createTable(table: string, columns: Readonly<Record<string, string>>, options?: TableOptions): MigrationStep;
|
|
111
|
+
/**
|
|
112
|
+
* Builds a step that drops a table, and creates it again on rollback as `createTable` of the same arguments does.
|
|
113
|
+
*
|
|
114
|
+
* Rollback restores neither the rows of the table nor its indexes and constraints that the arguments do not describe.
|
|
115
|
+
*/
|
|
116
|
+
export declare function dropTable(table: string, columns: Readonly<Record<string, string>>, options?: TableOptions): MigrationStep;
|
|
117
|
+
/** Builds a step that renames a table, and renames it back on rollback. */
|
|
118
|
+
export declare function renameTable(from: string, to: string): MigrationStep;
|
|
119
|
+
/** Options of `migrate`. */
|
|
120
|
+
interface MigrateOptions extends MigrationTargetOptions {
|
|
121
|
+
/** Number of the first pending migrations to apply; all by default. */
|
|
122
|
+
count?: number;
|
|
123
|
+
}
|
|
124
|
+
/** Options of `rollback`. */
|
|
125
|
+
interface RollbackOptions extends MigrationTargetOptions {
|
|
126
|
+
/** Number of the last applied migrations to roll back, or `"all"`; one by default. */
|
|
127
|
+
count?: number | "all";
|
|
128
|
+
}
|
|
129
|
+
/** Options of `redo`. */
|
|
130
|
+
interface RedoOptions extends MigrationTargetOptions {
|
|
131
|
+
/** Number of the last applied migrations to roll back and apply again; one by default. */
|
|
132
|
+
count?: number;
|
|
133
|
+
}
|
|
134
|
+
/** Migrations of a history as a target has applied them. */
|
|
135
|
+
interface MigrationStatus {
|
|
136
|
+
/** PostgreSQL schema of the target that the call resolved, such as `current_schema()` of a `Database`. */
|
|
137
|
+
readonly schemaName: string;
|
|
138
|
+
/** Keys of the applied migrations of the history, in order. */
|
|
139
|
+
readonly applied: readonly string[];
|
|
140
|
+
/** Keys of the migrations of the history that the target has not applied, in order. */
|
|
141
|
+
readonly pending: readonly string[];
|
|
142
|
+
/** Applied keys that the history lacks, such as of migrations that a newer version of the application has applied. */
|
|
143
|
+
readonly unknown: readonly string[];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Applies the pending migrations of `history` to `target`, or the first `count` of them, and resolves to their keys.
|
|
147
|
+
*
|
|
148
|
+
* It applies them in one transaction, unless a migration runs outside a transaction: that migration runs alone, after the migrations before it commit, and the migrations after it run in a transaction of their own.
|
|
149
|
+
* Inside a transaction, it runs in a nested transaction, so that the migrations commit or roll back with the enclosing one, and every migration runs in it.
|
|
150
|
+
* A target that is up to date costs two statements outside a transaction; otherwise, it locks the target, so that processes that migrate it at once apply each migration once.
|
|
151
|
+
* It rejects when the target has applied a migration that the history lacks before its last migration, or a migration after a pending one; applied migrations after the last one, such as of a newer version of the application, are allowed.
|
|
152
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when it has migrations to apply, before it locks the target, since the snapshot of the transaction misses the migrations that another process applies meanwhile.
|
|
153
|
+
*/
|
|
154
|
+
export declare function migrate(target: MigrationTarget, history: MigrationHistory, options?: MigrateOptions): Promise<string[]>;
|
|
155
|
+
/**
|
|
156
|
+
* Rolls back the last applied migrations of `target`, one by default, and resolves to their keys in the order of rolling back.
|
|
157
|
+
*
|
|
158
|
+
* It rolls them back in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
159
|
+
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
160
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
161
|
+
*/
|
|
162
|
+
export declare function rollback(target: MigrationTarget, history: MigrationHistory, options?: RollbackOptions): Promise<string[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Rolls back the last applied migrations of `target` and applies them again, one by default, and resolves to their keys in order.
|
|
165
|
+
*
|
|
166
|
+
* It runs them in one transaction, unless a migration runs outside a transaction, as `migrate` applies them.
|
|
167
|
+
* It rejects when the target has applied a migration that the history lacks, since it cannot roll it back.
|
|
168
|
+
* In a transaction at `repeatable read` or `serializable`, it rejects when the target has applied migrations, before it locks the target.
|
|
169
|
+
*/
|
|
170
|
+
export declare function redo(target: MigrationTarget, history: MigrationHistory, options?: RedoOptions): Promise<string[]>;
|
|
171
|
+
/** Lists the applied and the pending migrations of `history` on `target`, and the applied migrations that the history lacks. */
|
|
172
|
+
export declare function getMigrationStatus(target: MigrationTarget, history: MigrationHistory, options?: MigrationTargetOptions): Promise<MigrationStatus>;
|
|
173
|
+
/**
|
|
174
|
+
* Quotes a PostgreSQL identifier, such as the name of a schema or a table, so that it keeps its case and characters.
|
|
175
|
+
*
|
|
176
|
+
* It throws for an empty name, which PostgreSQL rejects, so that a helper with a name left blank fails when its migration loads rather than when it runs.
|
|
177
|
+
*/
|
|
178
|
+
export declare function quoteIdentifier(name: string): string;
|
|
179
|
+
/**
|
|
180
|
+
* Quotes a string as a PostgreSQL string literal, as `quote_literal` of PostgreSQL does.
|
|
181
|
+
*
|
|
182
|
+
* It doubles quotes and backslashes, and prefixes `E` to a literal with a backslash, so that the literal keeps its value whatever `standard_conforming_strings` is.
|
|
183
|
+
*/
|
|
184
|
+
export declare function quoteLiteral(value: string): string;
|
|
185
|
+
export { type ColumnState, type ExtensionOptions, type IndexOptions, type MigrateOptions, type Migration, type MigrationChange, type MigrationDatabase, type MigrationDirection, MigrationError, type MigrationHistory, type MigrationHistoryOptions, type MigrationLoader, type MigrationStatus, type MigrationStep, type MigrationTarget, type MigrationTargetDb, type MigrationTargetOptions, type RedoOptions, type RollbackOptions, type TableOptions, defineMigration, defineMigrations };
|