baasix 0.1.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.
@@ -0,0 +1,573 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ cancel,
6
+ confirm,
7
+ intro,
8
+ isCancel,
9
+ log,
10
+ outro,
11
+ select,
12
+ spinner,
13
+ text,
14
+ } from "@clack/prompts";
15
+ import chalk from "chalk";
16
+ import { Command } from "commander";
17
+ import { getConfig, type BaasixConfig } from "../utils/get-config.js";
18
+ import {
19
+ fetchMigrations,
20
+ runMigrations as apiRunMigrations,
21
+ rollbackMigrations as apiRollbackMigrations,
22
+ type MigrationInfo,
23
+ } from "../utils/api-client.js";
24
+
25
+ type MigrateAction = "status" | "run" | "create" | "rollback" | "reset" | "list";
26
+
27
+ interface MigrateOptions {
28
+ cwd: string;
29
+ url?: string;
30
+ action?: MigrateAction;
31
+ name?: string;
32
+ steps?: number;
33
+ yes?: boolean;
34
+ }
35
+
36
+ async function migrateAction(action: MigrateAction | undefined, opts: MigrateOptions) {
37
+ const cwd = path.resolve(opts.cwd);
38
+
39
+ intro(chalk.bgMagenta.black(" Baasix Migrations "));
40
+
41
+ // Load config
42
+ const config = await getConfig(cwd);
43
+ if (!config && !opts.url) {
44
+ log.error(
45
+ "No Baasix configuration found. Create a .env file with BAASIX_URL or use --url flag."
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ // Override URL if provided
51
+ const effectiveConfig: BaasixConfig = config
52
+ ? { ...config, url: opts.url || config.url }
53
+ : { url: opts.url || "http://localhost:8056" };
54
+
55
+ // Select action if not provided
56
+ let selectedAction = action || opts.action;
57
+ if (!selectedAction) {
58
+ const result = await select({
59
+ message: "What migration action do you want to perform?",
60
+ options: [
61
+ {
62
+ value: "status",
63
+ label: "Status",
64
+ hint: "Show current migration status",
65
+ },
66
+ {
67
+ value: "list",
68
+ label: "List",
69
+ hint: "List all available migrations",
70
+ },
71
+ {
72
+ value: "run",
73
+ label: "Run",
74
+ hint: "Run pending migrations",
75
+ },
76
+ {
77
+ value: "create",
78
+ label: "Create",
79
+ hint: "Create a new migration file",
80
+ },
81
+ {
82
+ value: "rollback",
83
+ label: "Rollback",
84
+ hint: "Rollback the last batch of migrations",
85
+ },
86
+ {
87
+ value: "reset",
88
+ label: "Reset",
89
+ hint: "Rollback all migrations (dangerous!)",
90
+ },
91
+ ],
92
+ });
93
+
94
+ if (isCancel(result)) {
95
+ cancel("Operation cancelled");
96
+ process.exit(0);
97
+ }
98
+ selectedAction = result as MigrateAction;
99
+ }
100
+
101
+ const s = spinner();
102
+
103
+ try {
104
+ switch (selectedAction) {
105
+ case "status":
106
+ await showStatus(s, effectiveConfig, cwd);
107
+ break;
108
+
109
+ case "list":
110
+ await listMigrations(s, effectiveConfig, cwd);
111
+ break;
112
+
113
+ case "run":
114
+ await runMigrations(s, effectiveConfig, cwd, opts.yes);
115
+ break;
116
+
117
+ case "create":
118
+ await createMigration(s, cwd, opts.name);
119
+ break;
120
+
121
+ case "rollback":
122
+ await rollbackMigrations(s, effectiveConfig, cwd, opts.steps || 1, opts.yes);
123
+ break;
124
+
125
+ case "reset":
126
+ await resetMigrations(s, effectiveConfig, cwd, opts.yes);
127
+ break;
128
+ }
129
+ } catch (error) {
130
+ s.stop("Migration failed");
131
+ if (error instanceof Error) {
132
+ log.error(error.message);
133
+ } else {
134
+ log.error("Unknown error occurred");
135
+ }
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ async function showStatus(
141
+ s: ReturnType<typeof spinner>,
142
+ config: BaasixConfig,
143
+ cwd: string
144
+ ) {
145
+ s.start("Checking migration status...");
146
+
147
+ // Get executed migrations from database
148
+ const executedMigrations = await getExecutedMigrations(config);
149
+
150
+ // Get local migration files
151
+ const localMigrations = await getLocalMigrations(cwd);
152
+
153
+ s.stop("Migration status retrieved");
154
+
155
+ // Calculate pending
156
+ const executedNames = new Set(executedMigrations.map((m) => m.name));
157
+ const pendingMigrations = localMigrations.filter((m) => !executedNames.has(m));
158
+
159
+ console.log();
160
+ console.log(chalk.bold("📊 Migration Status"));
161
+ console.log(chalk.dim("─".repeat(50)));
162
+ console.log(` Total migrations: ${chalk.cyan(localMigrations.length)}`);
163
+ console.log(` Executed: ${chalk.green(executedMigrations.length)}`);
164
+ console.log(
165
+ ` Pending: ${pendingMigrations.length > 0 ? chalk.yellow(pendingMigrations.length) : chalk.gray("0")}`
166
+ );
167
+ console.log();
168
+
169
+ if (pendingMigrations.length > 0) {
170
+ console.log(chalk.bold("Pending migrations:"));
171
+ for (const migration of pendingMigrations) {
172
+ console.log(` ${chalk.yellow("○")} ${migration}`);
173
+ }
174
+ console.log();
175
+ console.log(
176
+ chalk.dim(`Run ${chalk.cyan("baasix migrate run")} to execute pending migrations.`)
177
+ );
178
+ } else {
179
+ console.log(chalk.green("✓ All migrations have been executed."));
180
+ }
181
+
182
+ outro("");
183
+ }
184
+
185
+ async function listMigrations(
186
+ s: ReturnType<typeof spinner>,
187
+ config: BaasixConfig,
188
+ cwd: string
189
+ ) {
190
+ s.start("Fetching migrations...");
191
+
192
+ const executedMigrations = await getExecutedMigrations(config);
193
+ const localMigrations = await getLocalMigrations(cwd);
194
+
195
+ s.stop("Migrations retrieved");
196
+
197
+ const executedMap = new Map(executedMigrations.map((m) => [m.name, m]));
198
+
199
+ console.log();
200
+ console.log(chalk.bold("📋 All Migrations"));
201
+ console.log(chalk.dim("─".repeat(70)));
202
+
203
+ if (localMigrations.length === 0) {
204
+ console.log(chalk.dim(" No migrations found."));
205
+ } else {
206
+ for (const name of localMigrations) {
207
+ const executed = executedMap.get(name);
208
+ if (executed) {
209
+ const executedDate = executed.executedAt
210
+ ? new Date(executed.executedAt).toLocaleDateString()
211
+ : "unknown date";
212
+ console.log(
213
+ ` ${chalk.green("✓")} ${name} ${chalk.dim(`(batch ${executed.batch || "?"}, ${executedDate})`)}`
214
+ );
215
+ } else {
216
+ console.log(` ${chalk.yellow("○")} ${name} ${chalk.dim("(pending)")}`);
217
+ }
218
+ }
219
+ }
220
+
221
+ console.log();
222
+ outro("");
223
+ }
224
+
225
+ async function runMigrations(
226
+ s: ReturnType<typeof spinner>,
227
+ config: BaasixConfig,
228
+ cwd: string,
229
+ skipConfirm?: boolean
230
+ ) {
231
+ s.start("Checking for pending migrations...");
232
+
233
+ const executedMigrations = await getExecutedMigrations(config);
234
+ const localMigrations = await getLocalMigrations(cwd);
235
+
236
+ const executedNames = new Set(executedMigrations.map((m) => m.name));
237
+ const pendingMigrations = localMigrations.filter((m) => !executedNames.has(m));
238
+
239
+ if (pendingMigrations.length === 0) {
240
+ s.stop("No pending migrations");
241
+ log.info("All migrations have already been executed.");
242
+ outro("");
243
+ return;
244
+ }
245
+
246
+ s.stop(`Found ${pendingMigrations.length} pending migrations`);
247
+
248
+ console.log();
249
+ console.log(chalk.bold("Migrations to run:"));
250
+ for (const name of pendingMigrations) {
251
+ console.log(` ${chalk.cyan("→")} ${name}`);
252
+ }
253
+ console.log();
254
+
255
+ if (!skipConfirm) {
256
+ const confirmed = await confirm({
257
+ message: `Run ${pendingMigrations.length} migration(s)?`,
258
+ initialValue: true,
259
+ });
260
+
261
+ if (isCancel(confirmed) || !confirmed) {
262
+ cancel("Operation cancelled");
263
+ process.exit(0);
264
+ }
265
+ }
266
+
267
+ s.start("Running migrations...");
268
+
269
+ try {
270
+ const result = await apiRunMigrations(config, {
271
+ step: pendingMigrations.length,
272
+ });
273
+
274
+ if (result.success) {
275
+ s.stop("Migrations executed");
276
+ outro(chalk.green(`✨ ${result.message}`));
277
+ } else {
278
+ s.stop("Migration failed");
279
+ log.error(result.message);
280
+ process.exit(1);
281
+ }
282
+ } catch (error) {
283
+ s.stop("Migration failed");
284
+ throw error;
285
+ }
286
+ }
287
+
288
+ async function createMigration(
289
+ s: ReturnType<typeof spinner>,
290
+ cwd: string,
291
+ name?: string
292
+ ) {
293
+ // Get migration name
294
+ let migrationName = name;
295
+ if (!migrationName) {
296
+ const result = await text({
297
+ message: "Migration name:",
298
+ placeholder: "create_users_table",
299
+ validate: (value) => {
300
+ if (!value) return "Migration name is required";
301
+ if (!/^[a-z0-9_]+$/i.test(value)) {
302
+ return "Migration name can only contain letters, numbers, and underscores";
303
+ }
304
+ return undefined;
305
+ },
306
+ });
307
+
308
+ if (isCancel(result)) {
309
+ cancel("Operation cancelled");
310
+ process.exit(0);
311
+ }
312
+ migrationName = result as string;
313
+ }
314
+
315
+ s.start("Creating migration file...");
316
+
317
+ const migrationsDir = path.join(cwd, "migrations");
318
+
319
+ // Ensure migrations directory exists
320
+ if (!existsSync(migrationsDir)) {
321
+ await fs.mkdir(migrationsDir, { recursive: true });
322
+ }
323
+
324
+ // Generate timestamp prefix
325
+ const timestamp = new Date()
326
+ .toISOString()
327
+ .replace(/[-:T.Z]/g, "")
328
+ .slice(0, 14);
329
+ const filename = `${timestamp}_${migrationName}.js`;
330
+ const filepath = path.join(migrationsDir, filename);
331
+
332
+ // Check if file exists
333
+ if (existsSync(filepath)) {
334
+ s.stop("File already exists");
335
+ log.error(`Migration file ${filename} already exists.`);
336
+ process.exit(1);
337
+ }
338
+
339
+ // Generate migration template
340
+ const template = `/**
341
+ * Migration: ${migrationName}
342
+ * Created: ${new Date().toISOString()}
343
+ */
344
+
345
+ /**
346
+ * Run the migration
347
+ * @param {import("@tspvivek/baasix-sdk").BaasixClient} baasix - Baasix client
348
+ */
349
+ export async function up(baasix) {
350
+ // Example: Create a collection
351
+ // await baasix.schema.create("tableName", {
352
+ // name: "TableName",
353
+ // timestamps: true,
354
+ // fields: {
355
+ // id: { type: "UUID", primaryKey: true, defaultValue: { type: "UUIDV4" } },
356
+ // name: { type: "String", allowNull: false, values: { length: 255 } },
357
+ // },
358
+ // });
359
+
360
+ // Example: Add a field
361
+ // await baasix.schema.update("tableName", {
362
+ // fields: {
363
+ // newField: { type: "String", allowNull: true },
364
+ // },
365
+ // });
366
+
367
+ // Example: Insert data
368
+ // await baasix.items("tableName").create({ name: "Example" });
369
+ }
370
+
371
+ /**
372
+ * Reverse the migration
373
+ * @param {import("@tspvivek/baasix-sdk").BaasixClient} baasix - Baasix client
374
+ */
375
+ export async function down(baasix) {
376
+ // Reverse the changes made in up()
377
+ // Example: Drop a collection
378
+ // await baasix.schema.delete("tableName");
379
+ }
380
+ `;
381
+
382
+ await fs.writeFile(filepath, template);
383
+
384
+ s.stop("Migration created");
385
+
386
+ outro(chalk.green(`✨ Created migration: ${chalk.cyan(filename)}`));
387
+ console.log();
388
+ console.log(` Edit: ${chalk.dim(path.relative(cwd, filepath))}`);
389
+ console.log();
390
+ }
391
+
392
+ async function rollbackMigrations(
393
+ s: ReturnType<typeof spinner>,
394
+ config: BaasixConfig,
395
+ cwd: string,
396
+ steps: number,
397
+ skipConfirm?: boolean
398
+ ) {
399
+ s.start("Fetching executed migrations...");
400
+
401
+ const executedMigrations = await getExecutedMigrations(config);
402
+
403
+ if (executedMigrations.length === 0) {
404
+ s.stop("No migrations to rollback");
405
+ log.info("No migrations have been executed.");
406
+ outro("");
407
+ return;
408
+ }
409
+
410
+ // Get migrations to rollback (by batch, descending)
411
+ const sortedByBatch = [...executedMigrations].sort(
412
+ (a, b) => (b.batch || 0) - (a.batch || 0)
413
+ );
414
+ const batchesToRollback = new Set<number>();
415
+ const migrationsToRollback: MigrationInfo[] = [];
416
+
417
+ for (const migration of sortedByBatch) {
418
+ const batch = migration.batch || 0;
419
+ if (batchesToRollback.size < steps) {
420
+ batchesToRollback.add(batch);
421
+ }
422
+ if (batchesToRollback.has(batch)) {
423
+ migrationsToRollback.push(migration);
424
+ }
425
+ }
426
+
427
+ s.stop(`Found ${migrationsToRollback.length} migration(s) to rollback`);
428
+
429
+ console.log();
430
+ console.log(chalk.bold("Migrations to rollback:"));
431
+ for (const migration of migrationsToRollback) {
432
+ console.log(
433
+ ` ${chalk.red("←")} ${migration.name} ${chalk.dim(`(batch ${migration.batch || "?"})`)}`
434
+ );
435
+ }
436
+ console.log();
437
+
438
+ if (!skipConfirm) {
439
+ const confirmed = await confirm({
440
+ message: `Rollback ${migrationsToRollback.length} migration(s)?`,
441
+ initialValue: false,
442
+ });
443
+
444
+ if (isCancel(confirmed) || !confirmed) {
445
+ cancel("Operation cancelled");
446
+ process.exit(0);
447
+ }
448
+ }
449
+
450
+ s.start("Rolling back migrations...");
451
+
452
+ try {
453
+ const result = await apiRollbackMigrations(config, {
454
+ step: steps,
455
+ });
456
+
457
+ if (result.success) {
458
+ s.stop("Rollback complete");
459
+ outro(chalk.green(`✨ ${result.message}`));
460
+ } else {
461
+ s.stop("Rollback failed");
462
+ log.error(result.message);
463
+ process.exit(1);
464
+ }
465
+ } catch (error) {
466
+ s.stop("Rollback failed");
467
+ throw error;
468
+ }
469
+ }
470
+
471
+ async function resetMigrations(
472
+ s: ReturnType<typeof spinner>,
473
+ config: BaasixConfig,
474
+ cwd: string,
475
+ skipConfirm?: boolean
476
+ ) {
477
+ s.start("Fetching all executed migrations...");
478
+
479
+ const executedMigrations = await getExecutedMigrations(config);
480
+
481
+ if (executedMigrations.length === 0) {
482
+ s.stop("No migrations to reset");
483
+ log.info("No migrations have been executed.");
484
+ outro("");
485
+ return;
486
+ }
487
+
488
+ s.stop(`Found ${executedMigrations.length} executed migration(s)`);
489
+
490
+ console.log();
491
+ log.warn(chalk.red.bold("⚠️ This will rollback ALL migrations!"));
492
+ console.log();
493
+
494
+ if (!skipConfirm) {
495
+ const confirmed = await confirm({
496
+ message: `Reset all ${executedMigrations.length} migration(s)? This cannot be undone!`,
497
+ initialValue: false,
498
+ });
499
+
500
+ if (isCancel(confirmed) || !confirmed) {
501
+ cancel("Operation cancelled");
502
+ process.exit(0);
503
+ }
504
+
505
+ // Double confirmation for dangerous operation
506
+ const doubleConfirm = await text({
507
+ message: "Type 'reset' to confirm:",
508
+ placeholder: "reset",
509
+ validate: (value) =>
510
+ value !== "reset" ? "Please type 'reset' to confirm" : undefined,
511
+ });
512
+
513
+ if (isCancel(doubleConfirm)) {
514
+ cancel("Operation cancelled");
515
+ process.exit(0);
516
+ }
517
+ }
518
+
519
+ s.start("Resetting all migrations...");
520
+
521
+ try {
522
+ // Rollback all batches
523
+ const maxBatch = Math.max(...executedMigrations.map((m) => m.batch || 0));
524
+ const result = await apiRollbackMigrations(config, {
525
+ step: maxBatch,
526
+ });
527
+
528
+ if (result.success) {
529
+ s.stop("Reset complete");
530
+ outro(chalk.green(`✨ ${result.message}`));
531
+ } else {
532
+ s.stop("Reset failed");
533
+ log.error(result.message);
534
+ process.exit(1);
535
+ }
536
+ } catch (error) {
537
+ s.stop("Reset failed");
538
+ throw error;
539
+ }
540
+ }
541
+
542
+ async function getExecutedMigrations(config: BaasixConfig): Promise<MigrationInfo[]> {
543
+ try {
544
+ return await fetchMigrations(config);
545
+ } catch {
546
+ // If migrations endpoint doesn't exist, return empty
547
+ return [];
548
+ }
549
+ }
550
+
551
+ async function getLocalMigrations(cwd: string): Promise<string[]> {
552
+ const migrationsDir = path.join(cwd, "migrations");
553
+
554
+ if (!existsSync(migrationsDir)) {
555
+ return [];
556
+ }
557
+
558
+ const files = await fs.readdir(migrationsDir);
559
+ return files.filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
560
+ }
561
+
562
+ export const migrate = new Command("migrate")
563
+ .description("Run database migrations")
564
+ .argument(
565
+ "[action]",
566
+ "Migration action (status, list, run, create, rollback, reset)"
567
+ )
568
+ .option("-c, --cwd <path>", "Working directory", process.cwd())
569
+ .option("--url <url>", "Baasix server URL")
570
+ .option("-n, --name <name>", "Migration name (for create)")
571
+ .option("-s, --steps <number>", "Number of batches to rollback", parseInt)
572
+ .option("-y, --yes", "Skip confirmation prompts")
573
+ .action(migrateAction);
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import { init } from "./commands/init.js";
3
+ import { generate } from "./commands/generate.js";
4
+ import { extension } from "./commands/extension.js";
5
+ import { migrate } from "./commands/migrate.js";
6
+ import { getPackageInfo } from "./utils/get-package-info.js";
7
+
8
+ import "dotenv/config";
9
+
10
+ // Handle exit signals
11
+ process.on("SIGINT", () => process.exit(0));
12
+ process.on("SIGTERM", () => process.exit(0));
13
+
14
+ async function main() {
15
+ const program = new Command("baasix");
16
+
17
+ let packageInfo: Record<string, unknown> = {};
18
+ try {
19
+ packageInfo = await getPackageInfo();
20
+ } catch {
21
+ // Ignore errors reading package.json
22
+ }
23
+
24
+ program
25
+ .addCommand(init)
26
+ .addCommand(generate)
27
+ .addCommand(extension)
28
+ .addCommand(migrate)
29
+ .version((packageInfo.version as string) || "0.1.0")
30
+ .description("Baasix CLI - Backend-as-a-Service toolkit")
31
+ .action(() => program.help());
32
+
33
+ program.parse();
34
+ }
35
+
36
+ main().catch((error) => {
37
+ console.error("Error running Baasix CLI:", error);
38
+ process.exit(1);
39
+ });