@schemavaults/dbh 0.10.2 → 0.11.1
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 +15 -1
- package/dist/utils/validateMigrationDirectory.d.ts +35 -0
- package/dist/utils/validateMigrationDirectory.js +157 -0
- package/dist/utils/validateMigrationDirectory.js.map +1 -0
- package/dist-cli/cli.js +21 -21
- package/package.json +2 -2
- package/.claude/hooks/install-deps-in-fresh-environment.sh +0 -5
- package/.claude/settings.json +0 -15
- package/CLAUDE.md +0 -41
- package/eslint.config.cjs +0 -56
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Ensure that you have both `postgres` and a `postgres-ws-proxy` containers runnin
|
|
|
20
20
|
|
|
21
21
|
You'll likely want to replace the `build:` sections for the services in the e2e test example `.yml` file with `image:`. For example, use `image: postgres:17.7` for the `postgres` service. For the proxy, you can pull the docker image from `ghcr.io/schemavaults/dbh/postgres-ws-proxy`; use the version number equal to your `@schemavaults/dbh` npm package installation:
|
|
22
22
|
```md
|
|
23
|
-
# NPM Package: @schemavaults/dbh@0.
|
|
23
|
+
# NPM Package: @schemavaults/dbh@0.11.1 => ghcr.io/schemavaults/dbh/postgres-ws-proxy:0.11.1
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### In your application server code
|
|
@@ -40,6 +40,20 @@ npx @schemavaults/dbh --help
|
|
|
40
40
|
# or `bun run cli --help` if you have the dbh source repository as your working directory
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
#### Validate the shape of a migrations directory
|
|
44
|
+
```bash
|
|
45
|
+
# assert the migrations directory is well-formed:
|
|
46
|
+
# - non-empty
|
|
47
|
+
# - every file is prefixed with a 5-digit migration number (e.g. 00000-my-migration.ts)
|
|
48
|
+
# - every module exports an up() and down() function
|
|
49
|
+
# - there are no duplicate migration numbers (branch collisions, e.g. 00040-a.ts and 00040-b.ts)
|
|
50
|
+
# exits 0 when the directory is valid, non-zero otherwise.
|
|
51
|
+
bunx @schemavaults/dbh validate-migration-directory ./src/db/migrations
|
|
52
|
+
|
|
53
|
+
# treat duplicate migration numbers as warnings instead of errors (still exits 0)
|
|
54
|
+
bunx @schemavaults/dbh validate-migration-directory ./src/db/migrations --duplicates-as-warnings
|
|
55
|
+
```
|
|
56
|
+
|
|
43
57
|
#### Build example database migrations with the CLI
|
|
44
58
|
```bash
|
|
45
59
|
mkdir ./tests/tmp
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type MigrationValidationLevel = "error" | "warning";
|
|
2
|
+
export interface MigrationValidationIssue {
|
|
3
|
+
level: MigrationValidationLevel;
|
|
4
|
+
message: string;
|
|
5
|
+
/** The offending file name (relative to the migration directory), if any. */
|
|
6
|
+
file?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ValidateMigrationDirectoryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* When true, duplicate migration numbers are reported as warnings instead of
|
|
11
|
+
* errors (they will not, on their own, cause validation to fail).
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
duplicatesAsWarnings?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidateMigrationDirectoryResult {
|
|
17
|
+
/** True when there are no `error`-level issues. */
|
|
18
|
+
ok: boolean;
|
|
19
|
+
/** The absolute path that was validated. */
|
|
20
|
+
directory: string;
|
|
21
|
+
/** The migration file names that were considered (relative to `directory`). */
|
|
22
|
+
migrationFiles: string[];
|
|
23
|
+
issues: MigrationValidationIssue[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates the shape of an opinionated Kysely migrations directory.
|
|
27
|
+
*
|
|
28
|
+
* Asserts that the directory:
|
|
29
|
+
* - exists and is non-empty,
|
|
30
|
+
* - contains only files prefixed with a 5-digit migration number,
|
|
31
|
+
* - has no duplicate migration numbers (branch collisions), and
|
|
32
|
+
* - exports an `up()` and `down()` function from every migration module.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateMigrationDirectory(directory: string, options?: ValidateMigrationDirectoryOptions): Promise<ValidateMigrationDirectoryResult>;
|
|
35
|
+
export default validateMigrationDirectory;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
/**
|
|
5
|
+
* Extensions that are treated as migration modules. `.d.ts` declaration files
|
|
6
|
+
* are explicitly ignored (handled separately below).
|
|
7
|
+
*/
|
|
8
|
+
const MIGRATION_FILE_EXTENSIONS = [
|
|
9
|
+
".ts",
|
|
10
|
+
".mts",
|
|
11
|
+
".cts",
|
|
12
|
+
".js",
|
|
13
|
+
".mjs",
|
|
14
|
+
".cjs",
|
|
15
|
+
];
|
|
16
|
+
/** Migration file names must begin with exactly 5 digits (e.g. 00000-foo.ts). */
|
|
17
|
+
const MIGRATION_PREFIX_REGEX = /^(\d{5})(?:\D|$)/;
|
|
18
|
+
function isMigrationFile(fileName) {
|
|
19
|
+
if (fileName.startsWith(".")) {
|
|
20
|
+
// Ignore hidden/dotfiles.
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (fileName.endsWith(".d.ts")) {
|
|
24
|
+
// Ignore TypeScript declaration files.
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return MIGRATION_FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates the shape of an opinionated Kysely migrations directory.
|
|
31
|
+
*
|
|
32
|
+
* Asserts that the directory:
|
|
33
|
+
* - exists and is non-empty,
|
|
34
|
+
* - contains only files prefixed with a 5-digit migration number,
|
|
35
|
+
* - has no duplicate migration numbers (branch collisions), and
|
|
36
|
+
* - exports an `up()` and `down()` function from every migration module.
|
|
37
|
+
*/
|
|
38
|
+
export async function validateMigrationDirectory(directory, options = {}) {
|
|
39
|
+
const { duplicatesAsWarnings = false } = options;
|
|
40
|
+
const resolved = path.resolve(directory);
|
|
41
|
+
const issues = [];
|
|
42
|
+
if (!fs.existsSync(resolved)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
directory: resolved,
|
|
46
|
+
migrationFiles: [],
|
|
47
|
+
issues: [
|
|
48
|
+
{
|
|
49
|
+
level: "error",
|
|
50
|
+
message: `Migration directory does not exist: ${resolved}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const stat = fs.statSync(resolved);
|
|
56
|
+
if (!stat.isDirectory()) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
directory: resolved,
|
|
60
|
+
migrationFiles: [],
|
|
61
|
+
issues: [
|
|
62
|
+
{
|
|
63
|
+
level: "error",
|
|
64
|
+
message: `Migration path is not a directory: ${resolved}`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
70
|
+
const migrationFiles = entries
|
|
71
|
+
.filter((entry) => entry.isFile() && isMigrationFile(entry.name))
|
|
72
|
+
.map((entry) => entry.name)
|
|
73
|
+
.sort();
|
|
74
|
+
// 1) Directory must be non-empty.
|
|
75
|
+
if (migrationFiles.length === 0) {
|
|
76
|
+
issues.push({
|
|
77
|
+
level: "error",
|
|
78
|
+
message: `Migration directory is empty (no migration files found): ${resolved}`,
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
directory: resolved,
|
|
83
|
+
migrationFiles,
|
|
84
|
+
issues,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// 2) Every file must be prefixed with a 5-digit migration number.
|
|
88
|
+
// Track which numbers map to which files for duplicate detection.
|
|
89
|
+
const numberToFiles = new Map();
|
|
90
|
+
for (const file of migrationFiles) {
|
|
91
|
+
const match = MIGRATION_PREFIX_REGEX.exec(file);
|
|
92
|
+
if (!match) {
|
|
93
|
+
issues.push({
|
|
94
|
+
level: "error",
|
|
95
|
+
file,
|
|
96
|
+
message: `File is not prefixed with a 5-digit migration number (e.g. 00000-my-migration.ts): ${file}`,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const number = match[1];
|
|
101
|
+
const existing = numberToFiles.get(number);
|
|
102
|
+
if (existing) {
|
|
103
|
+
existing.push(file);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
numberToFiles.set(number, [file]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 3) No duplicate migration numbers (collisions across branches).
|
|
110
|
+
for (const [number, files] of numberToFiles) {
|
|
111
|
+
if (files.length > 1) {
|
|
112
|
+
issues.push({
|
|
113
|
+
level: duplicatesAsWarnings ? "warning" : "error",
|
|
114
|
+
message: `Duplicate migration number '${number}' used by ${files.length} files: ${files.join(", ")}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// 4) Every module must export an up() and down() function.
|
|
119
|
+
for (const file of migrationFiles) {
|
|
120
|
+
const fullPath = path.join(resolved, file);
|
|
121
|
+
let mod;
|
|
122
|
+
try {
|
|
123
|
+
mod = (await import(pathToFileURL(fullPath).href));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
issues.push({
|
|
127
|
+
level: "error",
|
|
128
|
+
file,
|
|
129
|
+
message: `Failed to import migration module '${file}': ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (typeof mod.up !== "function") {
|
|
134
|
+
issues.push({
|
|
135
|
+
level: "error",
|
|
136
|
+
file,
|
|
137
|
+
message: `Migration '${file}' does not export an up() function`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (typeof mod.down !== "function") {
|
|
141
|
+
issues.push({
|
|
142
|
+
level: "error",
|
|
143
|
+
file,
|
|
144
|
+
message: `Migration '${file}' does not export a down() function`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const ok = !issues.some((issue) => issue.level === "error");
|
|
149
|
+
return {
|
|
150
|
+
ok,
|
|
151
|
+
directory: resolved,
|
|
152
|
+
migrationFiles,
|
|
153
|
+
issues,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export default validateMigrationDirectory;
|
|
157
|
+
//# sourceMappingURL=validateMigrationDirectory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateMigrationDirectory.js","sourceRoot":"","sources":["../../src/utils/validateMigrationDirectory.ts"],"names":[],"mappings":"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"}
|