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/README.md +355 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +2521 -0
- package/package.json +56 -0
- package/src/commands/extension.ts +447 -0
- package/src/commands/generate.ts +485 -0
- package/src/commands/init.ts +1409 -0
- package/src/commands/migrate.ts +573 -0
- package/src/index.ts +39 -0
- package/src/utils/api-client.ts +121 -0
- package/src/utils/get-config.ts +69 -0
- package/src/utils/get-package-info.ts +12 -0
- package/src/utils/package-manager.ts +62 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +16 -0
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
|
+
});
|