peta-migrate 0.1.1 โ 0.2.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/LICENSE +21 -0
- package/README.md +6 -5
- 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-Be3BYUQM.mjs +88 -0
- package/dist/snapshot-DopEB8mx.mjs +550 -0
- package/package.json +13 -5
- package/dist/runner-DOQsuaSQ.mjs +0 -180
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 zfadhli
|
|
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
|
@@ -7,10 +7,10 @@
|
|
|
7
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
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun add peta-migrate
|
|
10
|
+
bun add peta-migrate kysely @libsql/kysely-libsql @libsql/client
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Requires `kysely` as a peer dependency.
|
|
13
|
+
Requires `kysely` as a peer dependency. SQLite via `@libsql/kysely-libsql`, PostgreSQL via `pg`, MySQL via `mysql2`.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -19,11 +19,12 @@ Requires `kysely` as a peer dependency.
|
|
|
19
19
|
### Programmatic
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { createClient } from "@libsql/client"
|
|
23
|
+
import { LibsqlDialect } from "@libsql/kysely-libsql"
|
|
24
|
+
import { Kysely } from "kysely"
|
|
24
25
|
import { createMigrationRunner, createMigrationGenerator } from "peta-migrate"
|
|
25
26
|
|
|
26
|
-
const db = new Kysely({ dialect: new
|
|
27
|
+
const db = new Kysely({ dialect: new LibsqlDialect({ url: "file:my-app.db" }) })
|
|
27
28
|
const runner = createMigrationRunner(db)
|
|
28
29
|
|
|
29
30
|
await runner.ensureTable() // create tracking table
|
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-Be3BYUQM.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
|
}
|