@cleverbrush/orm-cli 0.0.0-beta-20260424142030
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 +166 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +7 -0
- package/dist/chunk-E43XQHR3.js +42 -0
- package/dist/cli-XMDVRIZG.js +188 -0
- package/dist/cli.d.ts +3 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/push.d.ts +11 -0
- package/dist/commands/rollback.d.ts +5 -0
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/config.d.ts +13 -0
- package/dist/generate-CMLVA6DT.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/push-SNCDOATW.js +101 -0
- package/dist/rollback-AVV6DTXF.js +25 -0
- package/dist/run-PSIPMF6V.js +9 -0
- package/dist/status-W3A6JWR3.js +33 -0
- package/dist/types.d.ts +57 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# `@cleverbrush/orm-cli`
|
|
2
|
+
|
|
3
|
+
A standalone CLI for managing PostgreSQL schema migrations for projects built
|
|
4
|
+
with [`@cleverbrush/orm`](https://github.com/cleverbrush/framework).
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
cb-orm migrate generate [name] # diff DB → schema, emit TS migration file (name defaults to "migration")
|
|
8
|
+
cb-orm migrate run # apply pending migrations
|
|
9
|
+
cb-orm migrate rollback # roll back last batch
|
|
10
|
+
cb-orm migrate status # list applied/pending migrations
|
|
11
|
+
cb-orm db push # sync schema in-place (dev only)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install --save-dev @cleverbrush/orm-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`knex` is a peer dependency and must be installed in your project. `tsx` is a regular dependency and is installed automatically.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
### 1. Create `db.config.ts` in your project root
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// db.config.ts
|
|
32
|
+
import knex from 'knex';
|
|
33
|
+
import { defineConfig } from '@cleverbrush/orm-cli';
|
|
34
|
+
import { UserEntity, TodoEntity } from './src/db/schemas.js';
|
|
35
|
+
|
|
36
|
+
const db = knex({
|
|
37
|
+
client: 'pg',
|
|
38
|
+
connection: process.env.DATABASE_URL
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
knex: db,
|
|
43
|
+
entities: { users: UserEntity, todos: TodoEntity },
|
|
44
|
+
migrations: {
|
|
45
|
+
directory: './migrations', // where .ts migration files are written
|
|
46
|
+
tableName: 'knex_migrations' // default; override if needed
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Add convenience scripts to `package.json`
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"scripts": {
|
|
56
|
+
"db:generate": "cb-orm migrate generate",
|
|
57
|
+
"db:run": "cb-orm migrate run",
|
|
58
|
+
"db:rollback": "cb-orm migrate rollback",
|
|
59
|
+
"db:status": "cb-orm migrate status",
|
|
60
|
+
"db:push": "cb-orm db push"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
### `migrate generate <name>`
|
|
70
|
+
|
|
71
|
+
Compares every entity in `config.entities` against the live database and emits
|
|
72
|
+
a single timestamped TypeScript migration file when differences are found.
|
|
73
|
+
|
|
74
|
+
- New tables → `CREATE TABLE`.
|
|
75
|
+
- Existing tables with column/index/FK changes → `ALTER TABLE`.
|
|
76
|
+
- Multiple tables are FK-dependency-ordered (parents created first, dropped last).
|
|
77
|
+
- Polymorphic (CTI) entities include one entry per variant table.
|
|
78
|
+
- File written as `<dir>/YYYYMMDDHHmmss_<name>.ts`.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npx cb-orm migrate generate add_role_column
|
|
82
|
+
# → migrations/20260423120000_add_role_column.ts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `migrate run`
|
|
86
|
+
|
|
87
|
+
Applies all pending migrations via `knex.migrate.latest`.
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npx cb-orm migrate run
|
|
91
|
+
# Batch 1. Applied 2 migration(s):
|
|
92
|
+
# ✓ 20260423000001_init.ts
|
|
93
|
+
# ✓ 20260423120000_add_role_column.ts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use `--to <filename>` to migrate up to a specific file:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
npx cb-orm migrate run --to 20260423000001_init.ts
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `migrate rollback`
|
|
103
|
+
|
|
104
|
+
Rolls back the most recently applied batch.
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
npx cb-orm migrate rollback
|
|
108
|
+
# Roll back all batches:
|
|
109
|
+
npx cb-orm migrate rollback --all
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `migrate status`
|
|
113
|
+
|
|
114
|
+
Lists applied and pending migrations.
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
npx cb-orm migrate status
|
|
118
|
+
|
|
119
|
+
# Applied migrations:
|
|
120
|
+
# ✓ 20260423000001_init.ts
|
|
121
|
+
# Pending migrations:
|
|
122
|
+
# ○ 20260423120000_add_role_column.ts
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `db push`
|
|
126
|
+
|
|
127
|
+
Applies all schema changes directly to the database **without** writing a
|
|
128
|
+
migration file. Runs inside a single transaction.
|
|
129
|
+
|
|
130
|
+
> **Warning — dev only.** Requires `--yes` when `NODE_ENV=production`.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
npx cb-orm db push # asks for confirmation
|
|
134
|
+
npx cb-orm db push --yes # skip confirmation
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Options
|
|
140
|
+
|
|
141
|
+
| Flag | Commands | Description |
|
|
142
|
+
|------|----------|-------------|
|
|
143
|
+
| `--config <path>` | all | Path to config file (default: `db.config.ts` in cwd) |
|
|
144
|
+
| `--dir <path>` | all | Migrations directory (overrides `config.migrations.directory`) |
|
|
145
|
+
| `--to <name>` | `migrate run` | Apply up to a specific migration filename |
|
|
146
|
+
| `--all` | `migrate rollback` | Roll back all batches |
|
|
147
|
+
| `--yes` | `db push` | Skip the interactive confirmation prompt |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## How it works
|
|
152
|
+
|
|
153
|
+
The CLI delegates all schema intelligence to `@cleverbrush/knex-schema`:
|
|
154
|
+
|
|
155
|
+
| Step | Function |
|
|
156
|
+
|------|---------|
|
|
157
|
+
| Detect new tables | `tableExistsInDb(knex, tableName)` |
|
|
158
|
+
| Generate CREATE TABLE source | `generateCreateTableSource(schema)` |
|
|
159
|
+
| Introspect live table | `introspectDatabase(knex, tableName)` |
|
|
160
|
+
| Diff schema vs DB | `diffSchema(schema, dbState)` |
|
|
161
|
+
| Generate ALTER TABLE source | `generateMigration(diff, tableName)` |
|
|
162
|
+
| Apply diff without file | `applyDiff(knex, diff, tableName)` |
|
|
163
|
+
| Polymorphic variant tables | `getPolymorphicVariantSchemas(schema)` |
|
|
164
|
+
|
|
165
|
+
`tsx` is used to load `db.config.ts` at runtime by registering the
|
|
166
|
+
`tsx/esm/api` hook before any dynamic `import()` of the config file.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/run.ts
|
|
4
|
+
async function run(config, flags) {
|
|
5
|
+
const migrationConfig = buildMigrationConfig(config, flags);
|
|
6
|
+
const toMigration = flags["--to"];
|
|
7
|
+
if (toMigration) {
|
|
8
|
+
const [batchNo, applied] = await config.knex.migrate.up({
|
|
9
|
+
...migrationConfig,
|
|
10
|
+
name: toMigration
|
|
11
|
+
});
|
|
12
|
+
if (applied.length === 0) {
|
|
13
|
+
console.log(`${toMigration} \u2014 already applied.`);
|
|
14
|
+
} else {
|
|
15
|
+
console.log(
|
|
16
|
+
`Batch ${batchNo}. Applied: ${applied.join(", ")}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
const [batchNo, applied] = await config.knex.migrate.latest(migrationConfig);
|
|
21
|
+
if (applied.length === 0) {
|
|
22
|
+
console.log("Already up to date.");
|
|
23
|
+
} else {
|
|
24
|
+
console.log(
|
|
25
|
+
`Batch ${batchNo}. Applied ${applied.length} migration(s):`
|
|
26
|
+
);
|
|
27
|
+
for (const m of applied) console.log(` \u2713 ${m}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function buildMigrationConfig(config, flags) {
|
|
32
|
+
return {
|
|
33
|
+
directory: flags["--dir"] ?? config.migrations.directory,
|
|
34
|
+
tableName: config.migrations.tableName ?? "knex_migrations",
|
|
35
|
+
loadExtensions: [".ts", ".js"]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
run,
|
|
41
|
+
buildMigrationConfig
|
|
42
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
var CANDIDATE_NAMES = [
|
|
11
|
+
"db.config.ts",
|
|
12
|
+
"db.config.js",
|
|
13
|
+
"db.config.mjs"
|
|
14
|
+
];
|
|
15
|
+
async function loadConfig(configPath) {
|
|
16
|
+
const resolved = configPath ? path.resolve(process.cwd(), configPath) : findConfigFile();
|
|
17
|
+
if (!existsSync(resolved)) {
|
|
18
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
19
|
+
}
|
|
20
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
21
|
+
const config = mod.default ?? mod;
|
|
22
|
+
assertConfig(config);
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
function findConfigFile() {
|
|
26
|
+
for (const name of CANDIDATE_NAMES) {
|
|
27
|
+
const candidate = path.resolve(process.cwd(), name);
|
|
28
|
+
if (existsSync(candidate)) return candidate;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
`No config file found. Create db.config.ts in your project root, or pass --config <path>.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function assertConfig(config) {
|
|
35
|
+
if (!config || typeof config !== "object") {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Config must be an object (default export of db.config.ts)."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const c = config;
|
|
41
|
+
if (!c.knex || typeof c.knex.schema !== "object") {
|
|
42
|
+
throw new Error("config.knex must be a Knex instance.");
|
|
43
|
+
}
|
|
44
|
+
if (!c.entities || typeof c.entities !== "object") {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"config.entities must be an object mapping names to Entity instances."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!c.migrations || typeof c.migrations.directory !== "string") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"config.migrations.directory must be a non-empty string path."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/cli.ts
|
|
57
|
+
var _require = createRequire(import.meta.url);
|
|
58
|
+
var _pkg = _require("../package.json");
|
|
59
|
+
async function run(argv) {
|
|
60
|
+
const [cmd, sub, ...rest] = argv;
|
|
61
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
62
|
+
printHelp();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
66
|
+
console.log(_pkg.version ?? "unknown");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const flags = parseFlags(rest);
|
|
70
|
+
const configPath = flags["--config"];
|
|
71
|
+
try {
|
|
72
|
+
if (cmd === "migrate") {
|
|
73
|
+
switch (sub) {
|
|
74
|
+
case "generate": {
|
|
75
|
+
let name = "migration";
|
|
76
|
+
for (let i = 0; i < rest.length; i++) {
|
|
77
|
+
if (rest[i].startsWith("-")) {
|
|
78
|
+
const next = rest[i + 1];
|
|
79
|
+
if (next !== void 0 && !next.startsWith("-"))
|
|
80
|
+
i++;
|
|
81
|
+
} else {
|
|
82
|
+
name = rest[i];
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const config = await loadConfig(configPath);
|
|
87
|
+
const { generate } = await import("./generate-CMLVA6DT.js");
|
|
88
|
+
await generate(name, config, flags);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "run": {
|
|
92
|
+
const config = await loadConfig(configPath);
|
|
93
|
+
const { run: runMigrations } = await import("./run-PSIPMF6V.js");
|
|
94
|
+
await runMigrations(config, flags);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "rollback": {
|
|
98
|
+
const config = await loadConfig(configPath);
|
|
99
|
+
const { rollback } = await import("./rollback-AVV6DTXF.js");
|
|
100
|
+
await rollback(config, flags);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "status": {
|
|
104
|
+
const config = await loadConfig(configPath);
|
|
105
|
+
const { status } = await import("./status-W3A6JWR3.js");
|
|
106
|
+
await status(config, flags);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
fatal(
|
|
111
|
+
`Unknown migrate command: ${sub ?? "(none)"}. Run \`cb-orm --help\` for usage.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
} else if (cmd === "db") {
|
|
115
|
+
switch (sub) {
|
|
116
|
+
case "push": {
|
|
117
|
+
const config = await loadConfig(configPath);
|
|
118
|
+
const { push } = await import("./push-SNCDOATW.js");
|
|
119
|
+
await push(config, flags);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
fatal(
|
|
124
|
+
`Unknown db command: ${sub ?? "(none)"}. Run \`cb-orm --help\` for usage.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
fatal(`Unknown command: ${cmd}. Run \`cb-orm --help\` for usage.`);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(
|
|
132
|
+
`
|
|
133
|
+
Error: ${err instanceof Error ? err.message : String(err)}
|
|
134
|
+
`
|
|
135
|
+
);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function parseFlags(args) {
|
|
140
|
+
const flags = {};
|
|
141
|
+
for (let i = 0; i < args.length; i++) {
|
|
142
|
+
const arg = args[i];
|
|
143
|
+
if (arg.startsWith("--")) {
|
|
144
|
+
const next = args[i + 1];
|
|
145
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
146
|
+
flags[arg] = next;
|
|
147
|
+
i++;
|
|
148
|
+
} else {
|
|
149
|
+
flags[arg] = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return flags;
|
|
154
|
+
}
|
|
155
|
+
function fatal(msg) {
|
|
156
|
+
console.error(`
|
|
157
|
+
Error: ${msg}
|
|
158
|
+
`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
function printHelp() {
|
|
162
|
+
console.log(`
|
|
163
|
+
cb-orm \u2014 Schema migration CLI for @cleverbrush/orm
|
|
164
|
+
|
|
165
|
+
USAGE
|
|
166
|
+
cb-orm <command> [options]
|
|
167
|
+
|
|
168
|
+
COMMANDS
|
|
169
|
+
migrate generate [name] Diff DB vs schema, emit a timestamped TS migration file (default name: "migration")
|
|
170
|
+
migrate run Apply pending migrations (knex.migrate.latest)
|
|
171
|
+
migrate rollback Roll back last batch (knex.migrate.rollback)
|
|
172
|
+
migrate status List applied and pending migrations
|
|
173
|
+
db push Sync schema to DB in-place (dev only \u2014 no migration file)
|
|
174
|
+
|
|
175
|
+
OPTIONS
|
|
176
|
+
--config <path> Path to db.config.ts (default: db.config.ts in cwd)
|
|
177
|
+
--dir <path> Migrations directory (overrides config.migrations.directory)
|
|
178
|
+
--to <name> (migrate run) Apply up to a specific migration by filename
|
|
179
|
+
--all (migrate rollback) Roll back all applied migrations
|
|
180
|
+
--yes (db push) Skip the confirmation prompt
|
|
181
|
+
--help, -h Show this help
|
|
182
|
+
--version, -v Show version
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
parseFlags,
|
|
187
|
+
run
|
|
188
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OrmCliConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Diff the current entity schemas against the committed snapshot and write a
|
|
4
|
+
* single timestamped TypeScript migration file when changes are detected.
|
|
5
|
+
* Also updates the snapshot file so the next run starts from the new baseline.
|
|
6
|
+
*
|
|
7
|
+
* No live database connection is needed — the snapshot is the source of truth.
|
|
8
|
+
*
|
|
9
|
+
* File name pattern: `YYYYMMDDHHmmss_<name>.ts` (matches Knex defaults).
|
|
10
|
+
*/
|
|
11
|
+
export declare function generate(name: string, config: OrmCliConfig, flags: Record<string, string | true>): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OrmCliConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sync every registered entity's schema to the live database without
|
|
4
|
+
* generating a migration file.
|
|
5
|
+
*
|
|
6
|
+
* - New tables are created via {@link generateCreateTable}.
|
|
7
|
+
* - Existing tables are altered via {@link applyDiff}.
|
|
8
|
+
* - All changes run inside a single transaction.
|
|
9
|
+
* - Aborted (no `--yes`) when `NODE_ENV=production`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function push(config: OrmCliConfig, flags: Record<string, string | true>): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OrmCliConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Apply pending migrations via `knex.migrate.latest`.
|
|
4
|
+
* Pass `--to <migrationName>` to migrate up to a specific file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function run(config: OrmCliConfig, flags: Record<string, string | true>): Promise<void>;
|
|
7
|
+
export declare function buildMigrationConfig(config: OrmCliConfig, flags: Record<string, string | true>): {
|
|
8
|
+
directory: string;
|
|
9
|
+
tableName: string;
|
|
10
|
+
loadExtensions: string[];
|
|
11
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OrmCliConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load and validate a CLI config file.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. Explicit `configPath` argument (relative to `process.cwd()`).
|
|
7
|
+
* 2. `db.config.ts`, `db.config.js`, `db.config.mjs` in `process.cwd()`.
|
|
8
|
+
*
|
|
9
|
+
* The file must export a default {@link OrmCliConfig} object (or use the
|
|
10
|
+
* {@link defineConfig} helper). TypeScript files work because the tsx ESM
|
|
11
|
+
* hook was registered in `bin.ts` before this module is imported.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadConfig(configPath?: string): Promise<OrmCliConfig>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/generate.ts
|
|
4
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import {
|
|
7
|
+
generateMigrationsForContext,
|
|
8
|
+
loadSnapshot,
|
|
9
|
+
writeSnapshot
|
|
10
|
+
} from "@cleverbrush/knex-schema";
|
|
11
|
+
async function generate(name, config, flags) {
|
|
12
|
+
const dir = flags["--dir"] ?? config.migrations.directory;
|
|
13
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
14
|
+
const snapshotPath = config.migrations.snapshot ?? path.join(absDir, "snapshot.json");
|
|
15
|
+
const absSnapshotPath = path.resolve(process.cwd(), snapshotPath);
|
|
16
|
+
const entities = Object.values(config.entities);
|
|
17
|
+
const prevSnapshot = loadSnapshot(absSnapshotPath);
|
|
18
|
+
const result = generateMigrationsForContext(entities, prevSnapshot);
|
|
19
|
+
if (result.isEmpty) {
|
|
20
|
+
console.log("No schema changes detected.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
mkdirSync(absDir, { recursive: true });
|
|
24
|
+
const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
|
|
25
|
+
const safeName = name.replace(/[^a-z0-9_]/gi, "_");
|
|
26
|
+
const filename = `${timestamp}_${safeName}.ts`;
|
|
27
|
+
const filepath = path.join(absDir, filename);
|
|
28
|
+
writeFileSync(filepath, result.full, "utf-8");
|
|
29
|
+
console.log(`Created migration: ${filepath}`);
|
|
30
|
+
writeSnapshot(absSnapshotPath, result.nextSnapshot);
|
|
31
|
+
console.log(`Updated snapshot: ${absSnapshotPath}`);
|
|
32
|
+
}
|
|
33
|
+
function formatTimestamp(d) {
|
|
34
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
35
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
generate
|
|
39
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/push.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
import {
|
|
7
|
+
applyDiff,
|
|
8
|
+
diffSchema,
|
|
9
|
+
entitiesToSnapshot,
|
|
10
|
+
generateCreateTable,
|
|
11
|
+
getPolymorphicVariantSchemas,
|
|
12
|
+
getTableName,
|
|
13
|
+
introspectDatabase,
|
|
14
|
+
isDiffEmpty,
|
|
15
|
+
tableExistsInDb,
|
|
16
|
+
writeSnapshot
|
|
17
|
+
} from "@cleverbrush/knex-schema";
|
|
18
|
+
async function push(config, flags) {
|
|
19
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
20
|
+
const confirmed = flags["--yes"] === true;
|
|
21
|
+
if (isProduction && !confirmed) {
|
|
22
|
+
console.error(
|
|
23
|
+
"\nError: `db push` is not allowed in NODE_ENV=production without the --yes flag.\n"
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (!confirmed) {
|
|
28
|
+
const answer = await prompt(
|
|
29
|
+
"\nThis will apply schema changes directly to the database (no migration file will be created).\nContinue? [y/N] "
|
|
30
|
+
);
|
|
31
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
32
|
+
console.log("Aborted.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const tableEntries = [];
|
|
37
|
+
for (const entity of Object.values(config.entities)) {
|
|
38
|
+
const schema = entity.schema;
|
|
39
|
+
const tableName = getTableName(schema);
|
|
40
|
+
tableEntries.push({ schema, tableName });
|
|
41
|
+
for (const vs of getPolymorphicVariantSchemas(schema)) {
|
|
42
|
+
const vt = vs.getExtension("tableName");
|
|
43
|
+
if (vt) tableEntries.push({ schema: vs, tableName: vt });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const seen = /* @__PURE__ */ new Set();
|
|
47
|
+
const unique = tableEntries.filter((e) => {
|
|
48
|
+
if (seen.has(e.tableName)) return false;
|
|
49
|
+
seen.add(e.tableName);
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
await config.knex.transaction(async (trx) => {
|
|
53
|
+
let changed = 0;
|
|
54
|
+
for (const { schema, tableName } of unique) {
|
|
55
|
+
const exists = await tableExistsInDb(trx, tableName);
|
|
56
|
+
if (!exists) {
|
|
57
|
+
await generateCreateTable(schema)(trx);
|
|
58
|
+
console.log(` + Created table: ${tableName}`);
|
|
59
|
+
changed++;
|
|
60
|
+
} else {
|
|
61
|
+
const dbState = await introspectDatabase(trx, tableName);
|
|
62
|
+
const diff = diffSchema(schema, dbState);
|
|
63
|
+
if (!isDiffEmpty(diff)) {
|
|
64
|
+
await applyDiff(trx, diff, tableName);
|
|
65
|
+
console.log(` ~ Altered table: ${tableName}`);
|
|
66
|
+
changed++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (changed === 0) {
|
|
71
|
+
console.log("No schema changes detected.");
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`
|
|
74
|
+
Applied ${changed} table change(s).`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const dir = config.migrations.directory;
|
|
78
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
79
|
+
const snapshotPath = config.migrations.snapshot ?? path.join(absDir, "snapshot.json");
|
|
80
|
+
const absSnapshotPath = path.resolve(process.cwd(), snapshotPath);
|
|
81
|
+
writeSnapshot(
|
|
82
|
+
absSnapshotPath,
|
|
83
|
+
entitiesToSnapshot(Object.values(config.entities))
|
|
84
|
+
);
|
|
85
|
+
console.log(`Updated snapshot: ${absSnapshotPath}`);
|
|
86
|
+
}
|
|
87
|
+
function prompt(question) {
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout
|
|
91
|
+
});
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
rl.question(question, (answer) => {
|
|
94
|
+
rl.close();
|
|
95
|
+
resolve(answer);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
push
|
|
101
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildMigrationConfig
|
|
4
|
+
} from "./chunk-E43XQHR3.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/rollback.ts
|
|
7
|
+
async function rollback(config, flags) {
|
|
8
|
+
const migrationConfig = buildMigrationConfig(config, flags);
|
|
9
|
+
const all = flags["--all"] === true;
|
|
10
|
+
const [batchNo, reverted] = await config.knex.migrate.rollback(
|
|
11
|
+
migrationConfig,
|
|
12
|
+
all
|
|
13
|
+
);
|
|
14
|
+
if (reverted.length === 0) {
|
|
15
|
+
console.log("Nothing to roll back.");
|
|
16
|
+
} else {
|
|
17
|
+
console.log(
|
|
18
|
+
`Batch ${batchNo} rolled back. Reverted ${reverted.length} migration(s):`
|
|
19
|
+
);
|
|
20
|
+
for (const m of reverted) console.log(` \u2717 ${m}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
rollback
|
|
25
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildMigrationConfig
|
|
4
|
+
} from "./chunk-E43XQHR3.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/status.ts
|
|
7
|
+
async function status(config, flags) {
|
|
8
|
+
const migrationConfig = buildMigrationConfig(config, flags);
|
|
9
|
+
const [completed, pending] = await config.knex.migrate.list(migrationConfig);
|
|
10
|
+
if (completed.length === 0 && pending.length === 0) {
|
|
11
|
+
console.log("No migrations found in the configured directory.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (completed.length > 0) {
|
|
15
|
+
console.log("\nApplied migrations:");
|
|
16
|
+
for (const m of completed) {
|
|
17
|
+
const name = typeof m === "string" ? m : m.name ?? m.file ?? m;
|
|
18
|
+
console.log(` \u2713 ${name}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (pending.length > 0) {
|
|
22
|
+
console.log("\nPending migrations:");
|
|
23
|
+
for (const m of pending) {
|
|
24
|
+
const name = typeof m === "string" ? m : m.file ?? m.name ?? m;
|
|
25
|
+
console.log(` \u25CB ${name}`);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
console.log("\nDatabase is up to date.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
status
|
|
33
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Entity } from '@cleverbrush/knex-schema';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
/**
|
|
4
|
+
* Migration-runner configuration block inside {@link OrmCliConfig}.
|
|
5
|
+
*/
|
|
6
|
+
export interface MigrationsConfig {
|
|
7
|
+
/** Directory where migration files are read from / written to. */
|
|
8
|
+
directory: string;
|
|
9
|
+
/**
|
|
10
|
+
* Knex migration tracking table name.
|
|
11
|
+
* @defaultValue `'knex_migrations'`
|
|
12
|
+
*/
|
|
13
|
+
tableName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Path to the schema snapshot file. Must be committed to version control
|
|
16
|
+
* — it is the source of truth for `migrate generate`.
|
|
17
|
+
* @defaultValue `<directory>/snapshot.json`
|
|
18
|
+
*/
|
|
19
|
+
snapshot?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Shape of the default export expected in `db.config.ts`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // db.config.ts
|
|
27
|
+
* import { defineConfig } from '@cleverbrush/orm-cli';
|
|
28
|
+
* import knex from './src/db/knex.js';
|
|
29
|
+
* import { UserEntity, TodoEntity } from './src/db/schemas.js';
|
|
30
|
+
*
|
|
31
|
+
* export default defineConfig({
|
|
32
|
+
* knex,
|
|
33
|
+
* entities: { users: UserEntity, todos: TodoEntity },
|
|
34
|
+
* migrations: { directory: './migrations' },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export interface OrmCliConfig {
|
|
39
|
+
/** A live Knex instance connected to your database. */
|
|
40
|
+
knex: Knex;
|
|
41
|
+
/**
|
|
42
|
+
* Map of entity name to `Entity` definition. The same map you pass to
|
|
43
|
+
* `createDb()` from `@cleverbrush/orm`.
|
|
44
|
+
*/
|
|
45
|
+
entities: Record<string, Entity<any, any>>;
|
|
46
|
+
/** Migration configuration. */
|
|
47
|
+
migrations: MigrationsConfig;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Identity helper that provides TypeScript autocompletion for `db.config.ts`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* export default defineConfig({ knex, entities, migrations });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function defineConfig(config: OrmCliConfig): OrmCliConfig;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/cleverbrush/framework/issues",
|
|
5
|
+
"email": "andrew_zol@cleverbrush.com"
|
|
6
|
+
},
|
|
7
|
+
"bin": {
|
|
8
|
+
"cb-orm": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@cleverbrush/knex-schema": "0.0.0-beta-20260424142030",
|
|
12
|
+
"tsx": "^4.19.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"knex": ">=3.1.0"
|
|
16
|
+
},
|
|
17
|
+
"description": "CLI for @cleverbrush/orm — migrate generate/run/rollback/status and db push",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/cleverbrush/framework/tree/master/libs/orm-cli#readme",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"orm",
|
|
24
|
+
"knex",
|
|
25
|
+
"typescript",
|
|
26
|
+
"migrations",
|
|
27
|
+
"cli",
|
|
28
|
+
"cleverbrush"
|
|
29
|
+
],
|
|
30
|
+
"license": "BSD 3-Clause",
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"name": "@cleverbrush/orm-cli",
|
|
40
|
+
"readme": "https://github.com/cleverbrush/framework/tree/master/libs/orm-cli#readme",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "github:cleverbrush/framework"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
47
|
+
"clean": "rm -rf dist tsconfig.build.tsbuildinfo"
|
|
48
|
+
},
|
|
49
|
+
"type": "module",
|
|
50
|
+
"types": "./dist/index.d.ts",
|
|
51
|
+
"version": "0.0.0-beta-20260424142030"
|
|
52
|
+
}
|