peta-migrate 0.1.0 → 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/README.md +132 -0
- package/bin/peta +3 -0
- package/dist/cli.mjs +117 -9
- package/dist/index.d.mts +88 -379
- package/dist/index.mjs +3 -2
- package/dist/pusher-DJvKHlOT.mjs +88 -0
- package/dist/snapshot-DopEB8mx.mjs +550 -0
- package/package.json +11 -4
- package/dist/runner-DOQsuaSQ.mjs +0 -180
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# peta-migrate
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/peta-migrate)
|
|
4
|
+
[](https://www.typescriptlang.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Standalone migration runner and generator for [peta-orm](https://www.npmjs.com/package/peta-orm). Run, roll back, and generate database migrations with a clean programmatic API and CLI.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add peta-migrate kysely @libsql/kysely-libsql @libsql/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `kysely` as a peer dependency. SQLite via `@libsql/kysely-libsql`, PostgreSQL via `pg`, MySQL via `mysql2`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Programmatic
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createClient } from "@libsql/client"
|
|
23
|
+
import { LibsqlDialect } from "@libsql/kysely-libsql"
|
|
24
|
+
import { Kysely } from "kysely"
|
|
25
|
+
import { createMigrationRunner, createMigrationGenerator } from "peta-migrate"
|
|
26
|
+
|
|
27
|
+
const db = new Kysely({ dialect: new LibsqlDialect({ url: "file:my-app.db" }) })
|
|
28
|
+
const runner = createMigrationRunner(db)
|
|
29
|
+
|
|
30
|
+
await runner.ensureTable() // create tracking table
|
|
31
|
+
|
|
32
|
+
await runner.up([
|
|
33
|
+
{
|
|
34
|
+
name: "001_create_users",
|
|
35
|
+
up: async (k) => {
|
|
36
|
+
await k.schema
|
|
37
|
+
.createTable("users")
|
|
38
|
+
.addColumn("id", "integer", (c) => c.autoIncrement().primaryKey())
|
|
39
|
+
.addColumn("name", "varchar(255)", (c) => c.notNull())
|
|
40
|
+
.execute()
|
|
41
|
+
},
|
|
42
|
+
down: async (k) => {
|
|
43
|
+
await k.schema.dropTable("users").execute()
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
// Check status
|
|
49
|
+
const completed = await runner.getCompleted() // MigrationRecord[]
|
|
50
|
+
const status = await runner.status() // { completed: [...], pending: [...] }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### CLI
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bun x peta migrate:init # Create migrations directory and tracking table
|
|
57
|
+
bun x peta migrate:generate # Generate initial migration from models
|
|
58
|
+
bun x peta migrate:up # Run pending migrations
|
|
59
|
+
bun x peta migrate:status # Show migration status
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `createMigrationRunner(kysely)`
|
|
67
|
+
|
|
68
|
+
Creates a runner that manages migration execution.
|
|
69
|
+
|
|
70
|
+
| Method | Description |
|
|
71
|
+
|--------|-------------|
|
|
72
|
+
| `ensureTable()` | Create the migrations tracking table |
|
|
73
|
+
| `up(migrations)` | Apply pending migrations in order |
|
|
74
|
+
| `down()` | Roll back the last batch of migrations |
|
|
75
|
+
| `getCompleted()` | Return list of completed migration records |
|
|
76
|
+
| `status()` | Return `{ completed, pending }` with both lists |
|
|
77
|
+
|
|
78
|
+
### `createMigrationGenerator()`
|
|
79
|
+
|
|
80
|
+
Creates a generator that produces migration code from model definitions.
|
|
81
|
+
|
|
82
|
+
| Method | Description |
|
|
83
|
+
|--------|-------------|
|
|
84
|
+
| `generateInitialMigration(models)` | Generate a create-table migration from registered models |
|
|
85
|
+
|
|
86
|
+
### Configuration
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { defineConfig } from "peta-migrate"
|
|
90
|
+
|
|
91
|
+
const config = defineConfig({
|
|
92
|
+
migrationsDir: "./migrations",
|
|
93
|
+
models: ["./src/models/*.ts"],
|
|
94
|
+
getKysely: () => db,
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Option | Type | Description |
|
|
99
|
+
|--------|------|-------------|
|
|
100
|
+
| `migrationsDir` | `string` | Directory to store migration files |
|
|
101
|
+
| `models` | `string[]` \| `string` | Glob patterns for model files |
|
|
102
|
+
| `getKysely` | `() => Kysely` | Function returning a Kysely instance |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Types
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
interface MigrationFile {
|
|
110
|
+
name: string
|
|
111
|
+
up: (db: Kysely<unknown>) => Promise<void>
|
|
112
|
+
down: (db: Kysely<unknown>) => Promise<void>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface MigrationRecord {
|
|
116
|
+
name: string
|
|
117
|
+
appliedAt: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface MigrationStatus {
|
|
121
|
+
completed: MigrationRecord[]
|
|
122
|
+
pending: MigrationFile[]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Related packages
|
|
129
|
+
|
|
130
|
+
- [peta-orm](../orm) — ORM with models, relations, hooks, soft deletes
|
|
131
|
+
- [peta-auth](../auth) — Encrypted cookie sessions, JWT, OAuth
|
|
132
|
+
- [peta-docs](../docs) — OpenAPI 3.1 spec generation + Scalar UI
|
package/bin/peta
ADDED
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { a as createMigrationGenerator, c as loadConfig, d as computeChecksum, f as loadChecksums, i as createMigrationRunner, l as loadMigrationFiles, m as verifyChecksum, n as loadSnapshot, o as diffSnapshots, p as saveChecksums, r as saveSnapshot, t as createSnapshot, u as loadModels } from "./snapshot-DopEB8mx.mjs";
|
|
3
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
4
|
import cac from "cac";
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
//#region src/cli.ts
|
|
@@ -13,17 +13,71 @@ async function run() {
|
|
|
13
13
|
await createMigrationRunner(config.getKysely()).ensureTable();
|
|
14
14
|
spinner.succeed(`Migrations directory created at ${config.migrationsDir}`);
|
|
15
15
|
});
|
|
16
|
-
cli.command("migrate:generate [name]", "Generate
|
|
16
|
+
cli.command("migrate:generate [name]", "Generate migration from models (initial or incremental)").action(async (name) => {
|
|
17
17
|
const config = await loadConfig();
|
|
18
|
-
const spinner = ora("
|
|
19
|
-
const
|
|
18
|
+
const spinner = ora("Loading models...").start();
|
|
19
|
+
const models = await loadModels(config.models);
|
|
20
|
+
if (models.size === 0) {
|
|
21
|
+
spinner.warn("No models found matching the configured patterns.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
spinner.text = "Generating migration...";
|
|
25
|
+
const gen = createMigrationGenerator();
|
|
26
|
+
const snapshotPath = resolve(config.migrationsDir, "snapshot.json");
|
|
27
|
+
const prevSnapshot = await loadSnapshot(snapshotPath);
|
|
28
|
+
let code;
|
|
29
|
+
if (prevSnapshot) {
|
|
30
|
+
const currentSnapshot = createSnapshot(models);
|
|
31
|
+
const diffs = diffSnapshots(prevSnapshot, currentSnapshot);
|
|
32
|
+
if (diffs.length === 0) {
|
|
33
|
+
spinner.succeed("No schema changes detected since last snapshot.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
code = gen.generateMigrationFromDiff(diffs, { name: name ?? "changes" });
|
|
37
|
+
await saveSnapshot(snapshotPath, currentSnapshot);
|
|
38
|
+
spinner.text = `Generated incremental migration (${diffs.length} change(s))`;
|
|
39
|
+
} else {
|
|
40
|
+
code = gen.generateInitialMigration(models);
|
|
41
|
+
await saveSnapshot(snapshotPath, createSnapshot(models));
|
|
42
|
+
spinner.text = `Generated initial migration (${models.size} model(s))`;
|
|
43
|
+
}
|
|
20
44
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
21
45
|
const safeName = (name ?? "initial").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
22
46
|
const filename = resolve(config.migrationsDir, `${timestamp}_${safeName}.ts`);
|
|
23
47
|
mkdirSync(config.migrationsDir, { recursive: true });
|
|
24
48
|
writeFileSync(filename, code);
|
|
49
|
+
const checksums = loadChecksums(config.migrationsDir);
|
|
50
|
+
const fileName = `${timestamp}_${safeName}.ts`;
|
|
51
|
+
checksums[fileName.replace(/\.(ts|js)$/, "")] = computeChecksum(filename);
|
|
52
|
+
saveChecksums(config.migrationsDir, checksums);
|
|
25
53
|
spinner.succeed(`Created ${filename}`);
|
|
26
54
|
});
|
|
55
|
+
cli.command("migrate:diff", "Preview schema changes without writing a migration").action(async () => {
|
|
56
|
+
const config = await loadConfig();
|
|
57
|
+
const prevSnapshot = await loadSnapshot(resolve(config.migrationsDir, "snapshot.json"));
|
|
58
|
+
if (!prevSnapshot) {
|
|
59
|
+
console.log("No snapshot found. Run `migrate:generate` first to create one.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const spinner = ora("Loading models...").start();
|
|
63
|
+
const models = await loadModels(config.models);
|
|
64
|
+
spinner.stop();
|
|
65
|
+
if (models.size === 0) {
|
|
66
|
+
console.log("No models found.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const diffs = diffSnapshots(prevSnapshot, createSnapshot(models));
|
|
70
|
+
if (diffs.length === 0) {
|
|
71
|
+
console.log("✅ No schema changes detected.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(`\n 📋 Schema changes: ${diffs.length}\n`);
|
|
75
|
+
for (const d of diffs) {
|
|
76
|
+
const icon = d.type.startsWith("drop") ? "🗑️" : d.type.startsWith("create") || d.type.startsWith("add") ? "➕" : "✏️";
|
|
77
|
+
console.log(` ${icon} [${d.type}] ${d.table}${d.column ? `.${d.column}` : ""}`);
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
});
|
|
27
81
|
cli.command("migrate:up", "Apply pending migrations").action(async () => {
|
|
28
82
|
const config = await loadConfig();
|
|
29
83
|
const migrations = await loadMigrationFiles(config.migrationsDir);
|
|
@@ -31,6 +85,13 @@ async function run() {
|
|
|
31
85
|
console.log("No migration files found.");
|
|
32
86
|
return;
|
|
33
87
|
}
|
|
88
|
+
for (const m of migrations) {
|
|
89
|
+
const filePath = resolve(config.migrationsDir, `${m.name}.ts`);
|
|
90
|
+
if (!verifyChecksum(config.migrationsDir, m.name, filePath)) {
|
|
91
|
+
console.error(`❌ Checksum mismatch for migration "${m.name}". File has been modified since creation.`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
34
95
|
const runner = createMigrationRunner(config.getKysely());
|
|
35
96
|
const status = await runner.status(migrations);
|
|
36
97
|
if (status.pending.length === 0) {
|
|
@@ -41,7 +102,7 @@ async function run() {
|
|
|
41
102
|
await runner.up(migrations);
|
|
42
103
|
spinner.succeed(`Applied ${(await runner.getCompleted()).length} migration(s)`);
|
|
43
104
|
});
|
|
44
|
-
cli.command("migrate:down", "Rollback
|
|
105
|
+
cli.command("migrate:down [steps]", "Rollback migrations (default: 1)").action(async (steps) => {
|
|
45
106
|
const config = await loadConfig();
|
|
46
107
|
const migrations = await loadMigrationFiles(config.migrationsDir);
|
|
47
108
|
if (migrations.length === 0) {
|
|
@@ -53,9 +114,17 @@ async function run() {
|
|
|
53
114
|
console.log("Nothing to rollback.");
|
|
54
115
|
return;
|
|
55
116
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
117
|
+
const numSteps = steps ? Number.parseInt(steps, 10) : 1;
|
|
118
|
+
if (Number.isNaN(numSteps) || numSteps < 1) {
|
|
119
|
+
console.error("Steps must be a positive integer.");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const spinner = ora(`Rolling back ${numSteps} migration(s)...`).start();
|
|
123
|
+
for (let i = 0; i < numSteps; i++) {
|
|
124
|
+
if ((await runner.getCompleted()).length === 0) break;
|
|
125
|
+
await runner.down(migrations);
|
|
126
|
+
}
|
|
127
|
+
spinner.succeed(`Rolled back ${numSteps} migration(s)`);
|
|
59
128
|
});
|
|
60
129
|
cli.command("migrate:status", "Show migration status").action(async () => {
|
|
61
130
|
const config = await loadConfig();
|
|
@@ -67,6 +136,45 @@ async function run() {
|
|
|
67
136
|
for (const m of pending) console.log(` · ${m.name}`);
|
|
68
137
|
console.log();
|
|
69
138
|
});
|
|
139
|
+
cli.command("migrate:push", "Push schema directly to database (prototyping)").action(async () => {
|
|
140
|
+
const config = await loadConfig();
|
|
141
|
+
const spinner = ora("Loading models...").start();
|
|
142
|
+
const models = await loadModels(config.models);
|
|
143
|
+
if (models.size === 0) {
|
|
144
|
+
spinner.warn("No models found.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
spinner.text = "Pushing schema to database...";
|
|
148
|
+
const { pushSchema } = await import("./pusher-DJvKHlOT.mjs").then((n) => n.n);
|
|
149
|
+
const created = await pushSchema(config.getKysely(), models);
|
|
150
|
+
await saveSnapshot(resolve(config.migrationsDir, "snapshot.json"), createSnapshot(models));
|
|
151
|
+
if (created.length === 0) spinner.succeed("Schema is up to date (no new tables).");
|
|
152
|
+
else spinner.succeed(`Created tables: ${created.join(", ")}`);
|
|
153
|
+
});
|
|
154
|
+
cli.command("migrate:seed [name]", "Generate or run seed files").action(async (name) => {
|
|
155
|
+
const config = await loadConfig();
|
|
156
|
+
if (name) {
|
|
157
|
+
const spinner = ora("Generating seed...").start();
|
|
158
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
159
|
+
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
160
|
+
const filename = resolve(config.migrationsDir, `${timestamp}_seed_${safeName}.ts`);
|
|
161
|
+
writeFileSync(filename, `import type { Kysely } from "kysely"\n\nexport async function seed(db: Kysely<any>): Promise<void> {\n // TODO: add seed data\n}\n`);
|
|
162
|
+
spinner.succeed(`Created ${filename}`);
|
|
163
|
+
} else {
|
|
164
|
+
const { readdirSync } = await import("node:fs");
|
|
165
|
+
const spinner = ora("Running seeds...").start();
|
|
166
|
+
const seedFiles = readdirSync(config.migrationsDir).filter((f) => f.includes("_seed_") && (f.endsWith(".ts") || f.endsWith(".js"))).sort();
|
|
167
|
+
if (seedFiles.length === 0) {
|
|
168
|
+
spinner.warn("No seed files found.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const file of seedFiles) {
|
|
172
|
+
const mod = await import(resolve(config.migrationsDir, file));
|
|
173
|
+
if (mod.seed) await mod.seed(config.getKysely());
|
|
174
|
+
}
|
|
175
|
+
spinner.succeed(`Executed ${seedFiles.length} seed(s)`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
70
178
|
cli.help();
|
|
71
179
|
cli.parse();
|
|
72
180
|
}
|