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/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "baasix",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CLI for Baasix Backend-as-a-Service",
6
+ "module": "dist/index.mjs",
7
+ "main": "./dist/index.mjs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/tspvivek/baasix.git",
11
+ "directory": "cli"
12
+ },
13
+ "homepage": "https://baasix.com/docs/cli",
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "start": "node ./dist/index.mjs",
17
+ "dev": "tsx ./src/index.ts",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "executableFiles": [
23
+ "./dist/index.mjs"
24
+ ]
25
+ },
26
+ "license": "MIT",
27
+ "keywords": [
28
+ "baasix",
29
+ "cli",
30
+ "backend",
31
+ "baas",
32
+ "typescript"
33
+ ],
34
+ "exports": "./dist/index.mjs",
35
+ "bin": {
36
+ "baasix": "./dist/index.mjs"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.11.0",
40
+ "@types/prompts": "^2.4.9",
41
+ "tsup": "^8.0.1",
42
+ "tsx": "^4.20.6",
43
+ "typescript": "^5.3.3"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "^0.11.0",
47
+ "axios": "^1.6.0",
48
+ "chalk": "^5.3.0",
49
+ "commander": "^12.0.0",
50
+ "dotenv": "^16.3.1",
51
+ "ora": "^8.0.1",
52
+ "prettier": "^3.2.0",
53
+ "prompts": "^2.4.2",
54
+ "zod": "^3.22.0"
55
+ }
56
+ }
@@ -0,0 +1,447 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ cancel,
6
+ confirm,
7
+ intro,
8
+ isCancel,
9
+ log,
10
+ outro,
11
+ select,
12
+ spinner,
13
+ text,
14
+ } from "@clack/prompts";
15
+ import chalk from "chalk";
16
+ import { Command } from "commander";
17
+
18
+ type ExtensionType = "hook" | "endpoint";
19
+
20
+ interface ExtensionOptions {
21
+ cwd: string;
22
+ type?: ExtensionType;
23
+ name?: string;
24
+ collection?: string;
25
+ typescript?: boolean;
26
+ }
27
+
28
+ async function extensionAction(opts: ExtensionOptions) {
29
+ const cwd = path.resolve(opts.cwd);
30
+
31
+ intro(chalk.bgMagenta.black(" Baasix Extension Generator "));
32
+
33
+ // Select extension type
34
+ let extensionType = opts.type;
35
+ if (!extensionType) {
36
+ const result = await select({
37
+ message: "What type of extension do you want to create?",
38
+ options: [
39
+ {
40
+ value: "hook",
41
+ label: "Hook",
42
+ hint: "Intercept and modify CRUD operations",
43
+ },
44
+ {
45
+ value: "endpoint",
46
+ label: "Custom Endpoint",
47
+ hint: "Add new API routes",
48
+ },
49
+ ],
50
+ });
51
+
52
+ if (isCancel(result)) {
53
+ cancel("Operation cancelled");
54
+ process.exit(0);
55
+ }
56
+ extensionType = result as ExtensionType;
57
+ }
58
+
59
+ // Get extension name
60
+ let extensionName = opts.name;
61
+ if (!extensionName) {
62
+ const result = await text({
63
+ message: "What is your extension name?",
64
+ placeholder: extensionType === "hook" ? "my-hook" : "my-endpoint",
65
+ validate: (value) => {
66
+ if (!value) return "Extension name is required";
67
+ if (!/^[a-z0-9-_]+$/i.test(value)) return "Name must be alphanumeric with dashes or underscores";
68
+ return undefined;
69
+ },
70
+ });
71
+
72
+ if (isCancel(result)) {
73
+ cancel("Operation cancelled");
74
+ process.exit(0);
75
+ }
76
+ extensionName = result as string;
77
+ }
78
+
79
+ // For hooks, ask for collection name
80
+ let collectionName = opts.collection;
81
+ if (extensionType === "hook" && !collectionName) {
82
+ const result = await text({
83
+ message: "Which collection should this hook apply to?",
84
+ placeholder: "posts",
85
+ validate: (value) => {
86
+ if (!value) return "Collection name is required";
87
+ return undefined;
88
+ },
89
+ });
90
+
91
+ if (isCancel(result)) {
92
+ cancel("Operation cancelled");
93
+ process.exit(0);
94
+ }
95
+ collectionName = result as string;
96
+ }
97
+
98
+ // Use TypeScript?
99
+ let useTypeScript = opts.typescript ?? false;
100
+ if (opts.typescript === undefined) {
101
+ const result = await confirm({
102
+ message: "Use TypeScript?",
103
+ initialValue: false,
104
+ });
105
+
106
+ if (isCancel(result)) {
107
+ cancel("Operation cancelled");
108
+ process.exit(0);
109
+ }
110
+ useTypeScript = result;
111
+ }
112
+
113
+ const s = spinner();
114
+ s.start("Creating extension...");
115
+
116
+ try {
117
+ // Determine extensions directory
118
+ const extensionsDir = path.join(cwd, "extensions");
119
+ if (!existsSync(extensionsDir)) {
120
+ await fs.mkdir(extensionsDir, { recursive: true });
121
+ }
122
+
123
+ const ext = useTypeScript ? "ts" : "js";
124
+ const extensionDir = path.join(extensionsDir, `baasix-${extensionType}-${extensionName}`);
125
+
126
+ // Check if extension already exists
127
+ if (existsSync(extensionDir)) {
128
+ s.stop("Extension already exists");
129
+ const overwrite = await confirm({
130
+ message: `Extension baasix-${extensionType}-${extensionName} already exists. Overwrite?`,
131
+ initialValue: false,
132
+ });
133
+
134
+ if (isCancel(overwrite) || !overwrite) {
135
+ cancel("Operation cancelled");
136
+ process.exit(0);
137
+ }
138
+ }
139
+
140
+ await fs.mkdir(extensionDir, { recursive: true });
141
+
142
+ if (extensionType === "hook") {
143
+ await createHookExtension(extensionDir, extensionName, collectionName!, useTypeScript);
144
+ } else {
145
+ await createEndpointExtension(extensionDir, extensionName, useTypeScript);
146
+ }
147
+
148
+ s.stop("Extension created");
149
+
150
+ outro(chalk.green(`✨ Extension created at extensions/baasix-${extensionType}-${extensionName}/`));
151
+
152
+ // Print next steps
153
+ console.log();
154
+ console.log(chalk.bold("Next steps:"));
155
+ console.log(` ${chalk.dim("1.")} Edit ${chalk.cyan(`extensions/baasix-${extensionType}-${extensionName}/index.${ext}`)}`);
156
+ console.log(` ${chalk.dim("2.")} Restart your Baasix server to load the extension`);
157
+ console.log();
158
+
159
+ } catch (error) {
160
+ s.stop("Failed to create extension");
161
+ log.error(error instanceof Error ? error.message : "Unknown error");
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ async function createHookExtension(
167
+ extensionDir: string,
168
+ name: string,
169
+ collection: string,
170
+ useTypeScript: boolean
171
+ ) {
172
+ const ext = useTypeScript ? "ts" : "js";
173
+
174
+ const typeAnnotations = useTypeScript
175
+ ? `
176
+ import type { HooksService } from "@tspvivek/baasix";
177
+
178
+ interface HookContext {
179
+ ItemsService: any;
180
+ schemaManager: any;
181
+ services: Record<string, any>;
182
+ }
183
+
184
+ interface HookPayload {
185
+ data?: Record<string, any>;
186
+ query?: Record<string, any>;
187
+ id?: string | string[];
188
+ accountability: {
189
+ user: { id: string; email: string };
190
+ role: { id: string; name: string };
191
+ };
192
+ collection: string;
193
+ schema: any;
194
+ }
195
+ `
196
+ : "";
197
+
198
+ const hookContent = `${typeAnnotations}
199
+ /**
200
+ * Hook extension for ${collection} collection
201
+ *
202
+ * Available hooks:
203
+ * - items.create (before/after creating an item)
204
+ * - items.read (before/after reading items)
205
+ * - items.update (before/after updating an item)
206
+ * - items.delete (before/after deleting an item)
207
+ */
208
+ export default (hooksService${useTypeScript ? ": HooksService" : ""}, context${useTypeScript ? ": HookContext" : ""}) => {
209
+ const { ItemsService } = context;
210
+
211
+ // Hook for creating items
212
+ hooksService.registerHook(
213
+ "${collection}",
214
+ "items.create",
215
+ async ({ data, accountability, collection, schema }${useTypeScript ? ": HookPayload" : ""}) => {
216
+ console.log(\`[${name}] Creating \${collection} item:\`, data);
217
+
218
+ // Example: Add created_by field
219
+ // data.created_by = accountability.user.id;
220
+
221
+ // Return modified data
222
+ return { data };
223
+ }
224
+ );
225
+
226
+ // Hook for reading items
227
+ hooksService.registerHook(
228
+ "${collection}",
229
+ "items.read",
230
+ async ({ query, data, accountability, collection, schema }${useTypeScript ? ": HookPayload" : ""}) => {
231
+ console.log(\`[${name}] Reading \${collection} with query:\`, query);
232
+
233
+ // Example: Filter results for non-admin users
234
+ // if (accountability.role.name !== "administrator") {
235
+ // query.filter = { ...query.filter, published: true };
236
+ // }
237
+
238
+ return { query };
239
+ }
240
+ );
241
+
242
+ // Hook for updating items
243
+ hooksService.registerHook(
244
+ "${collection}",
245
+ "items.update",
246
+ async ({ id, data, accountability, schema }${useTypeScript ? ": HookPayload" : ""}) => {
247
+ console.log(\`[${name}] Updating item \${id}:\`, data);
248
+
249
+ // Example: Add updated_by field
250
+ // data.updated_by = accountability.user.id;
251
+
252
+ return { id, data };
253
+ }
254
+ );
255
+
256
+ // Hook for deleting items
257
+ hooksService.registerHook(
258
+ "${collection}",
259
+ "items.delete",
260
+ async ({ id, accountability }${useTypeScript ? ": HookPayload" : ""}) => {
261
+ console.log(\`[${name}] Deleting item:\`, id);
262
+
263
+ // Example: Soft delete instead of hard delete
264
+ // const itemsService = new ItemsService("${collection}", { accountability, schema });
265
+ // await itemsService.update(id, { deletedAt: new Date() });
266
+ // return { skip: true }; // Skip the actual delete
267
+
268
+ return { id };
269
+ }
270
+ );
271
+ };
272
+ `;
273
+
274
+ await fs.writeFile(path.join(extensionDir, `index.${ext}`), hookContent);
275
+
276
+ // Create README
277
+ const readme = `# baasix-hook-${name}
278
+
279
+ A Baasix hook extension for the \`${collection}\` collection.
280
+
281
+ ## Available Hooks
282
+
283
+ - \`items.create\` - Before/after creating an item
284
+ - \`items.read\` - Before/after reading items
285
+ - \`items.update\` - Before/after updating an item
286
+ - \`items.delete\` - Before/after deleting an item
287
+
288
+ ## Usage
289
+
290
+ This extension is automatically loaded when placed in the \`extensions/\` directory.
291
+
292
+ Edit \`index.${ext}\` to customize the hook behavior.
293
+
294
+ ## Documentation
295
+
296
+ See [Hooks Documentation](https://baasix.com/docs/hooks) for more details.
297
+ `;
298
+
299
+ await fs.writeFile(path.join(extensionDir, "README.md"), readme);
300
+ }
301
+
302
+ async function createEndpointExtension(
303
+ extensionDir: string,
304
+ name: string,
305
+ useTypeScript: boolean
306
+ ) {
307
+ const ext = useTypeScript ? "ts" : "js";
308
+
309
+ const typeAnnotations = useTypeScript
310
+ ? `
311
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
312
+ import { APIError } from "@tspvivek/baasix";
313
+
314
+ interface EndpointContext {
315
+ ItemsService: any;
316
+ schemaManager: any;
317
+ services: Record<string, any>;
318
+ }
319
+
320
+ interface RequestWithAccountability extends FastifyRequest {
321
+ accountability?: {
322
+ user: { id: string; email: string };
323
+ role: { id: string; name: string };
324
+ };
325
+ }
326
+ `
327
+ : `import { APIError } from "@tspvivek/baasix";`;
328
+
329
+ const endpointContent = `${typeAnnotations}
330
+
331
+ /**
332
+ * Custom endpoint extension
333
+ *
334
+ * Register custom routes on the Fastify app instance.
335
+ */
336
+ const registerEndpoint = (app${useTypeScript ? ": FastifyInstance" : ""}, context${useTypeScript ? ": EndpointContext" : ""}) => {
337
+ const { ItemsService } = context;
338
+
339
+ // GET endpoint example
340
+ app.get("/${name}", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
341
+ try {
342
+ // Check authentication (optional)
343
+ if (!req.accountability || !req.accountability.user) {
344
+ throw new APIError("Unauthorized", 401);
345
+ }
346
+
347
+ const { user, role } = req.accountability;
348
+
349
+ // Your custom logic here
350
+ const result = {
351
+ message: "Hello from ${name} endpoint!",
352
+ user: {
353
+ id: user.id,
354
+ email: user.email,
355
+ },
356
+ timestamp: new Date().toISOString(),
357
+ };
358
+
359
+ return res.send(result);
360
+ } catch (error) {
361
+ throw error;
362
+ }
363
+ });
364
+
365
+ // POST endpoint example
366
+ app.post("/${name}", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
367
+ try {
368
+ if (!req.accountability || !req.accountability.user) {
369
+ throw new APIError("Unauthorized", 401);
370
+ }
371
+
372
+ const body = req.body${useTypeScript ? " as Record<string, any>" : ""};
373
+
374
+ // Example: Create an item using ItemsService
375
+ // const itemsService = new ItemsService("my_collection", {
376
+ // accountability: req.accountability,
377
+ // schema: context.schemaManager,
378
+ // });
379
+ // const itemId = await itemsService.createOne(body);
380
+
381
+ return res.status(201).send({
382
+ message: "Created successfully",
383
+ data: body,
384
+ });
385
+ } catch (error) {
386
+ throw error;
387
+ }
388
+ });
389
+
390
+ // Parameterized endpoint example
391
+ app.get("/${name}/:id", async (req${useTypeScript ? ": RequestWithAccountability" : ""}, res${useTypeScript ? ": FastifyReply" : ""}) => {
392
+ try {
393
+ const { id } = req.params${useTypeScript ? " as { id: string }" : ""};
394
+
395
+ return res.send({
396
+ message: \`Getting item \${id}\`,
397
+ id,
398
+ });
399
+ } catch (error) {
400
+ throw error;
401
+ }
402
+ });
403
+ };
404
+
405
+ export default {
406
+ id: "${name}",
407
+ handler: registerEndpoint,
408
+ };
409
+ `;
410
+
411
+ await fs.writeFile(path.join(extensionDir, `index.${ext}`), endpointContent);
412
+
413
+ // Create README
414
+ const readme = `# baasix-endpoint-${name}
415
+
416
+ A Baasix custom endpoint extension.
417
+
418
+ ## Endpoints
419
+
420
+ - \`GET /${name}\` - Example GET endpoint
421
+ - \`POST /${name}\` - Example POST endpoint
422
+ - \`GET /${name}/:id\` - Example parameterized endpoint
423
+
424
+ ## Usage
425
+
426
+ This extension is automatically loaded when placed in the \`extensions/\` directory.
427
+
428
+ Edit \`index.${ext}\` to customize the endpoints.
429
+
430
+ ## Documentation
431
+
432
+ See [Custom Endpoints Documentation](https://baasix.com/docs/custom-endpoints) for more details.
433
+ `;
434
+
435
+ await fs.writeFile(path.join(extensionDir, "README.md"), readme);
436
+ }
437
+
438
+ export const extension = new Command("extension")
439
+ .alias("ext")
440
+ .description("Generate a new Baasix extension (hook or endpoint)")
441
+ .option("-c, --cwd <path>", "Working directory", process.cwd())
442
+ .option("-t, --type <type>", "Extension type (hook, endpoint)")
443
+ .option("-n, --name <name>", "Extension name")
444
+ .option("--collection <collection>", "Collection name (for hooks)")
445
+ .option("--typescript", "Use TypeScript")
446
+ .option("--no-typescript", "Use JavaScript")
447
+ .action(extensionAction);