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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2521 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command5 } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import fs from "fs/promises";
9
+ import path2 from "path";
10
+ import {
11
+ cancel,
12
+ confirm,
13
+ intro,
14
+ isCancel,
15
+ log,
16
+ outro,
17
+ select,
18
+ spinner,
19
+ text,
20
+ multiselect
21
+ } from "@clack/prompts";
22
+ import chalk from "chalk";
23
+ import { Command } from "commander";
24
+ import crypto from "crypto";
25
+
26
+ // src/utils/package-manager.ts
27
+ import { exec } from "child_process";
28
+ import { existsSync } from "fs";
29
+ import path from "path";
30
+ function detectPackageManager(cwd) {
31
+ if (existsSync(path.join(cwd, "bun.lockb"))) {
32
+ return "bun";
33
+ }
34
+ if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
35
+ return "pnpm";
36
+ }
37
+ if (existsSync(path.join(cwd, "yarn.lock"))) {
38
+ return "yarn";
39
+ }
40
+ return "npm";
41
+ }
42
+ function installDependencies({
43
+ dependencies,
44
+ packageManager,
45
+ cwd,
46
+ dev = false
47
+ }) {
48
+ let installCommand;
49
+ const devFlag = dev ? packageManager === "npm" ? " --save-dev" : " -D" : "";
50
+ switch (packageManager) {
51
+ case "npm":
52
+ installCommand = `npm install${devFlag}`;
53
+ break;
54
+ case "pnpm":
55
+ installCommand = `pnpm add${devFlag}`;
56
+ break;
57
+ case "bun":
58
+ installCommand = `bun add${devFlag}`;
59
+ break;
60
+ case "yarn":
61
+ installCommand = `yarn add${devFlag}`;
62
+ break;
63
+ default:
64
+ throw new Error("Invalid package manager");
65
+ }
66
+ const command = `${installCommand} ${dependencies.join(" ")}`;
67
+ return new Promise((resolve, reject) => {
68
+ exec(command, { cwd }, (error, stdout, stderr) => {
69
+ if (error) {
70
+ reject(new Error(stderr || error.message));
71
+ return;
72
+ }
73
+ resolve(true);
74
+ });
75
+ });
76
+ }
77
+
78
+ // src/commands/init.ts
79
+ function generateSecret(length = 64) {
80
+ return crypto.randomBytes(length).toString("base64url").slice(0, length);
81
+ }
82
+ async function initAction(opts) {
83
+ const cwd = path2.resolve(opts.cwd);
84
+ intro(chalk.bgCyan.black(" Baasix Project Setup "));
85
+ let projectName = opts.name;
86
+ if (!projectName) {
87
+ const result = await text({
88
+ message: "What is your project name?",
89
+ placeholder: "my-baasix-app",
90
+ defaultValue: "my-baasix-app",
91
+ validate: (value) => {
92
+ if (!value) return "Project name is required";
93
+ if (!/^[a-z0-9-_]+$/i.test(value)) return "Project name must be alphanumeric with dashes or underscores";
94
+ return void 0;
95
+ }
96
+ });
97
+ if (isCancel(result)) {
98
+ cancel("Operation cancelled");
99
+ process.exit(0);
100
+ }
101
+ projectName = result;
102
+ }
103
+ let template = opts.template;
104
+ if (!template) {
105
+ const result = await select({
106
+ message: "Select a project template:",
107
+ options: [
108
+ {
109
+ value: "api",
110
+ label: "API Only",
111
+ hint: "Baasix server with basic configuration"
112
+ },
113
+ {
114
+ value: "nextjs-app",
115
+ label: "Next.js (App Router)",
116
+ hint: "Next.js 14+ with App Router and SDK integration"
117
+ },
118
+ {
119
+ value: "nextjs",
120
+ label: "Next.js (Pages Router)",
121
+ hint: "Next.js with Pages Router and SDK integration"
122
+ }
123
+ ]
124
+ });
125
+ if (isCancel(result)) {
126
+ cancel("Operation cancelled");
127
+ process.exit(0);
128
+ }
129
+ template = result;
130
+ }
131
+ const config = await collectProjectConfig(projectName, template, opts.yes);
132
+ if (!config) {
133
+ cancel("Operation cancelled");
134
+ process.exit(0);
135
+ }
136
+ const projectPath = path2.join(cwd, projectName);
137
+ if (existsSync2(projectPath)) {
138
+ const overwrite = await confirm({
139
+ message: `Directory ${projectName} already exists. Overwrite?`,
140
+ initialValue: false
141
+ });
142
+ if (isCancel(overwrite) || !overwrite) {
143
+ cancel("Operation cancelled");
144
+ process.exit(0);
145
+ }
146
+ }
147
+ const s = spinner();
148
+ s.start("Creating project structure...");
149
+ try {
150
+ await fs.mkdir(projectPath, { recursive: true });
151
+ if (template === "api") {
152
+ await createApiProject(projectPath, config);
153
+ } else if (template === "nextjs-app" || template === "nextjs") {
154
+ await createNextJsProject(projectPath, config, template === "nextjs-app");
155
+ }
156
+ s.stop("Project structure created");
157
+ const packageManager = detectPackageManager(cwd);
158
+ const shouldInstall = opts.yes || await confirm({
159
+ message: `Install dependencies with ${packageManager}?`,
160
+ initialValue: true
161
+ });
162
+ if (shouldInstall && !isCancel(shouldInstall)) {
163
+ s.start("Installing dependencies...");
164
+ try {
165
+ await installDependencies({
166
+ dependencies: [],
167
+ packageManager,
168
+ cwd: projectPath
169
+ });
170
+ s.stop("Dependencies installed");
171
+ } catch (error) {
172
+ s.stop("Failed to install dependencies");
173
+ log.warn(`Run ${chalk.cyan(`cd ${projectName} && ${packageManager} install`)} to install manually`);
174
+ }
175
+ }
176
+ outro(chalk.green("\u2728 Project created successfully!"));
177
+ console.log();
178
+ console.log(chalk.bold("Next steps:"));
179
+ console.log(` ${chalk.cyan(`cd ${projectName}`)}`);
180
+ if (template === "api") {
181
+ console.log(` ${chalk.cyan("# Review and update your .env file")}`);
182
+ console.log(` ${chalk.cyan(`${packageManager} run dev`)}`);
183
+ } else {
184
+ console.log(` ${chalk.cyan(`${packageManager} run dev`)} ${chalk.dim("# Start Next.js frontend")}`);
185
+ console.log();
186
+ console.log(chalk.dim(" Note: This is a frontend-only project. You need a separate Baasix API."));
187
+ console.log(chalk.dim(` To create an API: ${chalk.cyan("npx @tspvivek/baasix-cli init --template api")}`));
188
+ }
189
+ console.log();
190
+ } catch (error) {
191
+ s.stop("Failed to create project");
192
+ log.error(error instanceof Error ? error.message : "Unknown error");
193
+ process.exit(1);
194
+ }
195
+ }
196
+ async function collectProjectConfig(projectName, template, skipPrompts) {
197
+ if (skipPrompts) {
198
+ return {
199
+ projectName,
200
+ template,
201
+ databaseUrl: "postgresql://postgres:password@localhost:5432/baasix",
202
+ socketEnabled: false,
203
+ multiTenant: false,
204
+ publicRegistration: true,
205
+ storageDriver: "LOCAL",
206
+ s3Config: void 0,
207
+ cacheAdapter: "memory",
208
+ redisUrl: void 0,
209
+ authServices: ["LOCAL"],
210
+ mailEnabled: false,
211
+ openApiEnabled: true
212
+ };
213
+ }
214
+ const dbUrl = await text({
215
+ message: "PostgreSQL connection URL:",
216
+ placeholder: "postgresql://postgres:password@localhost:5432/baasix",
217
+ defaultValue: "postgresql://postgres:password@localhost:5432/baasix"
218
+ });
219
+ if (isCancel(dbUrl)) return null;
220
+ const multiTenant = await confirm({
221
+ message: "Enable multi-tenancy?",
222
+ initialValue: false
223
+ });
224
+ if (isCancel(multiTenant)) return null;
225
+ const publicRegistration = await confirm({
226
+ message: "Allow public user registration?",
227
+ initialValue: true
228
+ });
229
+ if (isCancel(publicRegistration)) return null;
230
+ const socketEnabled = await confirm({
231
+ message: "Enable real-time features (WebSocket)?",
232
+ initialValue: false
233
+ });
234
+ if (isCancel(socketEnabled)) return null;
235
+ const storageDriver = await select({
236
+ message: "Select storage driver:",
237
+ options: [
238
+ { value: "LOCAL", label: "Local Storage", hint: "Store files locally in uploads folder" },
239
+ { value: "S3", label: "S3 Compatible", hint: "AWS S3, DigitalOcean Spaces, MinIO, etc." }
240
+ ]
241
+ });
242
+ if (isCancel(storageDriver)) return null;
243
+ let s3Config;
244
+ if (storageDriver === "S3") {
245
+ const endpoint = await text({
246
+ message: "S3 endpoint:",
247
+ placeholder: "s3.amazonaws.com",
248
+ defaultValue: "s3.amazonaws.com"
249
+ });
250
+ if (isCancel(endpoint)) return null;
251
+ const bucket = await text({
252
+ message: "S3 bucket name:",
253
+ placeholder: "my-bucket"
254
+ });
255
+ if (isCancel(bucket)) return null;
256
+ const accessKey = await text({
257
+ message: "S3 Access Key ID:",
258
+ placeholder: "AKIAIOSFODNN7EXAMPLE"
259
+ });
260
+ if (isCancel(accessKey)) return null;
261
+ const secretKey = await text({
262
+ message: "S3 Secret Access Key:",
263
+ placeholder: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
264
+ });
265
+ if (isCancel(secretKey)) return null;
266
+ const region = await text({
267
+ message: "S3 Region:",
268
+ placeholder: "us-east-1",
269
+ defaultValue: "us-east-1"
270
+ });
271
+ if (isCancel(region)) return null;
272
+ s3Config = {
273
+ endpoint,
274
+ bucket,
275
+ accessKey,
276
+ secretKey,
277
+ region
278
+ };
279
+ }
280
+ const cacheAdapter = await select({
281
+ message: "Select cache adapter:",
282
+ options: [
283
+ { value: "memory", label: "In-Memory", hint: "Simple, good for development" },
284
+ { value: "redis", label: "Redis/Valkey", hint: "Recommended for production" }
285
+ ]
286
+ });
287
+ if (isCancel(cacheAdapter)) return null;
288
+ let redisUrl;
289
+ if (cacheAdapter === "redis") {
290
+ const url = await text({
291
+ message: "Redis connection URL:",
292
+ placeholder: "redis://localhost:6379",
293
+ defaultValue: "redis://localhost:6379"
294
+ });
295
+ if (isCancel(url)) return null;
296
+ redisUrl = url;
297
+ }
298
+ const authServices = await multiselect({
299
+ message: "Select authentication methods:",
300
+ options: [
301
+ { value: "LOCAL", label: "Email/Password", hint: "Built-in authentication" },
302
+ { value: "GOOGLE", label: "Google OAuth" },
303
+ { value: "FACEBOOK", label: "Facebook OAuth" },
304
+ { value: "GITHUB", label: "GitHub OAuth" },
305
+ { value: "APPLE", label: "Apple Sign In" }
306
+ ],
307
+ initialValues: ["LOCAL"],
308
+ required: true
309
+ });
310
+ if (isCancel(authServices)) return null;
311
+ const openApiEnabled = await confirm({
312
+ message: "Enable OpenAPI documentation (Swagger)?",
313
+ initialValue: true
314
+ });
315
+ if (isCancel(openApiEnabled)) return null;
316
+ const mailEnabled = await confirm({
317
+ message: "Configure email sending?",
318
+ initialValue: false
319
+ });
320
+ if (isCancel(mailEnabled)) return null;
321
+ return {
322
+ projectName,
323
+ template,
324
+ databaseUrl: dbUrl,
325
+ socketEnabled,
326
+ multiTenant,
327
+ publicRegistration,
328
+ storageDriver,
329
+ s3Config,
330
+ cacheAdapter,
331
+ redisUrl,
332
+ authServices,
333
+ mailEnabled,
334
+ openApiEnabled
335
+ };
336
+ }
337
+ async function createApiProject(projectPath, config) {
338
+ const secretKey = generateSecret(64);
339
+ const packageJson = {
340
+ name: config.projectName,
341
+ version: "0.1.0",
342
+ type: "module",
343
+ scripts: {
344
+ dev: "node --watch server.js",
345
+ start: "node server.js"
346
+ },
347
+ dependencies: {
348
+ "@tspvivek/baasix": "latest",
349
+ "dotenv": "^16.3.1"
350
+ }
351
+ };
352
+ await fs.writeFile(
353
+ path2.join(projectPath, "package.json"),
354
+ JSON.stringify(packageJson, null, 2)
355
+ );
356
+ const serverJs = `import { startServer } from "@tspvivek/baasix";
357
+
358
+ startServer({
359
+ port: process.env.PORT || 8056,
360
+ logger: {
361
+ level: process.env.LOG_LEVEL || "info",
362
+ pretty: process.env.NODE_ENV !== "production",
363
+ },
364
+ }).catch((error) => {
365
+ console.error("Failed to start server:", error);
366
+ process.exit(1);
367
+ });
368
+ `;
369
+ await fs.writeFile(path2.join(projectPath, "server.js"), serverJs);
370
+ const envContent = generateEnvContent(config, secretKey);
371
+ await fs.writeFile(path2.join(projectPath, ".env"), envContent);
372
+ const envExample = generateEnvExample(config);
373
+ await fs.writeFile(path2.join(projectPath, ".env.example"), envExample);
374
+ const gitignore = `node_modules/
375
+ .env
376
+ uploads/
377
+ logs/
378
+ dist/
379
+ .cache/
380
+ .temp/
381
+ `;
382
+ await fs.writeFile(path2.join(projectPath, ".gitignore"), gitignore);
383
+ await fs.mkdir(path2.join(projectPath, "extensions"), { recursive: true });
384
+ await fs.writeFile(
385
+ path2.join(projectPath, "extensions", ".gitkeep"),
386
+ "# Place your Baasix extensions here\n"
387
+ );
388
+ if (config.storageDriver === "LOCAL") {
389
+ await fs.mkdir(path2.join(projectPath, "uploads"), { recursive: true });
390
+ await fs.writeFile(path2.join(projectPath, "uploads", ".gitkeep"), "");
391
+ }
392
+ await fs.mkdir(path2.join(projectPath, "migrations"), { recursive: true });
393
+ await fs.writeFile(path2.join(projectPath, "migrations", ".gitkeep"), "");
394
+ const readme = generateReadme(config);
395
+ await fs.writeFile(path2.join(projectPath, "README.md"), readme);
396
+ }
397
+ function generateEnvContent(config, secretKey) {
398
+ const lines = [];
399
+ lines.push("#-----------------------------------");
400
+ lines.push("# Server");
401
+ lines.push("#-----------------------------------");
402
+ lines.push("PORT=8056");
403
+ lines.push("HOST=localhost");
404
+ lines.push("NODE_ENV=development");
405
+ lines.push("DEBUGGING=false");
406
+ lines.push("");
407
+ lines.push("#-----------------------------------");
408
+ lines.push("# Database");
409
+ lines.push("#-----------------------------------");
410
+ lines.push(`DATABASE_URL="${config.databaseUrl}"`);
411
+ lines.push("DATABASE_LOGGING=false");
412
+ lines.push("DATABASE_POOL_MAX=20");
413
+ lines.push("DATABASE_POOL_MIN=0");
414
+ lines.push("");
415
+ lines.push("#-----------------------------------");
416
+ lines.push("# Security");
417
+ lines.push("#-----------------------------------");
418
+ lines.push(`SECRET_KEY=${secretKey}`);
419
+ lines.push("ACCESS_TOKEN_EXPIRES_IN=31536000");
420
+ lines.push("");
421
+ lines.push("#-----------------------------------");
422
+ lines.push("# Multi-tenancy");
423
+ lines.push("#-----------------------------------");
424
+ lines.push(`MULTI_TENANT=${config.multiTenant}`);
425
+ lines.push(`PUBLIC_REGISTRATION=${config.publicRegistration}`);
426
+ if (!config.multiTenant) {
427
+ lines.push("DEFAULT_ROLE_REGISTERED=user");
428
+ }
429
+ lines.push("");
430
+ lines.push("#-----------------------------------");
431
+ lines.push("# Real-time (WebSocket)");
432
+ lines.push("#-----------------------------------");
433
+ lines.push(`SOCKET_ENABLED=${config.socketEnabled}`);
434
+ if (config.socketEnabled) {
435
+ lines.push('SOCKET_CORS_ENABLED_ORIGINS="http://localhost:3000,http://localhost:8056"');
436
+ lines.push("SOCKET_PATH=/realtime");
437
+ if (config.cacheAdapter === "redis" && config.redisUrl) {
438
+ lines.push("SOCKET_REDIS_ENABLED=true");
439
+ lines.push(`SOCKET_REDIS_URL=${config.redisUrl}`);
440
+ }
441
+ }
442
+ lines.push("");
443
+ lines.push("#-----------------------------------");
444
+ lines.push("# Cache");
445
+ lines.push("#-----------------------------------");
446
+ lines.push("CACHE_ENABLED=true");
447
+ lines.push(`CACHE_ADAPTER=${config.cacheAdapter}`);
448
+ lines.push("CACHE_TTL=300");
449
+ lines.push("CACHE_STRATEGY=explicit");
450
+ if (config.cacheAdapter === "memory") {
451
+ lines.push("CACHE_SIZE_GB=1");
452
+ } else if (config.cacheAdapter === "redis" && config.redisUrl) {
453
+ lines.push(`CACHE_REDIS_URL=${config.redisUrl}`);
454
+ }
455
+ lines.push("");
456
+ lines.push("#-----------------------------------");
457
+ lines.push("# Storage");
458
+ lines.push("#-----------------------------------");
459
+ if (config.storageDriver === "LOCAL") {
460
+ lines.push('STORAGE_SERVICES_ENABLED="LOCAL"');
461
+ lines.push('STORAGE_DEFAULT_SERVICE="LOCAL"');
462
+ lines.push("STORAGE_TEMP_PATH=./.temp");
463
+ lines.push("");
464
+ lines.push("# Local Storage");
465
+ lines.push("LOCAL_STORAGE_DRIVER=LOCAL");
466
+ lines.push('LOCAL_STORAGE_PATH="./uploads"');
467
+ } else if (config.storageDriver === "S3" && config.s3Config) {
468
+ lines.push('STORAGE_SERVICES_ENABLED="S3"');
469
+ lines.push('STORAGE_DEFAULT_SERVICE="S3"');
470
+ lines.push("STORAGE_TEMP_PATH=./.temp");
471
+ lines.push("");
472
+ lines.push("# S3 Compatible Storage");
473
+ lines.push("S3_STORAGE_DRIVER=S3");
474
+ lines.push(`S3_STORAGE_ENDPOINT=${config.s3Config.endpoint}`);
475
+ lines.push(`S3_STORAGE_BUCKET=${config.s3Config.bucket}`);
476
+ lines.push(`S3_STORAGE_ACCESS_KEY_ID=${config.s3Config.accessKey}`);
477
+ lines.push(`S3_STORAGE_SECRET_ACCESS_KEY=${config.s3Config.secretKey}`);
478
+ lines.push(`S3_STORAGE_REGION=${config.s3Config.region}`);
479
+ }
480
+ lines.push("");
481
+ lines.push("#-----------------------------------");
482
+ lines.push("# Authentication");
483
+ lines.push("#-----------------------------------");
484
+ lines.push(`AUTH_SERVICES_ENABLED=${config.authServices.join(",")}`);
485
+ lines.push('AUTH_APP_URL="http://localhost:3000,http://localhost:8056"');
486
+ lines.push("");
487
+ if (config.authServices.includes("GOOGLE")) {
488
+ lines.push("# Google OAuth");
489
+ lines.push("GOOGLE_CLIENT_ID=your_google_client_id");
490
+ lines.push("GOOGLE_CLIENT_SECRET=your_google_client_secret");
491
+ lines.push("");
492
+ }
493
+ if (config.authServices.includes("FACEBOOK")) {
494
+ lines.push("# Facebook OAuth");
495
+ lines.push("FACEBOOK_CLIENT_ID=your_facebook_client_id");
496
+ lines.push("FACEBOOK_CLIENT_SECRET=your_facebook_client_secret");
497
+ lines.push("");
498
+ }
499
+ if (config.authServices.includes("GITHUB")) {
500
+ lines.push("# GitHub OAuth");
501
+ lines.push("GITHUB_CLIENT_ID=your_github_client_id");
502
+ lines.push("GITHUB_CLIENT_SECRET=your_github_client_secret");
503
+ lines.push("");
504
+ }
505
+ if (config.authServices.includes("APPLE")) {
506
+ lines.push("# Apple Sign In");
507
+ lines.push("APPLE_CLIENT_ID=your_apple_client_id");
508
+ lines.push("APPLE_CLIENT_SECRET=your_apple_client_secret");
509
+ lines.push("APPLE_TEAM_ID=your_apple_team_id");
510
+ lines.push("APPLE_KEY_ID=your_apple_key_id");
511
+ lines.push("");
512
+ }
513
+ lines.push("#-----------------------------------");
514
+ lines.push("# CORS");
515
+ lines.push("#-----------------------------------");
516
+ lines.push('AUTH_CORS_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:8056"');
517
+ lines.push("AUTH_CORS_ALLOW_ANY_PORT=true");
518
+ lines.push("AUTH_CORS_CREDENTIALS=true");
519
+ lines.push("");
520
+ lines.push("#-----------------------------------");
521
+ lines.push("# Cookies");
522
+ lines.push("#-----------------------------------");
523
+ lines.push("AUTH_COOKIE_HTTP_ONLY=true");
524
+ lines.push("AUTH_COOKIE_SECURE=false");
525
+ lines.push("AUTH_COOKIE_SAME_SITE=lax");
526
+ lines.push("AUTH_COOKIE_PATH=/");
527
+ lines.push("");
528
+ if (config.mailEnabled) {
529
+ lines.push("#-----------------------------------");
530
+ lines.push("# Mail");
531
+ lines.push("#-----------------------------------");
532
+ lines.push('MAIL_SENDERS_ENABLED="SMTP"');
533
+ lines.push('MAIL_DEFAULT_SENDER="SMTP"');
534
+ lines.push("SEND_WELCOME_EMAIL=true");
535
+ lines.push("");
536
+ lines.push("# SMTP Configuration");
537
+ lines.push("SMTP_SMTP_HOST=smtp.example.com");
538
+ lines.push("SMTP_SMTP_PORT=587");
539
+ lines.push("SMTP_SMTP_SECURE=false");
540
+ lines.push("SMTP_SMTP_USER=your_smtp_user");
541
+ lines.push("SMTP_SMTP_PASS=your_smtp_password");
542
+ lines.push('SMTP_FROM_ADDRESS="Your App" <noreply@example.com>');
543
+ lines.push("");
544
+ }
545
+ lines.push("#-----------------------------------");
546
+ lines.push("# OpenAPI Documentation");
547
+ lines.push("#-----------------------------------");
548
+ lines.push(`OPENAPI_ENABLED=${config.openApiEnabled}`);
549
+ if (config.openApiEnabled) {
550
+ lines.push("OPENAPI_INCLUDE_AUTH=true");
551
+ lines.push("OPENAPI_INCLUDE_SCHEMA=true");
552
+ lines.push("OPENAPI_INCLUDE_PERMISSIONS=true");
553
+ }
554
+ lines.push("");
555
+ return lines.join("\n");
556
+ }
557
+ function generateEnvExample(config) {
558
+ const lines = [];
559
+ lines.push("# Database (PostgreSQL 14+ required)");
560
+ lines.push('DATABASE_URL="postgresql://username:password@localhost:5432/baasix"');
561
+ lines.push("");
562
+ lines.push("# Server");
563
+ lines.push("PORT=8056");
564
+ lines.push("NODE_ENV=development");
565
+ lines.push("");
566
+ lines.push("# Security (REQUIRED - generate unique keys)");
567
+ lines.push("SECRET_KEY=your-secret-key-minimum-32-characters-long");
568
+ lines.push("");
569
+ lines.push("# Features");
570
+ lines.push(`MULTI_TENANT=${config.multiTenant}`);
571
+ lines.push(`PUBLIC_REGISTRATION=${config.publicRegistration}`);
572
+ lines.push(`SOCKET_ENABLED=${config.socketEnabled}`);
573
+ lines.push("");
574
+ lines.push("# Storage");
575
+ lines.push(`STORAGE_DEFAULT_SERVICE="${config.storageDriver}"`);
576
+ if (config.storageDriver === "LOCAL") {
577
+ lines.push('LOCAL_STORAGE_PATH="./uploads"');
578
+ } else {
579
+ lines.push("S3_STORAGE_ENDPOINT=your-s3-endpoint");
580
+ lines.push("S3_STORAGE_BUCKET=your-bucket-name");
581
+ lines.push("S3_STORAGE_ACCESS_KEY_ID=your-access-key");
582
+ lines.push("S3_STORAGE_SECRET_ACCESS_KEY=your-secret-key");
583
+ }
584
+ lines.push("");
585
+ lines.push("# Cache");
586
+ lines.push(`CACHE_ADAPTER=${config.cacheAdapter}`);
587
+ if (config.cacheAdapter === "redis") {
588
+ lines.push("CACHE_REDIS_URL=redis://localhost:6379");
589
+ }
590
+ lines.push("");
591
+ lines.push("# Auth");
592
+ lines.push(`AUTH_SERVICES_ENABLED=${config.authServices.join(",")}`);
593
+ lines.push("");
594
+ return lines.join("\n");
595
+ }
596
+ function generateReadme(config) {
597
+ return `# ${config.projectName}
598
+
599
+ A Baasix Backend-as-a-Service project.
600
+
601
+ ## Configuration
602
+
603
+ | Feature | Status |
604
+ |---------|--------|
605
+ | Multi-tenancy | ${config.multiTenant ? "\u2705 Enabled" : "\u274C Disabled"} |
606
+ | Public Registration | ${config.publicRegistration ? "\u2705 Enabled" : "\u274C Disabled"} |
607
+ | Real-time (WebSocket) | ${config.socketEnabled ? "\u2705 Enabled" : "\u274C Disabled"} |
608
+ | Storage | ${config.storageDriver} |
609
+ | Cache | ${config.cacheAdapter} |
610
+ | Auth Methods | ${config.authServices.join(", ")} |
611
+ | OpenAPI Docs | ${config.openApiEnabled ? "\u2705 Enabled" : "\u274C Disabled"} |
612
+
613
+ ## Getting Started
614
+
615
+ 1. **Configure your database**
616
+
617
+ Edit \`.env\` and verify your PostgreSQL connection:
618
+ \`\`\`
619
+ DATABASE_URL="postgresql://username:password@localhost:5432/baasix"
620
+ \`\`\`
621
+
622
+ 2. **Start the server**
623
+
624
+ \`\`\`bash
625
+ npm run dev
626
+ \`\`\`
627
+
628
+ 3. **Access the API**
629
+
630
+ - API: http://localhost:8056
631
+ - ${config.openApiEnabled ? "Swagger UI: http://localhost:8056/documentation" : "OpenAPI: Disabled"}
632
+ - Default admin: admin@baasix.com / admin@123
633
+
634
+ ## Project Structure
635
+
636
+ \`\`\`
637
+ ${config.projectName}/
638
+ \u251C\u2500\u2500 .env # Environment configuration
639
+ \u251C\u2500\u2500 .env.example # Example configuration
640
+ \u251C\u2500\u2500 package.json
641
+ \u251C\u2500\u2500 server.js # Server entry point
642
+ \u251C\u2500\u2500 extensions/ # Custom hooks and endpoints
643
+ \u251C\u2500\u2500 migrations/ # Database migrations
644
+ ${config.storageDriver === "LOCAL" ? "\u2514\u2500\u2500 uploads/ # Local file storage" : ""}
645
+ \`\`\`
646
+
647
+ ## Extensions
648
+
649
+ Place your custom hooks and endpoints in the \`extensions/\` directory:
650
+
651
+ - **Endpoint extensions**: Add custom API routes
652
+ - **Hook extensions**: Add lifecycle hooks (before/after CRUD)
653
+
654
+ See [Extensions Documentation](https://baasix.com/docs/extensions) for details.
655
+
656
+ ## Migrations
657
+
658
+ \`\`\`bash
659
+ # Create a migration
660
+ npx baasix migrate create -n create_products_table
661
+
662
+ # Run migrations
663
+ npx baasix migrate run
664
+
665
+ # Check status
666
+ npx baasix migrate status
667
+ \`\`\`
668
+
669
+ ## Documentation
670
+
671
+ - [Baasix Documentation](https://baasix.com/docs)
672
+ - [SDK Guide](https://baasix.com/docs/sdk-guide)
673
+ - [API Reference](https://baasix.com/docs/api-reference)
674
+ `;
675
+ }
676
+ function generateNextJsEnvContent(config) {
677
+ const lines = [];
678
+ lines.push("#-----------------------------------");
679
+ lines.push("# Baasix API Connection");
680
+ lines.push("#-----------------------------------");
681
+ lines.push("# URL of your Baasix API server");
682
+ lines.push("NEXT_PUBLIC_BAASIX_URL=http://localhost:8056");
683
+ lines.push("");
684
+ lines.push("# Note: Create a separate Baasix API project using:");
685
+ lines.push("# npx @tspvivek/baasix-cli init --template api");
686
+ lines.push("");
687
+ return lines.join("\n");
688
+ }
689
+ async function createNextJsProject(projectPath, config, useAppRouter) {
690
+ const packageJson = {
691
+ name: config.projectName,
692
+ version: "0.1.0",
693
+ private: true,
694
+ scripts: {
695
+ dev: "next dev",
696
+ build: "next build",
697
+ start: "next start",
698
+ lint: "next lint"
699
+ },
700
+ dependencies: {
701
+ "@tspvivek/baasix-sdk": "latest",
702
+ next: "^14.0.0",
703
+ react: "^18.2.0",
704
+ "react-dom": "^18.2.0"
705
+ },
706
+ devDependencies: {
707
+ "@types/node": "^20.0.0",
708
+ "@types/react": "^18.2.0",
709
+ "@types/react-dom": "^18.2.0",
710
+ typescript: "^5.0.0"
711
+ }
712
+ };
713
+ await fs.writeFile(
714
+ path2.join(projectPath, "package.json"),
715
+ JSON.stringify(packageJson, null, 2)
716
+ );
717
+ const envContent = generateNextJsEnvContent(config);
718
+ await fs.writeFile(path2.join(projectPath, ".env.local"), envContent);
719
+ const tsconfig = {
720
+ compilerOptions: {
721
+ target: "es5",
722
+ lib: ["dom", "dom.iterable", "esnext"],
723
+ allowJs: true,
724
+ skipLibCheck: true,
725
+ strict: true,
726
+ noEmit: true,
727
+ esModuleInterop: true,
728
+ module: "esnext",
729
+ moduleResolution: "bundler",
730
+ resolveJsonModule: true,
731
+ isolatedModules: true,
732
+ jsx: "preserve",
733
+ incremental: true,
734
+ plugins: [{ name: "next" }],
735
+ paths: {
736
+ "@/*": [useAppRouter ? "./src/*" : "./*"]
737
+ }
738
+ },
739
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
740
+ exclude: ["node_modules"]
741
+ };
742
+ await fs.writeFile(
743
+ path2.join(projectPath, "tsconfig.json"),
744
+ JSON.stringify(tsconfig, null, 2)
745
+ );
746
+ const nextConfig = `/** @type {import('next').NextConfig} */
747
+ const nextConfig = {
748
+ reactStrictMode: true,
749
+ };
750
+
751
+ export default nextConfig;
752
+ `;
753
+ await fs.writeFile(path2.join(projectPath, "next.config.mjs"), nextConfig);
754
+ if (useAppRouter) {
755
+ await fs.mkdir(path2.join(projectPath, "src", "app"), { recursive: true });
756
+ await fs.mkdir(path2.join(projectPath, "src", "lib"), { recursive: true });
757
+ const baasixClient = `import { createBaasix } from "@tspvivek/baasix-sdk";
758
+
759
+ export const baasix = createBaasix({
760
+ url: process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056",
761
+ authMode: "jwt",
762
+ autoRefresh: true,
763
+ });
764
+
765
+ // Re-export for convenience
766
+ export type { User, Role, QueryParams, Filter } from "@tspvivek/baasix-sdk";
767
+ `;
768
+ await fs.writeFile(path2.join(projectPath, "src", "lib", "baasix.ts"), baasixClient);
769
+ const layout = `import type { Metadata } from "next";
770
+ import "./globals.css";
771
+
772
+ export const metadata: Metadata = {
773
+ title: "${config.projectName}",
774
+ description: "Built with Baasix",
775
+ };
776
+
777
+ export default function RootLayout({
778
+ children,
779
+ }: {
780
+ children: React.ReactNode;
781
+ }) {
782
+ return (
783
+ <html lang="en">
784
+ <body>{children}</body>
785
+ </html>
786
+ );
787
+ }
788
+ `;
789
+ await fs.writeFile(path2.join(projectPath, "src", "app", "layout.tsx"), layout);
790
+ const globalsCss = `* {
791
+ box-sizing: border-box;
792
+ padding: 0;
793
+ margin: 0;
794
+ }
795
+
796
+ html,
797
+ body {
798
+ max-width: 100vw;
799
+ overflow-x: hidden;
800
+ font-family: system-ui, -apple-system, sans-serif;
801
+ }
802
+
803
+ body {
804
+ background: #0a0a0a;
805
+ color: #ededed;
806
+ }
807
+
808
+ a {
809
+ color: #0070f3;
810
+ text-decoration: none;
811
+ }
812
+
813
+ a:hover {
814
+ text-decoration: underline;
815
+ }
816
+ `;
817
+ await fs.writeFile(path2.join(projectPath, "src", "app", "globals.css"), globalsCss);
818
+ const page = `"use client";
819
+
820
+ import { useState, useEffect } from "react";
821
+ import { baasix, type User } from "@/lib/baasix";
822
+
823
+ export default function Home() {
824
+ const [user, setUser] = useState<User | null>(null);
825
+ const [loading, setLoading] = useState(true);
826
+ const [error, setError] = useState<string | null>(null);
827
+
828
+ useEffect(() => {
829
+ baasix.auth.getCachedUser().then((u) => {
830
+ setUser(u);
831
+ setLoading(false);
832
+ }).catch(() => {
833
+ setLoading(false);
834
+ });
835
+ }, []);
836
+
837
+ const handleLogin = async () => {
838
+ setError(null);
839
+ try {
840
+ const { user } = await baasix.auth.login({
841
+ email: "admin@baasix.com",
842
+ password: "admin@123",
843
+ });
844
+ setUser(user);
845
+ } catch (err) {
846
+ setError("Login failed. Make sure your Baasix API server is running.");
847
+ console.error("Login failed:", err);
848
+ }
849
+ };
850
+
851
+ const handleLogout = async () => {
852
+ await baasix.auth.logout();
853
+ setUser(null);
854
+ };
855
+
856
+ if (loading) {
857
+ return (
858
+ <main style={{ padding: "2rem", textAlign: "center" }}>
859
+ <p>Loading...</p>
860
+ </main>
861
+ );
862
+ }
863
+
864
+ return (
865
+ <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
866
+ <h1 style={{ marginBottom: "1rem" }}>\u{1F680} ${config.projectName}</h1>
867
+ <p style={{ marginBottom: "2rem", color: "#888" }}>
868
+ Next.js Frontend with Baasix SDK
869
+ </p>
870
+
871
+ {error && (
872
+ <div style={{ padding: "1rem", background: "#3a1a1a", borderRadius: "8px", marginBottom: "1rem", color: "#ff6b6b" }}>
873
+ {error}
874
+ </div>
875
+ )}
876
+
877
+ {user ? (
878
+ <div>
879
+ <p style={{ marginBottom: "1rem" }}>
880
+ Welcome, <strong>{user.email}</strong>!
881
+ </p>
882
+ <button
883
+ onClick={handleLogout}
884
+ style={{
885
+ padding: "0.5rem 1rem",
886
+ background: "#333",
887
+ color: "#fff",
888
+ border: "none",
889
+ borderRadius: "4px",
890
+ cursor: "pointer",
891
+ }}
892
+ >
893
+ Logout
894
+ </button>
895
+ </div>
896
+ ) : (
897
+ <div>
898
+ <p style={{ marginBottom: "1rem" }}>Not logged in</p>
899
+ <button
900
+ onClick={handleLogin}
901
+ style={{
902
+ padding: "0.5rem 1rem",
903
+ background: "#0070f3",
904
+ color: "#fff",
905
+ border: "none",
906
+ borderRadius: "4px",
907
+ cursor: "pointer",
908
+ }}
909
+ >
910
+ Login as Admin
911
+ </button>
912
+ </div>
913
+ )}
914
+
915
+ <div style={{ marginTop: "3rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
916
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>Getting Started</h2>
917
+ <p style={{ marginBottom: "1rem", color: "#888", fontSize: "0.9rem" }}>
918
+ This is a frontend-only Next.js app. You need a separate Baasix API server.
919
+ </p>
920
+ <ol style={{ paddingLeft: "1.5rem", lineHeight: "1.8" }}>
921
+ <li>Create a Baasix API project: <code>npx @tspvivek/baasix-cli init --template api</code></li>
922
+ <li>Start the API server: <code>cd your-api && npm run dev</code></li>
923
+ <li>Update <code>.env.local</code> with your API URL if needed</li>
924
+ <li>Start this Next.js app: <code>npm run dev</code></li>
925
+ </ol>
926
+ </div>
927
+
928
+ <div style={{ marginTop: "1.5rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
929
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>API Connection</h2>
930
+ <p style={{ color: "#888", fontSize: "0.9rem" }}>
931
+ Currently configured to connect to: <code>{process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056"}</code>
932
+ </p>
933
+ </div>
934
+ </main>
935
+ );
936
+ }
937
+ `;
938
+ await fs.writeFile(path2.join(projectPath, "src", "app", "page.tsx"), page);
939
+ } else {
940
+ await fs.mkdir(path2.join(projectPath, "pages"), { recursive: true });
941
+ await fs.mkdir(path2.join(projectPath, "lib"), { recursive: true });
942
+ await fs.mkdir(path2.join(projectPath, "styles"), { recursive: true });
943
+ const baasixClient = `import { createBaasix } from "@tspvivek/baasix-sdk";
944
+
945
+ export const baasix = createBaasix({
946
+ url: process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056",
947
+ authMode: "jwt",
948
+ autoRefresh: true,
949
+ });
950
+
951
+ export type { User, Role, QueryParams, Filter } from "@tspvivek/baasix-sdk";
952
+ `;
953
+ await fs.writeFile(path2.join(projectPath, "lib", "baasix.ts"), baasixClient);
954
+ const app = `import type { AppProps } from "next/app";
955
+ import "@/styles/globals.css";
956
+
957
+ export default function App({ Component, pageProps }: AppProps) {
958
+ return <Component {...pageProps} />;
959
+ }
960
+ `;
961
+ await fs.writeFile(path2.join(projectPath, "pages", "_app.tsx"), app);
962
+ const globalsCss = `* {
963
+ box-sizing: border-box;
964
+ padding: 0;
965
+ margin: 0;
966
+ }
967
+
968
+ html,
969
+ body {
970
+ max-width: 100vw;
971
+ overflow-x: hidden;
972
+ font-family: system-ui, -apple-system, sans-serif;
973
+ }
974
+
975
+ body {
976
+ background: #0a0a0a;
977
+ color: #ededed;
978
+ }
979
+
980
+ a {
981
+ color: #0070f3;
982
+ text-decoration: none;
983
+ }
984
+
985
+ a:hover {
986
+ text-decoration: underline;
987
+ }
988
+ `;
989
+ await fs.writeFile(path2.join(projectPath, "styles", "globals.css"), globalsCss);
990
+ const index = `import { useState, useEffect } from "react";
991
+ import { baasix, type User } from "@/lib/baasix";
992
+
993
+ export default function Home() {
994
+ const [user, setUser] = useState<User | null>(null);
995
+ const [loading, setLoading] = useState(true);
996
+ const [error, setError] = useState<string | null>(null);
997
+
998
+ useEffect(() => {
999
+ baasix.auth.getCachedUser().then((u) => {
1000
+ setUser(u);
1001
+ setLoading(false);
1002
+ }).catch(() => {
1003
+ setLoading(false);
1004
+ });
1005
+ }, []);
1006
+
1007
+ const handleLogin = async () => {
1008
+ setError(null);
1009
+ try {
1010
+ const { user } = await baasix.auth.login({
1011
+ email: "admin@baasix.com",
1012
+ password: "admin@123",
1013
+ });
1014
+ setUser(user);
1015
+ } catch (err) {
1016
+ setError("Login failed. Make sure your Baasix API server is running.");
1017
+ console.error("Login failed:", err);
1018
+ }
1019
+ };
1020
+
1021
+ const handleLogout = async () => {
1022
+ await baasix.auth.logout();
1023
+ setUser(null);
1024
+ };
1025
+
1026
+ if (loading) {
1027
+ return (
1028
+ <main style={{ padding: "2rem", textAlign: "center" }}>
1029
+ <p>Loading...</p>
1030
+ </main>
1031
+ );
1032
+ }
1033
+
1034
+ return (
1035
+ <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
1036
+ <h1 style={{ marginBottom: "1rem" }}>\u{1F680} ${config.projectName}</h1>
1037
+ <p style={{ marginBottom: "2rem", color: "#888" }}>
1038
+ Next.js Frontend with Baasix SDK
1039
+ </p>
1040
+
1041
+ {error && (
1042
+ <div style={{ padding: "1rem", background: "#3a1a1a", borderRadius: "8px", marginBottom: "1rem", color: "#ff6b6b" }}>
1043
+ {error}
1044
+ </div>
1045
+ )}
1046
+
1047
+ {user ? (
1048
+ <div>
1049
+ <p style={{ marginBottom: "1rem" }}>
1050
+ Welcome, <strong>{user.email}</strong>!
1051
+ </p>
1052
+ <button
1053
+ onClick={handleLogout}
1054
+ style={{
1055
+ padding: "0.5rem 1rem",
1056
+ background: "#333",
1057
+ color: "#fff",
1058
+ border: "none",
1059
+ borderRadius: "4px",
1060
+ cursor: "pointer",
1061
+ }}
1062
+ >
1063
+ Logout
1064
+ </button>
1065
+ </div>
1066
+ ) : (
1067
+ <div>
1068
+ <p style={{ marginBottom: "1rem" }}>Not logged in</p>
1069
+ <button
1070
+ onClick={handleLogin}
1071
+ style={{
1072
+ padding: "0.5rem 1rem",
1073
+ background: "#0070f3",
1074
+ color: "#fff",
1075
+ border: "none",
1076
+ borderRadius: "4px",
1077
+ cursor: "pointer",
1078
+ }}
1079
+ >
1080
+ Login as Admin
1081
+ </button>
1082
+ </div>
1083
+ )}
1084
+
1085
+ <div style={{ marginTop: "3rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1086
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>Getting Started</h2>
1087
+ <p style={{ marginBottom: "1rem", color: "#888", fontSize: "0.9rem" }}>
1088
+ This is a frontend-only Next.js app. You need a separate Baasix API server.
1089
+ </p>
1090
+ <ol style={{ paddingLeft: "1.5rem", lineHeight: "1.8" }}>
1091
+ <li>Create a Baasix API project: <code>npx @tspvivek/baasix-cli init --template api</code></li>
1092
+ <li>Start the API server: <code>cd your-api && npm run dev</code></li>
1093
+ <li>Update <code>.env.local</code> with your API URL if needed</li>
1094
+ <li>Start this Next.js app: <code>npm run dev</code></li>
1095
+ </ol>
1096
+ </div>
1097
+
1098
+ <div style={{ marginTop: "1.5rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1099
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>API Connection</h2>
1100
+ <p style={{ color: "#888", fontSize: "0.9rem" }}>
1101
+ Currently configured to connect to: <code>{process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056"}</code>
1102
+ </p>
1103
+ </div>
1104
+ </main>
1105
+ );
1106
+ }
1107
+ `;
1108
+ await fs.writeFile(path2.join(projectPath, "pages", "index.tsx"), index);
1109
+ }
1110
+ const gitignore = `# Dependencies
1111
+ node_modules/
1112
+ .pnp
1113
+ .pnp.js
1114
+
1115
+ # Testing
1116
+ coverage/
1117
+
1118
+ # Next.js
1119
+ .next/
1120
+ out/
1121
+ build/
1122
+
1123
+ # Environment
1124
+ .env
1125
+ .env.local
1126
+ .env.development.local
1127
+ .env.test.local
1128
+ .env.production.local
1129
+
1130
+ # Misc
1131
+ .DS_Store
1132
+ *.pem
1133
+ npm-debug.log*
1134
+ yarn-debug.log*
1135
+ yarn-error.log*
1136
+
1137
+ # Vercel
1138
+ .vercel
1139
+
1140
+ # TypeScript
1141
+ *.tsbuildinfo
1142
+ next-env.d.ts
1143
+ `;
1144
+ await fs.writeFile(path2.join(projectPath, ".gitignore"), gitignore);
1145
+ const readme = `# ${config.projectName}
1146
+
1147
+ A Next.js frontend project that connects to a Baasix API server using the SDK.
1148
+
1149
+ ## Architecture
1150
+
1151
+ This is a **frontend-only** project. You need a separate Baasix API server running.
1152
+
1153
+ \`\`\`
1154
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 HTTP/WS \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1155
+ \u2502 Next.js App \u2502 \u25C4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25BA \u2502 Baasix API \u2502
1156
+ \u2502 (Frontend) \u2502 via SDK \u2502 (Backend) \u2502
1157
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
1158
+ Port 3000 Port 8056
1159
+ \`\`\`
1160
+
1161
+ ## Getting Started
1162
+
1163
+ ### 1. Start your Baasix API Server
1164
+
1165
+ If you don't have a Baasix API project yet, create one:
1166
+
1167
+ \`\`\`bash
1168
+ npx @tspvivek/baasix-cli init --template api my-api
1169
+ cd my-api
1170
+ npm install
1171
+ npm run dev
1172
+ \`\`\`
1173
+
1174
+ ### 2. Configure this Frontend
1175
+
1176
+ Update \`.env.local\` if your API is running on a different URL:
1177
+
1178
+ \`\`\`
1179
+ NEXT_PUBLIC_BAASIX_URL=http://localhost:8056
1180
+ \`\`\`
1181
+
1182
+ ### 3. Start the Frontend
1183
+
1184
+ \`\`\`bash
1185
+ npm install
1186
+ npm run dev
1187
+ \`\`\`
1188
+
1189
+ ### 4. Open your browser
1190
+
1191
+ - Frontend: http://localhost:3000
1192
+
1193
+ ## Default Admin Credentials
1194
+
1195
+ Use these credentials to login (configured in your API server):
1196
+
1197
+ - Email: admin@baasix.com
1198
+ - Password: admin@123
1199
+
1200
+ ## Project Structure
1201
+
1202
+ \`\`\`
1203
+ ${config.projectName}/
1204
+ \u251C\u2500\u2500 .env.local # API URL configuration
1205
+ \u251C\u2500\u2500 package.json
1206
+ ${useAppRouter ? `\u251C\u2500\u2500 src/
1207
+ \u2502 \u251C\u2500\u2500 app/ # Next.js App Router pages
1208
+ \u2502 \u2502 \u251C\u2500\u2500 layout.tsx
1209
+ \u2502 \u2502 \u251C\u2500\u2500 page.tsx
1210
+ \u2502 \u2502 \u2514\u2500\u2500 globals.css
1211
+ \u2502 \u2514\u2500\u2500 lib/
1212
+ \u2502 \u2514\u2500\u2500 baasix.ts # SDK client` : `\u251C\u2500\u2500 pages/ # Next.js Pages Router
1213
+ \u2502 \u251C\u2500\u2500 _app.tsx
1214
+ \u2502 \u2514\u2500\u2500 index.tsx
1215
+ \u251C\u2500\u2500 lib/
1216
+ \u2502 \u2514\u2500\u2500 baasix.ts # SDK client
1217
+ \u2514\u2500\u2500 styles/
1218
+ \u2514\u2500\u2500 globals.css`}
1219
+ \`\`\`
1220
+
1221
+ ## SDK Usage
1222
+
1223
+ The SDK is pre-configured in \`${useAppRouter ? "src/lib/baasix.ts" : "lib/baasix.ts"}\`:
1224
+
1225
+ \`\`\`typescript
1226
+ import { baasix } from "${useAppRouter ? "@/lib/baasix" : "@/lib/baasix"}";
1227
+
1228
+ // Authentication
1229
+ const { user } = await baasix.auth.login({ email, password });
1230
+ await baasix.auth.logout();
1231
+
1232
+ // CRUD operations
1233
+ const items = await baasix.items("posts").list();
1234
+ const item = await baasix.items("posts").create({ title: "Hello" });
1235
+ await baasix.items("posts").update(id, { title: "Updated" });
1236
+ await baasix.items("posts").delete(id);
1237
+ \`\`\`
1238
+
1239
+ ## Documentation
1240
+
1241
+ - [Baasix Documentation](https://baasix.com/docs)
1242
+ - [SDK Guide](https://baasix.com/docs/sdk-guide)
1243
+ - [Next.js Documentation](https://nextjs.org/docs)
1244
+ `;
1245
+ await fs.writeFile(path2.join(projectPath, "README.md"), readme);
1246
+ }
1247
+ var init = new Command("init").description("Initialize a new Baasix project").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-t, --template <template>", "Project template (api, nextjs, nextjs-app)").option("-n, --name <name>", "Project name").option("-y, --yes", "Skip confirmation prompts").action(initAction);
1248
+
1249
+ // src/commands/generate.ts
1250
+ import { existsSync as existsSync4 } from "fs";
1251
+ import fs3 from "fs/promises";
1252
+ import path4 from "path";
1253
+ import {
1254
+ cancel as cancel2,
1255
+ confirm as confirm2,
1256
+ intro as intro2,
1257
+ isCancel as isCancel2,
1258
+ log as log2,
1259
+ outro as outro2,
1260
+ select as select2,
1261
+ spinner as spinner2,
1262
+ text as text2
1263
+ } from "@clack/prompts";
1264
+ import chalk2 from "chalk";
1265
+ import { Command as Command2 } from "commander";
1266
+ import { format as prettierFormat } from "prettier";
1267
+
1268
+ // src/utils/get-config.ts
1269
+ import { existsSync as existsSync3 } from "fs";
1270
+ import fs2 from "fs/promises";
1271
+ import path3 from "path";
1272
+ import { parse } from "dotenv";
1273
+ async function getConfig(cwd) {
1274
+ const envPath = path3.join(cwd, ".env");
1275
+ let envVars = {};
1276
+ if (existsSync3(envPath)) {
1277
+ const envContent = await fs2.readFile(envPath, "utf-8");
1278
+ envVars = parse(envContent);
1279
+ }
1280
+ const mergedEnv = { ...envVars, ...process.env };
1281
+ const url = mergedEnv.BAASIX_URL || mergedEnv.API_URL || "http://localhost:8056";
1282
+ const email = mergedEnv.BAASIX_EMAIL || mergedEnv.ADMIN_EMAIL;
1283
+ const password = mergedEnv.BAASIX_PASSWORD || mergedEnv.ADMIN_PASSWORD;
1284
+ const token = mergedEnv.BAASIX_TOKEN || mergedEnv.BAASIX_AUTH_TOKEN;
1285
+ if (!url) {
1286
+ return null;
1287
+ }
1288
+ return {
1289
+ url,
1290
+ email,
1291
+ password,
1292
+ token
1293
+ };
1294
+ }
1295
+
1296
+ // src/utils/api-client.ts
1297
+ import axios from "axios";
1298
+ var client = null;
1299
+ var authToken = null;
1300
+ async function createApiClient(config) {
1301
+ if (client) {
1302
+ return client;
1303
+ }
1304
+ client = axios.create({
1305
+ baseURL: config.url,
1306
+ timeout: 3e4,
1307
+ headers: {
1308
+ "Content-Type": "application/json"
1309
+ }
1310
+ });
1311
+ if (config.token) {
1312
+ authToken = config.token;
1313
+ client.defaults.headers.common["Authorization"] = `Bearer ${authToken}`;
1314
+ } else if (config.email && config.password) {
1315
+ try {
1316
+ const response = await client.post("/auth/login", {
1317
+ email: config.email,
1318
+ password: config.password
1319
+ });
1320
+ authToken = response.data.token;
1321
+ client.defaults.headers.common["Authorization"] = `Bearer ${authToken}`;
1322
+ } catch (error) {
1323
+ throw new Error(`Failed to authenticate: ${error instanceof Error ? error.message : "Unknown error"}`);
1324
+ }
1325
+ }
1326
+ return client;
1327
+ }
1328
+ async function fetchSchemas(config) {
1329
+ const client2 = await createApiClient(config);
1330
+ const response = await client2.get("/schemas", {
1331
+ params: { limit: -1 }
1332
+ });
1333
+ return response.data.data || [];
1334
+ }
1335
+ async function fetchMigrations(config) {
1336
+ const client2 = await createApiClient(config);
1337
+ const response = await client2.get("/migrations");
1338
+ return response.data.data || [];
1339
+ }
1340
+ async function runMigrations(config, options) {
1341
+ const client2 = await createApiClient(config);
1342
+ const response = await client2.post("/migrations/run", options || {});
1343
+ return response.data;
1344
+ }
1345
+ async function rollbackMigrations(config, options) {
1346
+ const client2 = await createApiClient(config);
1347
+ const response = await client2.post("/migrations/rollback", options || {});
1348
+ return response.data;
1349
+ }
1350
+
1351
+ // src/commands/generate.ts
1352
+ async function generateAction(opts) {
1353
+ const cwd = path4.resolve(opts.cwd);
1354
+ intro2(chalk2.bgBlue.black(" Baasix Type Generator "));
1355
+ const config = await getConfig(cwd);
1356
+ if (!config && !opts.url) {
1357
+ log2.error("No Baasix configuration found. Create a .env file with BAASIX_URL or use --url flag.");
1358
+ process.exit(1);
1359
+ }
1360
+ const baasixUrl = opts.url || config?.url || "http://localhost:8056";
1361
+ let target = opts.target;
1362
+ if (!target) {
1363
+ const result = await select2({
1364
+ message: "What do you want to generate?",
1365
+ options: [
1366
+ {
1367
+ value: "types",
1368
+ label: "TypeScript Types",
1369
+ hint: "Generate types for all collections"
1370
+ },
1371
+ {
1372
+ value: "sdk-types",
1373
+ label: "SDK Collection Types",
1374
+ hint: "Generate typed SDK helpers for collections"
1375
+ },
1376
+ {
1377
+ value: "schema-json",
1378
+ label: "Schema JSON",
1379
+ hint: "Export all schemas as JSON"
1380
+ }
1381
+ ]
1382
+ });
1383
+ if (isCancel2(result)) {
1384
+ cancel2("Operation cancelled");
1385
+ process.exit(0);
1386
+ }
1387
+ target = result;
1388
+ }
1389
+ let outputPath = opts.output;
1390
+ if (!outputPath) {
1391
+ const defaultPath = target === "schema-json" ? "schemas.json" : "baasix.d.ts";
1392
+ const result = await text2({
1393
+ message: "Output file path:",
1394
+ placeholder: defaultPath,
1395
+ defaultValue: defaultPath
1396
+ });
1397
+ if (isCancel2(result)) {
1398
+ cancel2("Operation cancelled");
1399
+ process.exit(0);
1400
+ }
1401
+ outputPath = result;
1402
+ }
1403
+ const s = spinner2();
1404
+ s.start("Fetching schemas from Baasix...");
1405
+ try {
1406
+ const schemas = await fetchSchemas({
1407
+ url: baasixUrl,
1408
+ email: config?.email,
1409
+ password: config?.password,
1410
+ token: config?.token
1411
+ });
1412
+ if (!schemas || schemas.length === 0) {
1413
+ s.stop("No schemas found");
1414
+ log2.warn("No schemas found in your Baasix instance.");
1415
+ process.exit(0);
1416
+ }
1417
+ s.message(`Found ${schemas.length} schemas`);
1418
+ let output;
1419
+ if (target === "types") {
1420
+ output = generateTypeScriptTypes(schemas);
1421
+ } else if (target === "sdk-types") {
1422
+ output = generateSDKTypes(schemas);
1423
+ } else {
1424
+ output = JSON.stringify(schemas, null, 2);
1425
+ }
1426
+ if (target !== "schema-json") {
1427
+ try {
1428
+ output = await prettierFormat(output, {
1429
+ parser: "typescript",
1430
+ printWidth: 100,
1431
+ tabWidth: 2,
1432
+ singleQuote: true
1433
+ });
1434
+ } catch {
1435
+ }
1436
+ }
1437
+ const fullOutputPath = path4.resolve(cwd, outputPath);
1438
+ if (existsSync4(fullOutputPath) && !opts.yes) {
1439
+ s.stop("File already exists");
1440
+ const overwrite = await confirm2({
1441
+ message: `File ${outputPath} already exists. Overwrite?`,
1442
+ initialValue: true
1443
+ });
1444
+ if (isCancel2(overwrite) || !overwrite) {
1445
+ cancel2("Operation cancelled");
1446
+ process.exit(0);
1447
+ }
1448
+ s.start("Writing file...");
1449
+ }
1450
+ const outputDir = path4.dirname(fullOutputPath);
1451
+ if (!existsSync4(outputDir)) {
1452
+ await fs3.mkdir(outputDir, { recursive: true });
1453
+ }
1454
+ await fs3.writeFile(fullOutputPath, output);
1455
+ s.stop("Types generated successfully");
1456
+ outro2(chalk2.green(`\u2728 Generated ${outputPath}`));
1457
+ if (target === "types" || target === "sdk-types") {
1458
+ console.log();
1459
+ console.log(chalk2.bold("Usage:"));
1460
+ console.log(` ${chalk2.dim("// Import types in your TypeScript files")}`);
1461
+ console.log(` ${chalk2.cyan(`import type { Products, Users } from "./${outputPath.replace(/\.d\.ts$/, "")}";`)}`);
1462
+ console.log();
1463
+ }
1464
+ } catch (error) {
1465
+ s.stop("Failed to generate types");
1466
+ if (error instanceof Error) {
1467
+ log2.error(error.message);
1468
+ } else {
1469
+ log2.error("Unknown error occurred");
1470
+ }
1471
+ process.exit(1);
1472
+ }
1473
+ }
1474
+ function fieldTypeToTS(field, allSchemas) {
1475
+ if (field.relType && field.target) {
1476
+ const targetType = toPascalCase(field.target);
1477
+ const isSystemCollection = field.target.startsWith("baasix_");
1478
+ if (field.relType === "HasMany" || field.relType === "BelongsToMany") {
1479
+ return { type: `${targetType}[] | null` };
1480
+ }
1481
+ return { type: `${targetType} | null` };
1482
+ }
1483
+ const type = field.type?.toUpperCase();
1484
+ const nullable = field.allowNull !== false;
1485
+ const nullSuffix = nullable ? " | null" : "";
1486
+ const jsdocParts = [];
1487
+ if (field.validate) {
1488
+ if (field.validate.min !== void 0) jsdocParts.push(`@min ${field.validate.min}`);
1489
+ if (field.validate.max !== void 0) jsdocParts.push(`@max ${field.validate.max}`);
1490
+ if (field.validate.len) jsdocParts.push(`@length ${field.validate.len[0]}-${field.validate.len[1]}`);
1491
+ if (field.validate.isEmail) jsdocParts.push(`@format email`);
1492
+ if (field.validate.isUrl) jsdocParts.push(`@format url`);
1493
+ if (field.validate.isIP) jsdocParts.push(`@format ip`);
1494
+ if (field.validate.isUUID) jsdocParts.push(`@format uuid`);
1495
+ if (field.validate.regex) jsdocParts.push(`@pattern ${field.validate.regex}`);
1496
+ }
1497
+ if (field.values && typeof field.values === "object" && !Array.isArray(field.values)) {
1498
+ const vals = field.values;
1499
+ if (vals.length) jsdocParts.push(`@maxLength ${vals.length}`);
1500
+ if (vals.precision && vals.scale) jsdocParts.push(`@precision ${vals.precision},${vals.scale}`);
1501
+ }
1502
+ const jsdoc = jsdocParts.length > 0 ? jsdocParts.join(" ") : void 0;
1503
+ switch (type) {
1504
+ case "STRING":
1505
+ case "TEXT":
1506
+ case "UUID":
1507
+ case "SUID":
1508
+ return { type: `string${nullSuffix}`, jsdoc };
1509
+ case "INTEGER":
1510
+ case "BIGINT":
1511
+ case "FLOAT":
1512
+ case "REAL":
1513
+ case "DOUBLE":
1514
+ case "DECIMAL":
1515
+ return { type: `number${nullSuffix}`, jsdoc };
1516
+ case "BOOLEAN":
1517
+ return { type: `boolean${nullSuffix}`, jsdoc };
1518
+ case "DATE":
1519
+ case "DATETIME":
1520
+ case "TIME":
1521
+ return { type: `string${nullSuffix}`, jsdoc };
1522
+ // ISO date strings
1523
+ case "JSON":
1524
+ case "JSONB":
1525
+ return { type: `Record<string, unknown>${nullSuffix}`, jsdoc };
1526
+ case "ARRAY": {
1527
+ const vals = field.values;
1528
+ const arrayType = vals?.type || "unknown";
1529
+ const innerType = arrayType.toUpperCase() === "STRING" ? "string" : arrayType.toUpperCase() === "INTEGER" ? "number" : arrayType.toUpperCase() === "BOOLEAN" ? "boolean" : "unknown";
1530
+ return { type: `${innerType}[]${nullSuffix}`, jsdoc };
1531
+ }
1532
+ case "ENUM": {
1533
+ let enumValues;
1534
+ if (Array.isArray(field.values)) {
1535
+ enumValues = field.values;
1536
+ } else if (field.values && typeof field.values === "object") {
1537
+ const vals = field.values;
1538
+ if (Array.isArray(vals.values)) {
1539
+ enumValues = vals.values;
1540
+ }
1541
+ }
1542
+ if (enumValues && enumValues.length > 0) {
1543
+ const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
1544
+ return { type: `(${enumType})${nullSuffix}`, jsdoc };
1545
+ }
1546
+ return { type: `string${nullSuffix}`, jsdoc };
1547
+ }
1548
+ case "GEOMETRY":
1549
+ case "POINT":
1550
+ case "LINESTRING":
1551
+ case "POLYGON":
1552
+ return { type: `GeoJSON.Geometry${nullSuffix}`, jsdoc };
1553
+ default:
1554
+ return { type: `unknown${nullSuffix}`, jsdoc };
1555
+ }
1556
+ }
1557
+ function toPascalCase(str) {
1558
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1559
+ }
1560
+ function generateTypeScriptTypes(schemas) {
1561
+ const lines = [
1562
+ "/**",
1563
+ " * Auto-generated TypeScript types for Baasix collections",
1564
+ ` * Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1565
+ " * ",
1566
+ " * Do not edit this file manually. Re-run 'baasix generate types' to update.",
1567
+ " */",
1568
+ "",
1569
+ "// GeoJSON types for PostGIS fields",
1570
+ "declare namespace GeoJSON {",
1571
+ " interface Point { type: 'Point'; coordinates: [number, number]; }",
1572
+ " interface LineString { type: 'LineString'; coordinates: [number, number][]; }",
1573
+ " interface Polygon { type: 'Polygon'; coordinates: [number, number][][]; }",
1574
+ " type Geometry = Point | LineString | Polygon;",
1575
+ "}",
1576
+ ""
1577
+ ];
1578
+ const referencedSystemCollections = /* @__PURE__ */ new Set();
1579
+ for (const schema of schemas) {
1580
+ for (const field of Object.values(schema.schema.fields)) {
1581
+ const fieldDef = field;
1582
+ if (fieldDef.relType && fieldDef.target && fieldDef.target.startsWith("baasix_")) {
1583
+ referencedSystemCollections.add(fieldDef.target);
1584
+ }
1585
+ }
1586
+ }
1587
+ const systemSchemas = schemas.filter(
1588
+ (s) => referencedSystemCollections.has(s.collectionName)
1589
+ );
1590
+ for (const schema of systemSchemas) {
1591
+ const typeName = toPascalCase(schema.collectionName);
1592
+ const fields = schema.schema.fields;
1593
+ lines.push(`/**`);
1594
+ lines.push(` * ${schema.schema.name || schema.collectionName} (system collection)`);
1595
+ lines.push(` */`);
1596
+ lines.push(`export interface ${typeName} {`);
1597
+ for (const [fieldName, field] of Object.entries(fields)) {
1598
+ const fieldDef = field;
1599
+ if (fieldDef.relType) continue;
1600
+ const { type: tsType, jsdoc } = fieldTypeToTS(fieldDef, schemas);
1601
+ const optional = fieldDef.allowNull !== false && !fieldDef.primaryKey ? "?" : "";
1602
+ if (jsdoc) {
1603
+ lines.push(` /** ${jsdoc} */`);
1604
+ }
1605
+ lines.push(` ${fieldName}${optional}: ${tsType};`);
1606
+ }
1607
+ lines.push(`}`);
1608
+ lines.push("");
1609
+ }
1610
+ const userSchemas = schemas.filter(
1611
+ (s) => !s.collectionName.startsWith("baasix_")
1612
+ );
1613
+ for (const schema of userSchemas) {
1614
+ const typeName = toPascalCase(schema.collectionName);
1615
+ const fields = schema.schema.fields;
1616
+ lines.push(`/**`);
1617
+ lines.push(` * ${schema.schema.name || schema.collectionName} collection`);
1618
+ lines.push(` */`);
1619
+ lines.push(`export interface ${typeName} {`);
1620
+ for (const [fieldName, field] of Object.entries(fields)) {
1621
+ const fieldDef = field;
1622
+ const { type: tsType, jsdoc } = fieldTypeToTS(fieldDef, schemas);
1623
+ const optional = fieldDef.allowNull !== false && !fieldDef.primaryKey ? "?" : "";
1624
+ if (jsdoc) {
1625
+ lines.push(` /** ${jsdoc} */`);
1626
+ }
1627
+ lines.push(` ${fieldName}${optional}: ${tsType};`);
1628
+ }
1629
+ if (schema.schema.timestamps) {
1630
+ lines.push(` createdAt?: string;`);
1631
+ lines.push(` updatedAt?: string;`);
1632
+ }
1633
+ if (schema.schema.paranoid) {
1634
+ lines.push(` deletedAt?: string | null;`);
1635
+ }
1636
+ lines.push(`}`);
1637
+ lines.push("");
1638
+ }
1639
+ lines.push("/**");
1640
+ lines.push(" * All collection names");
1641
+ lines.push(" */");
1642
+ lines.push("export type CollectionName =");
1643
+ for (const schema of userSchemas) {
1644
+ lines.push(` | "${schema.collectionName}"`);
1645
+ }
1646
+ lines.push(";");
1647
+ lines.push("");
1648
+ lines.push("/**");
1649
+ lines.push(" * Map collection names to their types");
1650
+ lines.push(" */");
1651
+ lines.push("export interface CollectionTypeMap {");
1652
+ for (const schema of userSchemas) {
1653
+ const typeName = toPascalCase(schema.collectionName);
1654
+ lines.push(` ${schema.collectionName}: ${typeName};`);
1655
+ }
1656
+ lines.push("}");
1657
+ lines.push("");
1658
+ return lines.join("\n");
1659
+ }
1660
+ function generateSDKTypes(schemas) {
1661
+ const lines = [
1662
+ "/**",
1663
+ " * Auto-generated typed SDK helpers for Baasix collections",
1664
+ ` * Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1665
+ " * ",
1666
+ " * Do not edit this file manually. Re-run 'baasix generate sdk-types' to update.",
1667
+ " */",
1668
+ "",
1669
+ 'import { createBaasix } from "@tspvivek/baasix-sdk";',
1670
+ 'import type { QueryParams, Filter, PaginatedResponse } from "@tspvivek/baasix-sdk";',
1671
+ ""
1672
+ ];
1673
+ lines.push(generateTypeScriptTypes(schemas));
1674
+ lines.push("/**");
1675
+ lines.push(" * Create a typed Baasix client with collection-specific methods");
1676
+ lines.push(" */");
1677
+ lines.push("export function createTypedBaasix(config: Parameters<typeof createBaasix>[0]) {");
1678
+ lines.push(" const client = createBaasix(config);");
1679
+ lines.push("");
1680
+ lines.push(" return {");
1681
+ lines.push(" ...client,");
1682
+ lines.push(" /**");
1683
+ lines.push(" * Type-safe items access");
1684
+ lines.push(" */");
1685
+ lines.push(" collections: {");
1686
+ const userSchemas = schemas.filter((s) => !s.collectionName.startsWith("baasix_"));
1687
+ for (const schema of userSchemas) {
1688
+ const typeName = toPascalCase(schema.collectionName);
1689
+ lines.push(` ${schema.collectionName}: client.items<${typeName}>("${schema.collectionName}"),`);
1690
+ }
1691
+ lines.push(" },");
1692
+ lines.push(" };");
1693
+ lines.push("}");
1694
+ lines.push("");
1695
+ return lines.join("\n");
1696
+ }
1697
+ var generate = new Command2("generate").alias("gen").description("Generate TypeScript types from Baasix schemas").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-o, --output <path>", "Output file path").option("-t, --target <target>", "Generation target (types, sdk-types, schema-json)").option("--url <url>", "Baasix server URL").option("-y, --yes", "Skip confirmation prompts").action(generateAction);
1698
+
1699
+ // src/commands/extension.ts
1700
+ import { existsSync as existsSync5 } from "fs";
1701
+ import fs4 from "fs/promises";
1702
+ import path5 from "path";
1703
+ import {
1704
+ cancel as cancel3,
1705
+ confirm as confirm3,
1706
+ intro as intro3,
1707
+ isCancel as isCancel3,
1708
+ log as log3,
1709
+ outro as outro3,
1710
+ select as select3,
1711
+ spinner as spinner3,
1712
+ text as text3
1713
+ } from "@clack/prompts";
1714
+ import chalk3 from "chalk";
1715
+ import { Command as Command3 } from "commander";
1716
+ async function extensionAction(opts) {
1717
+ const cwd = path5.resolve(opts.cwd);
1718
+ intro3(chalk3.bgMagenta.black(" Baasix Extension Generator "));
1719
+ let extensionType = opts.type;
1720
+ if (!extensionType) {
1721
+ const result = await select3({
1722
+ message: "What type of extension do you want to create?",
1723
+ options: [
1724
+ {
1725
+ value: "hook",
1726
+ label: "Hook",
1727
+ hint: "Intercept and modify CRUD operations"
1728
+ },
1729
+ {
1730
+ value: "endpoint",
1731
+ label: "Custom Endpoint",
1732
+ hint: "Add new API routes"
1733
+ }
1734
+ ]
1735
+ });
1736
+ if (isCancel3(result)) {
1737
+ cancel3("Operation cancelled");
1738
+ process.exit(0);
1739
+ }
1740
+ extensionType = result;
1741
+ }
1742
+ let extensionName = opts.name;
1743
+ if (!extensionName) {
1744
+ const result = await text3({
1745
+ message: "What is your extension name?",
1746
+ placeholder: extensionType === "hook" ? "my-hook" : "my-endpoint",
1747
+ validate: (value) => {
1748
+ if (!value) return "Extension name is required";
1749
+ if (!/^[a-z0-9-_]+$/i.test(value)) return "Name must be alphanumeric with dashes or underscores";
1750
+ return void 0;
1751
+ }
1752
+ });
1753
+ if (isCancel3(result)) {
1754
+ cancel3("Operation cancelled");
1755
+ process.exit(0);
1756
+ }
1757
+ extensionName = result;
1758
+ }
1759
+ let collectionName = opts.collection;
1760
+ if (extensionType === "hook" && !collectionName) {
1761
+ const result = await text3({
1762
+ message: "Which collection should this hook apply to?",
1763
+ placeholder: "posts",
1764
+ validate: (value) => {
1765
+ if (!value) return "Collection name is required";
1766
+ return void 0;
1767
+ }
1768
+ });
1769
+ if (isCancel3(result)) {
1770
+ cancel3("Operation cancelled");
1771
+ process.exit(0);
1772
+ }
1773
+ collectionName = result;
1774
+ }
1775
+ let useTypeScript = opts.typescript ?? false;
1776
+ if (opts.typescript === void 0) {
1777
+ const result = await confirm3({
1778
+ message: "Use TypeScript?",
1779
+ initialValue: false
1780
+ });
1781
+ if (isCancel3(result)) {
1782
+ cancel3("Operation cancelled");
1783
+ process.exit(0);
1784
+ }
1785
+ useTypeScript = result;
1786
+ }
1787
+ const s = spinner3();
1788
+ s.start("Creating extension...");
1789
+ try {
1790
+ const extensionsDir = path5.join(cwd, "extensions");
1791
+ if (!existsSync5(extensionsDir)) {
1792
+ await fs4.mkdir(extensionsDir, { recursive: true });
1793
+ }
1794
+ const ext = useTypeScript ? "ts" : "js";
1795
+ const extensionDir = path5.join(extensionsDir, `baasix-${extensionType}-${extensionName}`);
1796
+ if (existsSync5(extensionDir)) {
1797
+ s.stop("Extension already exists");
1798
+ const overwrite = await confirm3({
1799
+ message: `Extension baasix-${extensionType}-${extensionName} already exists. Overwrite?`,
1800
+ initialValue: false
1801
+ });
1802
+ if (isCancel3(overwrite) || !overwrite) {
1803
+ cancel3("Operation cancelled");
1804
+ process.exit(0);
1805
+ }
1806
+ }
1807
+ await fs4.mkdir(extensionDir, { recursive: true });
1808
+ if (extensionType === "hook") {
1809
+ await createHookExtension(extensionDir, extensionName, collectionName, useTypeScript);
1810
+ } else {
1811
+ await createEndpointExtension(extensionDir, extensionName, useTypeScript);
1812
+ }
1813
+ s.stop("Extension created");
1814
+ outro3(chalk3.green(`\u2728 Extension created at extensions/baasix-${extensionType}-${extensionName}/`));
1815
+ console.log();
1816
+ console.log(chalk3.bold("Next steps:"));
1817
+ console.log(` ${chalk3.dim("1.")} Edit ${chalk3.cyan(`extensions/baasix-${extensionType}-${extensionName}/index.${ext}`)}`);
1818
+ console.log(` ${chalk3.dim("2.")} Restart your Baasix server to load the extension`);
1819
+ console.log();
1820
+ } catch (error) {
1821
+ s.stop("Failed to create extension");
1822
+ log3.error(error instanceof Error ? error.message : "Unknown error");
1823
+ process.exit(1);
1824
+ }
1825
+ }
1826
+ async function createHookExtension(extensionDir, name, collection, useTypeScript) {
1827
+ const ext = useTypeScript ? "ts" : "js";
1828
+ const typeAnnotations = useTypeScript ? `
1829
+ import type { HooksService } from "@tspvivek/baasix";
1830
+
1831
+ interface HookContext {
1832
+ ItemsService: any;
1833
+ schemaManager: any;
1834
+ services: Record<string, any>;
1835
+ }
1836
+
1837
+ interface HookPayload {
1838
+ data?: Record<string, any>;
1839
+ query?: Record<string, any>;
1840
+ id?: string | string[];
1841
+ accountability: {
1842
+ user: { id: string; email: string };
1843
+ role: { id: string; name: string };
1844
+ };
1845
+ collection: string;
1846
+ schema: any;
1847
+ }
1848
+ ` : "";
1849
+ const hookContent = `${typeAnnotations}
1850
+ /**
1851
+ * Hook extension for ${collection} collection
1852
+ *
1853
+ * Available hooks:
1854
+ * - items.create (before/after creating an item)
1855
+ * - items.read (before/after reading items)
1856
+ * - items.update (before/after updating an item)
1857
+ * - items.delete (before/after deleting an item)
1858
+ */
1859
+ export default (hooksService${useTypeScript ? ": HooksService" : ""}, context${useTypeScript ? ": HookContext" : ""}) => {
1860
+ const { ItemsService } = context;
1861
+
1862
+ // Hook for creating items
1863
+ hooksService.registerHook(
1864
+ "${collection}",
1865
+ "items.create",
1866
+ async ({ data, accountability, collection, schema }${useTypeScript ? ": HookPayload" : ""}) => {
1867
+ console.log(\`[${name}] Creating \${collection} item:\`, data);
1868
+
1869
+ // Example: Add created_by field
1870
+ // data.created_by = accountability.user.id;
1871
+
1872
+ // Return modified data
1873
+ return { data };
1874
+ }
1875
+ );
1876
+
1877
+ // Hook for reading items
1878
+ hooksService.registerHook(
1879
+ "${collection}",
1880
+ "items.read",
1881
+ async ({ query, data, accountability, collection, schema }${useTypeScript ? ": HookPayload" : ""}) => {
1882
+ console.log(\`[${name}] Reading \${collection} with query:\`, query);
1883
+
1884
+ // Example: Filter results for non-admin users
1885
+ // if (accountability.role.name !== "administrator") {
1886
+ // query.filter = { ...query.filter, published: true };
1887
+ // }
1888
+
1889
+ return { query };
1890
+ }
1891
+ );
1892
+
1893
+ // Hook for updating items
1894
+ hooksService.registerHook(
1895
+ "${collection}",
1896
+ "items.update",
1897
+ async ({ id, data, accountability, schema }${useTypeScript ? ": HookPayload" : ""}) => {
1898
+ console.log(\`[${name}] Updating item \${id}:\`, data);
1899
+
1900
+ // Example: Add updated_by field
1901
+ // data.updated_by = accountability.user.id;
1902
+
1903
+ return { id, data };
1904
+ }
1905
+ );
1906
+
1907
+ // Hook for deleting items
1908
+ hooksService.registerHook(
1909
+ "${collection}",
1910
+ "items.delete",
1911
+ async ({ id, accountability }${useTypeScript ? ": HookPayload" : ""}) => {
1912
+ console.log(\`[${name}] Deleting item:\`, id);
1913
+
1914
+ // Example: Soft delete instead of hard delete
1915
+ // const itemsService = new ItemsService("${collection}", { accountability, schema });
1916
+ // await itemsService.update(id, { deletedAt: new Date() });
1917
+ // return { skip: true }; // Skip the actual delete
1918
+
1919
+ return { id };
1920
+ }
1921
+ );
1922
+ };
1923
+ `;
1924
+ await fs4.writeFile(path5.join(extensionDir, `index.${ext}`), hookContent);
1925
+ const readme = `# baasix-hook-${name}
1926
+
1927
+ A Baasix hook extension for the \`${collection}\` collection.
1928
+
1929
+ ## Available Hooks
1930
+
1931
+ - \`items.create\` - Before/after creating an item
1932
+ - \`items.read\` - Before/after reading items
1933
+ - \`items.update\` - Before/after updating an item
1934
+ - \`items.delete\` - Before/after deleting an item
1935
+
1936
+ ## Usage
1937
+
1938
+ This extension is automatically loaded when placed in the \`extensions/\` directory.
1939
+
1940
+ Edit \`index.${ext}\` to customize the hook behavior.
1941
+
1942
+ ## Documentation
1943
+
1944
+ See [Hooks Documentation](https://baasix.com/docs/hooks) for more details.
1945
+ `;
1946
+ await fs4.writeFile(path5.join(extensionDir, "README.md"), readme);
1947
+ }
1948
+ async function createEndpointExtension(extensionDir, name, useTypeScript) {
1949
+ const ext = useTypeScript ? "ts" : "js";
1950
+ const typeAnnotations = useTypeScript ? `
1951
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
1952
+ import { APIError } from "@tspvivek/baasix";
1953
+
1954
+ interface EndpointContext {
1955
+ ItemsService: any;
1956
+ schemaManager: any;
1957
+ services: Record<string, any>;
1958
+ }
1959
+
1960
+ interface RequestWithAccountability extends FastifyRequest {
1961
+ accountability?: {
1962
+ user: { id: string; email: string };
1963
+ role: { id: string; name: string };
1964
+ };
1965
+ }
1966
+ ` : `import { APIError } from "@tspvivek/baasix";`;
1967
+ const endpointContent = `${typeAnnotations}
1968
+
1969
+ /**
1970
+ * Custom endpoint extension
1971
+ *
1972
+ * Register custom routes on the Fastify app instance.
1973
+ */
1974
+ const registerEndpoint = (app${useTypeScript ? ": FastifyInstance" : ""}, context${useTypeScript ? ": EndpointContext" : ""}) => {
1975
+ const { ItemsService } = context;
1976
+
1977
+ // GET endpoint example
1978
+ app.get("/${name}", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
1979
+ try {
1980
+ // Check authentication (optional)
1981
+ if (!req.accountability || !req.accountability.user) {
1982
+ throw new APIError("Unauthorized", 401);
1983
+ }
1984
+
1985
+ const { user, role } = req.accountability;
1986
+
1987
+ // Your custom logic here
1988
+ const result = {
1989
+ message: "Hello from ${name} endpoint!",
1990
+ user: {
1991
+ id: user.id,
1992
+ email: user.email,
1993
+ },
1994
+ timestamp: new Date().toISOString(),
1995
+ };
1996
+
1997
+ return res.send(result);
1998
+ } catch (error) {
1999
+ throw error;
2000
+ }
2001
+ });
2002
+
2003
+ // POST endpoint example
2004
+ app.post("/${name}", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
2005
+ try {
2006
+ if (!req.accountability || !req.accountability.user) {
2007
+ throw new APIError("Unauthorized", 401);
2008
+ }
2009
+
2010
+ const body = req.body${useTypeScript ? " as Record<string, any>" : ""};
2011
+
2012
+ // Example: Create an item using ItemsService
2013
+ // const itemsService = new ItemsService("my_collection", {
2014
+ // accountability: req.accountability,
2015
+ // schema: context.schemaManager,
2016
+ // });
2017
+ // const itemId = await itemsService.createOne(body);
2018
+
2019
+ return res.status(201).send({
2020
+ message: "Created successfully",
2021
+ data: body,
2022
+ });
2023
+ } catch (error) {
2024
+ throw error;
2025
+ }
2026
+ });
2027
+
2028
+ // Parameterized endpoint example
2029
+ app.get("/${name}/:id", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
2030
+ try {
2031
+ const { id } = req.params${useTypeScript ? " as { id: string }" : ""};
2032
+
2033
+ return res.send({
2034
+ message: \`Getting item \${id}\`,
2035
+ id,
2036
+ });
2037
+ } catch (error) {
2038
+ throw error;
2039
+ }
2040
+ });
2041
+ };
2042
+
2043
+ export default {
2044
+ id: "${name}",
2045
+ handler: registerEndpoint,
2046
+ };
2047
+ `;
2048
+ await fs4.writeFile(path5.join(extensionDir, `index.${ext}`), endpointContent);
2049
+ const readme = `# baasix-endpoint-${name}
2050
+
2051
+ A Baasix custom endpoint extension.
2052
+
2053
+ ## Endpoints
2054
+
2055
+ - \`GET /${name}\` - Example GET endpoint
2056
+ - \`POST /${name}\` - Example POST endpoint
2057
+ - \`GET /${name}/:id\` - Example parameterized endpoint
2058
+
2059
+ ## Usage
2060
+
2061
+ This extension is automatically loaded when placed in the \`extensions/\` directory.
2062
+
2063
+ Edit \`index.${ext}\` to customize the endpoints.
2064
+
2065
+ ## Documentation
2066
+
2067
+ See [Custom Endpoints Documentation](https://baasix.com/docs/custom-endpoints) for more details.
2068
+ `;
2069
+ await fs4.writeFile(path5.join(extensionDir, "README.md"), readme);
2070
+ }
2071
+ var extension = new Command3("extension").alias("ext").description("Generate a new Baasix extension (hook or endpoint)").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-t, --type <type>", "Extension type (hook, endpoint)").option("-n, --name <name>", "Extension name").option("--collection <collection>", "Collection name (for hooks)").option("--typescript", "Use TypeScript").option("--no-typescript", "Use JavaScript").action(extensionAction);
2072
+
2073
+ // src/commands/migrate.ts
2074
+ import { existsSync as existsSync6 } from "fs";
2075
+ import fs5 from "fs/promises";
2076
+ import path6 from "path";
2077
+ import {
2078
+ cancel as cancel4,
2079
+ confirm as confirm4,
2080
+ intro as intro4,
2081
+ isCancel as isCancel4,
2082
+ log as log4,
2083
+ outro as outro4,
2084
+ select as select4,
2085
+ spinner as spinner4,
2086
+ text as text4
2087
+ } from "@clack/prompts";
2088
+ import chalk4 from "chalk";
2089
+ import { Command as Command4 } from "commander";
2090
+ async function migrateAction(action, opts) {
2091
+ const cwd = path6.resolve(opts.cwd);
2092
+ intro4(chalk4.bgMagenta.black(" Baasix Migrations "));
2093
+ const config = await getConfig(cwd);
2094
+ if (!config && !opts.url) {
2095
+ log4.error(
2096
+ "No Baasix configuration found. Create a .env file with BAASIX_URL or use --url flag."
2097
+ );
2098
+ process.exit(1);
2099
+ }
2100
+ const effectiveConfig = config ? { ...config, url: opts.url || config.url } : { url: opts.url || "http://localhost:8056" };
2101
+ let selectedAction = action || opts.action;
2102
+ if (!selectedAction) {
2103
+ const result = await select4({
2104
+ message: "What migration action do you want to perform?",
2105
+ options: [
2106
+ {
2107
+ value: "status",
2108
+ label: "Status",
2109
+ hint: "Show current migration status"
2110
+ },
2111
+ {
2112
+ value: "list",
2113
+ label: "List",
2114
+ hint: "List all available migrations"
2115
+ },
2116
+ {
2117
+ value: "run",
2118
+ label: "Run",
2119
+ hint: "Run pending migrations"
2120
+ },
2121
+ {
2122
+ value: "create",
2123
+ label: "Create",
2124
+ hint: "Create a new migration file"
2125
+ },
2126
+ {
2127
+ value: "rollback",
2128
+ label: "Rollback",
2129
+ hint: "Rollback the last batch of migrations"
2130
+ },
2131
+ {
2132
+ value: "reset",
2133
+ label: "Reset",
2134
+ hint: "Rollback all migrations (dangerous!)"
2135
+ }
2136
+ ]
2137
+ });
2138
+ if (isCancel4(result)) {
2139
+ cancel4("Operation cancelled");
2140
+ process.exit(0);
2141
+ }
2142
+ selectedAction = result;
2143
+ }
2144
+ const s = spinner4();
2145
+ try {
2146
+ switch (selectedAction) {
2147
+ case "status":
2148
+ await showStatus(s, effectiveConfig, cwd);
2149
+ break;
2150
+ case "list":
2151
+ await listMigrations(s, effectiveConfig, cwd);
2152
+ break;
2153
+ case "run":
2154
+ await runMigrations2(s, effectiveConfig, cwd, opts.yes);
2155
+ break;
2156
+ case "create":
2157
+ await createMigration(s, cwd, opts.name);
2158
+ break;
2159
+ case "rollback":
2160
+ await rollbackMigrations2(s, effectiveConfig, cwd, opts.steps || 1, opts.yes);
2161
+ break;
2162
+ case "reset":
2163
+ await resetMigrations(s, effectiveConfig, cwd, opts.yes);
2164
+ break;
2165
+ }
2166
+ } catch (error) {
2167
+ s.stop("Migration failed");
2168
+ if (error instanceof Error) {
2169
+ log4.error(error.message);
2170
+ } else {
2171
+ log4.error("Unknown error occurred");
2172
+ }
2173
+ process.exit(1);
2174
+ }
2175
+ }
2176
+ async function showStatus(s, config, cwd) {
2177
+ s.start("Checking migration status...");
2178
+ const executedMigrations = await getExecutedMigrations(config);
2179
+ const localMigrations = await getLocalMigrations(cwd);
2180
+ s.stop("Migration status retrieved");
2181
+ const executedNames = new Set(executedMigrations.map((m) => m.name));
2182
+ const pendingMigrations = localMigrations.filter((m) => !executedNames.has(m));
2183
+ console.log();
2184
+ console.log(chalk4.bold("\u{1F4CA} Migration Status"));
2185
+ console.log(chalk4.dim("\u2500".repeat(50)));
2186
+ console.log(` Total migrations: ${chalk4.cyan(localMigrations.length)}`);
2187
+ console.log(` Executed: ${chalk4.green(executedMigrations.length)}`);
2188
+ console.log(
2189
+ ` Pending: ${pendingMigrations.length > 0 ? chalk4.yellow(pendingMigrations.length) : chalk4.gray("0")}`
2190
+ );
2191
+ console.log();
2192
+ if (pendingMigrations.length > 0) {
2193
+ console.log(chalk4.bold("Pending migrations:"));
2194
+ for (const migration of pendingMigrations) {
2195
+ console.log(` ${chalk4.yellow("\u25CB")} ${migration}`);
2196
+ }
2197
+ console.log();
2198
+ console.log(
2199
+ chalk4.dim(`Run ${chalk4.cyan("baasix migrate run")} to execute pending migrations.`)
2200
+ );
2201
+ } else {
2202
+ console.log(chalk4.green("\u2713 All migrations have been executed."));
2203
+ }
2204
+ outro4("");
2205
+ }
2206
+ async function listMigrations(s, config, cwd) {
2207
+ s.start("Fetching migrations...");
2208
+ const executedMigrations = await getExecutedMigrations(config);
2209
+ const localMigrations = await getLocalMigrations(cwd);
2210
+ s.stop("Migrations retrieved");
2211
+ const executedMap = new Map(executedMigrations.map((m) => [m.name, m]));
2212
+ console.log();
2213
+ console.log(chalk4.bold("\u{1F4CB} All Migrations"));
2214
+ console.log(chalk4.dim("\u2500".repeat(70)));
2215
+ if (localMigrations.length === 0) {
2216
+ console.log(chalk4.dim(" No migrations found."));
2217
+ } else {
2218
+ for (const name of localMigrations) {
2219
+ const executed = executedMap.get(name);
2220
+ if (executed) {
2221
+ const executedDate = executed.executedAt ? new Date(executed.executedAt).toLocaleDateString() : "unknown date";
2222
+ console.log(
2223
+ ` ${chalk4.green("\u2713")} ${name} ${chalk4.dim(`(batch ${executed.batch || "?"}, ${executedDate})`)}`
2224
+ );
2225
+ } else {
2226
+ console.log(` ${chalk4.yellow("\u25CB")} ${name} ${chalk4.dim("(pending)")}`);
2227
+ }
2228
+ }
2229
+ }
2230
+ console.log();
2231
+ outro4("");
2232
+ }
2233
+ async function runMigrations2(s, config, cwd, skipConfirm) {
2234
+ s.start("Checking for pending migrations...");
2235
+ const executedMigrations = await getExecutedMigrations(config);
2236
+ const localMigrations = await getLocalMigrations(cwd);
2237
+ const executedNames = new Set(executedMigrations.map((m) => m.name));
2238
+ const pendingMigrations = localMigrations.filter((m) => !executedNames.has(m));
2239
+ if (pendingMigrations.length === 0) {
2240
+ s.stop("No pending migrations");
2241
+ log4.info("All migrations have already been executed.");
2242
+ outro4("");
2243
+ return;
2244
+ }
2245
+ s.stop(`Found ${pendingMigrations.length} pending migrations`);
2246
+ console.log();
2247
+ console.log(chalk4.bold("Migrations to run:"));
2248
+ for (const name of pendingMigrations) {
2249
+ console.log(` ${chalk4.cyan("\u2192")} ${name}`);
2250
+ }
2251
+ console.log();
2252
+ if (!skipConfirm) {
2253
+ const confirmed = await confirm4({
2254
+ message: `Run ${pendingMigrations.length} migration(s)?`,
2255
+ initialValue: true
2256
+ });
2257
+ if (isCancel4(confirmed) || !confirmed) {
2258
+ cancel4("Operation cancelled");
2259
+ process.exit(0);
2260
+ }
2261
+ }
2262
+ s.start("Running migrations...");
2263
+ try {
2264
+ const result = await runMigrations(config, {
2265
+ step: pendingMigrations.length
2266
+ });
2267
+ if (result.success) {
2268
+ s.stop("Migrations executed");
2269
+ outro4(chalk4.green(`\u2728 ${result.message}`));
2270
+ } else {
2271
+ s.stop("Migration failed");
2272
+ log4.error(result.message);
2273
+ process.exit(1);
2274
+ }
2275
+ } catch (error) {
2276
+ s.stop("Migration failed");
2277
+ throw error;
2278
+ }
2279
+ }
2280
+ async function createMigration(s, cwd, name) {
2281
+ let migrationName = name;
2282
+ if (!migrationName) {
2283
+ const result = await text4({
2284
+ message: "Migration name:",
2285
+ placeholder: "create_users_table",
2286
+ validate: (value) => {
2287
+ if (!value) return "Migration name is required";
2288
+ if (!/^[a-z0-9_]+$/i.test(value)) {
2289
+ return "Migration name can only contain letters, numbers, and underscores";
2290
+ }
2291
+ return void 0;
2292
+ }
2293
+ });
2294
+ if (isCancel4(result)) {
2295
+ cancel4("Operation cancelled");
2296
+ process.exit(0);
2297
+ }
2298
+ migrationName = result;
2299
+ }
2300
+ s.start("Creating migration file...");
2301
+ const migrationsDir = path6.join(cwd, "migrations");
2302
+ if (!existsSync6(migrationsDir)) {
2303
+ await fs5.mkdir(migrationsDir, { recursive: true });
2304
+ }
2305
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
2306
+ const filename = `${timestamp}_${migrationName}.js`;
2307
+ const filepath = path6.join(migrationsDir, filename);
2308
+ if (existsSync6(filepath)) {
2309
+ s.stop("File already exists");
2310
+ log4.error(`Migration file ${filename} already exists.`);
2311
+ process.exit(1);
2312
+ }
2313
+ const template = `/**
2314
+ * Migration: ${migrationName}
2315
+ * Created: ${(/* @__PURE__ */ new Date()).toISOString()}
2316
+ */
2317
+
2318
+ /**
2319
+ * Run the migration
2320
+ * @param {import("@tspvivek/baasix-sdk").BaasixClient} baasix - Baasix client
2321
+ */
2322
+ export async function up(baasix) {
2323
+ // Example: Create a collection
2324
+ // await baasix.schema.create("tableName", {
2325
+ // name: "TableName",
2326
+ // timestamps: true,
2327
+ // fields: {
2328
+ // id: { type: "UUID", primaryKey: true, defaultValue: { type: "UUIDV4" } },
2329
+ // name: { type: "String", allowNull: false, values: { length: 255 } },
2330
+ // },
2331
+ // });
2332
+
2333
+ // Example: Add a field
2334
+ // await baasix.schema.update("tableName", {
2335
+ // fields: {
2336
+ // newField: { type: "String", allowNull: true },
2337
+ // },
2338
+ // });
2339
+
2340
+ // Example: Insert data
2341
+ // await baasix.items("tableName").create({ name: "Example" });
2342
+ }
2343
+
2344
+ /**
2345
+ * Reverse the migration
2346
+ * @param {import("@tspvivek/baasix-sdk").BaasixClient} baasix - Baasix client
2347
+ */
2348
+ export async function down(baasix) {
2349
+ // Reverse the changes made in up()
2350
+ // Example: Drop a collection
2351
+ // await baasix.schema.delete("tableName");
2352
+ }
2353
+ `;
2354
+ await fs5.writeFile(filepath, template);
2355
+ s.stop("Migration created");
2356
+ outro4(chalk4.green(`\u2728 Created migration: ${chalk4.cyan(filename)}`));
2357
+ console.log();
2358
+ console.log(` Edit: ${chalk4.dim(path6.relative(cwd, filepath))}`);
2359
+ console.log();
2360
+ }
2361
+ async function rollbackMigrations2(s, config, cwd, steps, skipConfirm) {
2362
+ s.start("Fetching executed migrations...");
2363
+ const executedMigrations = await getExecutedMigrations(config);
2364
+ if (executedMigrations.length === 0) {
2365
+ s.stop("No migrations to rollback");
2366
+ log4.info("No migrations have been executed.");
2367
+ outro4("");
2368
+ return;
2369
+ }
2370
+ const sortedByBatch = [...executedMigrations].sort(
2371
+ (a, b) => (b.batch || 0) - (a.batch || 0)
2372
+ );
2373
+ const batchesToRollback = /* @__PURE__ */ new Set();
2374
+ const migrationsToRollback = [];
2375
+ for (const migration of sortedByBatch) {
2376
+ const batch = migration.batch || 0;
2377
+ if (batchesToRollback.size < steps) {
2378
+ batchesToRollback.add(batch);
2379
+ }
2380
+ if (batchesToRollback.has(batch)) {
2381
+ migrationsToRollback.push(migration);
2382
+ }
2383
+ }
2384
+ s.stop(`Found ${migrationsToRollback.length} migration(s) to rollback`);
2385
+ console.log();
2386
+ console.log(chalk4.bold("Migrations to rollback:"));
2387
+ for (const migration of migrationsToRollback) {
2388
+ console.log(
2389
+ ` ${chalk4.red("\u2190")} ${migration.name} ${chalk4.dim(`(batch ${migration.batch || "?"})`)}`
2390
+ );
2391
+ }
2392
+ console.log();
2393
+ if (!skipConfirm) {
2394
+ const confirmed = await confirm4({
2395
+ message: `Rollback ${migrationsToRollback.length} migration(s)?`,
2396
+ initialValue: false
2397
+ });
2398
+ if (isCancel4(confirmed) || !confirmed) {
2399
+ cancel4("Operation cancelled");
2400
+ process.exit(0);
2401
+ }
2402
+ }
2403
+ s.start("Rolling back migrations...");
2404
+ try {
2405
+ const result = await rollbackMigrations(config, {
2406
+ step: steps
2407
+ });
2408
+ if (result.success) {
2409
+ s.stop("Rollback complete");
2410
+ outro4(chalk4.green(`\u2728 ${result.message}`));
2411
+ } else {
2412
+ s.stop("Rollback failed");
2413
+ log4.error(result.message);
2414
+ process.exit(1);
2415
+ }
2416
+ } catch (error) {
2417
+ s.stop("Rollback failed");
2418
+ throw error;
2419
+ }
2420
+ }
2421
+ async function resetMigrations(s, config, cwd, skipConfirm) {
2422
+ s.start("Fetching all executed migrations...");
2423
+ const executedMigrations = await getExecutedMigrations(config);
2424
+ if (executedMigrations.length === 0) {
2425
+ s.stop("No migrations to reset");
2426
+ log4.info("No migrations have been executed.");
2427
+ outro4("");
2428
+ return;
2429
+ }
2430
+ s.stop(`Found ${executedMigrations.length} executed migration(s)`);
2431
+ console.log();
2432
+ log4.warn(chalk4.red.bold("\u26A0\uFE0F This will rollback ALL migrations!"));
2433
+ console.log();
2434
+ if (!skipConfirm) {
2435
+ const confirmed = await confirm4({
2436
+ message: `Reset all ${executedMigrations.length} migration(s)? This cannot be undone!`,
2437
+ initialValue: false
2438
+ });
2439
+ if (isCancel4(confirmed) || !confirmed) {
2440
+ cancel4("Operation cancelled");
2441
+ process.exit(0);
2442
+ }
2443
+ const doubleConfirm = await text4({
2444
+ message: "Type 'reset' to confirm:",
2445
+ placeholder: "reset",
2446
+ validate: (value) => value !== "reset" ? "Please type 'reset' to confirm" : void 0
2447
+ });
2448
+ if (isCancel4(doubleConfirm)) {
2449
+ cancel4("Operation cancelled");
2450
+ process.exit(0);
2451
+ }
2452
+ }
2453
+ s.start("Resetting all migrations...");
2454
+ try {
2455
+ const maxBatch = Math.max(...executedMigrations.map((m) => m.batch || 0));
2456
+ const result = await rollbackMigrations(config, {
2457
+ step: maxBatch
2458
+ });
2459
+ if (result.success) {
2460
+ s.stop("Reset complete");
2461
+ outro4(chalk4.green(`\u2728 ${result.message}`));
2462
+ } else {
2463
+ s.stop("Reset failed");
2464
+ log4.error(result.message);
2465
+ process.exit(1);
2466
+ }
2467
+ } catch (error) {
2468
+ s.stop("Reset failed");
2469
+ throw error;
2470
+ }
2471
+ }
2472
+ async function getExecutedMigrations(config) {
2473
+ try {
2474
+ return await fetchMigrations(config);
2475
+ } catch {
2476
+ return [];
2477
+ }
2478
+ }
2479
+ async function getLocalMigrations(cwd) {
2480
+ const migrationsDir = path6.join(cwd, "migrations");
2481
+ if (!existsSync6(migrationsDir)) {
2482
+ return [];
2483
+ }
2484
+ const files = await fs5.readdir(migrationsDir);
2485
+ return files.filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
2486
+ }
2487
+ var migrate = new Command4("migrate").description("Run database migrations").argument(
2488
+ "[action]",
2489
+ "Migration action (status, list, run, create, rollback, reset)"
2490
+ ).option("-c, --cwd <path>", "Working directory", process.cwd()).option("--url <url>", "Baasix server URL").option("-n, --name <name>", "Migration name (for create)").option("-s, --steps <number>", "Number of batches to rollback", parseInt).option("-y, --yes", "Skip confirmation prompts").action(migrateAction);
2491
+
2492
+ // src/utils/get-package-info.ts
2493
+ import fs6 from "fs/promises";
2494
+ import path7 from "path";
2495
+ import { fileURLToPath } from "url";
2496
+ async function getPackageInfo() {
2497
+ const __filename = fileURLToPath(import.meta.url);
2498
+ const __dirname = path7.dirname(__filename);
2499
+ const packageJsonPath = path7.resolve(__dirname, "../../package.json");
2500
+ const content = await fs6.readFile(packageJsonPath, "utf-8");
2501
+ return JSON.parse(content);
2502
+ }
2503
+
2504
+ // src/index.ts
2505
+ import "dotenv/config";
2506
+ process.on("SIGINT", () => process.exit(0));
2507
+ process.on("SIGTERM", () => process.exit(0));
2508
+ async function main() {
2509
+ const program = new Command5("baasix");
2510
+ let packageInfo = {};
2511
+ try {
2512
+ packageInfo = await getPackageInfo();
2513
+ } catch {
2514
+ }
2515
+ program.addCommand(init).addCommand(generate).addCommand(extension).addCommand(migrate).version(packageInfo.version || "0.1.0").description("Baasix CLI - Backend-as-a-Service toolkit").action(() => program.help());
2516
+ program.parse();
2517
+ }
2518
+ main().catch((error) => {
2519
+ console.error("Error running Baasix CLI:", error);
2520
+ process.exit(1);
2521
+ });