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 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 { Kysely, BunSqliteDialect } from "kysely-bun-sqlite"
23
- import { Database } from "bun:sqlite"
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 BunSqliteDialect({ database: new Database("my-app.db") }) })
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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { run } from "../dist/cli.mjs"
3
+ await run()
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { a as loadMigrationFiles, i as loadConfig, n as createMigrationGenerator, t as createMigrationRunner } from "./runner-DOQsuaSQ.mjs";
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 initial migration from models").action(async (name) => {
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("Generating migration...").start();
19
- const code = createMigrationGenerator().generateInitialMigration(/* @__PURE__ */ new Map(), { name: name ?? "Initial" });
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 last batch").action(async () => {
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 spinner = ora("Rolling back...").start();
57
- await runner.down(migrations);
58
- spinner.succeed("Rolled back");
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
  }