@tamayrp14/fire-api 0.0.0-stage → 1.0.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.
Files changed (98) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bun.lock +20 -0
  4. package/docs/GETTING_STARTED.md +150 -0
  5. package/docs/LAUNCH_KIT.md +236 -0
  6. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  7. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  8. package/examples/basic-api/index.ts +177 -0
  9. package/package.json +68 -4
  10. package/src/ai/mcp.ts +140 -0
  11. package/src/auth/auth.ts +110 -0
  12. package/src/auth/crypto.ts +70 -0
  13. package/src/auth/jwt.ts +105 -0
  14. package/src/cache/cache.ts +72 -0
  15. package/src/cli/bench.ts +118 -0
  16. package/src/cli/compile.ts +29 -0
  17. package/src/cli/create-app.ts +284 -0
  18. package/src/cli/docker.ts +51 -0
  19. package/src/cli/generator.ts +75 -0
  20. package/src/cli/routes.ts +76 -0
  21. package/src/client.ts +120 -0
  22. package/src/compat/express.ts +139 -0
  23. package/src/core/app.ts +723 -0
  24. package/src/core/context.ts +218 -0
  25. package/src/core/error.ts +42 -0
  26. package/src/core/router.ts +262 -0
  27. package/src/db/db-interface.ts +9 -0
  28. package/src/db/hub.ts +256 -0
  29. package/src/db/memory-adapter.ts +202 -0
  30. package/src/db/migrator.ts +100 -0
  31. package/src/db/sqlite-adapter.ts +58 -0
  32. package/src/docs/collection.ts +57 -0
  33. package/src/docs/scalar-ui.ts +25 -0
  34. package/src/events/event-bus.ts +61 -0
  35. package/src/index.ts +37 -0
  36. package/src/jobs/cron.ts +77 -0
  37. package/src/jobs/queue.ts +108 -0
  38. package/src/logging/logger.ts +66 -0
  39. package/src/metrics/metrics.ts +66 -0
  40. package/src/modules/product/product.route.ts +36 -0
  41. package/src/modules/product/product.schema.ts +10 -0
  42. package/src/schema/mock.ts +60 -0
  43. package/src/schema/openapi.ts +101 -0
  44. package/src/schema/schema.ts +519 -0
  45. package/src/schema/validator.ts +5 -0
  46. package/src/shield/compress.ts +61 -0
  47. package/src/shield/cors.ts +94 -0
  48. package/src/shield/headers.ts +55 -0
  49. package/src/shield/idempotency.ts +76 -0
  50. package/src/shield/rate-limiter.ts +81 -0
  51. package/src/shield/sanitizer.ts +23 -0
  52. package/src/shield/webhook.ts +101 -0
  53. package/src/static/serve.ts +51 -0
  54. package/src/storage/storage.ts +176 -0
  55. package/src/stream/sse.ts +89 -0
  56. package/src/studio/studio.ts +221 -0
  57. package/src/types.ts +241 -0
  58. package/src/ws/ws.ts +118 -0
  59. package/test/ai-mcp.test.ts +61 -0
  60. package/test/auth.test.ts +108 -0
  61. package/test/bench.test.ts +31 -0
  62. package/test/cache.test.ts +36 -0
  63. package/test/cli-create.test.ts +64 -0
  64. package/test/client.test.ts +54 -0
  65. package/test/collection-export.test.ts +52 -0
  66. package/test/compat.test.ts +48 -0
  67. package/test/compress.test.ts +48 -0
  68. package/test/context.test.ts +48 -0
  69. package/test/db.test.ts +62 -0
  70. package/test/docker.test.ts +28 -0
  71. package/test/events.test.ts +62 -0
  72. package/test/file-upload.test.ts +116 -0
  73. package/test/hooks.test.ts +77 -0
  74. package/test/idempotency.test.ts +65 -0
  75. package/test/integration.test.ts +111 -0
  76. package/test/jobs.test.ts +60 -0
  77. package/test/metrics.test.ts +40 -0
  78. package/test/mock.test.ts +33 -0
  79. package/test/multi-tenant.test.ts +80 -0
  80. package/test/observability.test.ts +95 -0
  81. package/test/openapi.test.ts +41 -0
  82. package/test/orm-relations.test.ts +85 -0
  83. package/test/orm-shortcuts.test.ts +52 -0
  84. package/test/rbac.test.ts +85 -0
  85. package/test/router-fastpath.test.ts +42 -0
  86. package/test/router.test.ts +84 -0
  87. package/test/schema.test.ts +49 -0
  88. package/test/seeder-migrator.test.ts +78 -0
  89. package/test/shield.test.ts +55 -0
  90. package/test/shorthand-dx.test.ts +31 -0
  91. package/test/sse.test.ts +43 -0
  92. package/test/static-serve.test.ts +53 -0
  93. package/test/storage.test.ts +31 -0
  94. package/test/studio.test.ts +43 -0
  95. package/test/tx.test.ts +92 -0
  96. package/test/webhook.test.ts +113 -0
  97. package/test/ws.test.ts +54 -0
  98. package/tsconfig.json +19 -0
@@ -0,0 +1,29 @@
1
+ import { execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ export interface CompileOptions {
6
+ entry?: string;
7
+ output?: string;
8
+ minify?: boolean;
9
+ }
10
+
11
+ export function compileBinary(options: CompileOptions = {}): { command: string; success: boolean; output: string } {
12
+ const entry = options.entry || (fs.existsSync('./src/index.ts') ? './src/index.ts' : './index.ts');
13
+ const output = options.output || './dist/server';
14
+ const minifyFlag = options.minify !== false ? '--minify' : '';
15
+
16
+ const outDir = path.dirname(output);
17
+ if (!fs.existsSync(outDir)) {
18
+ fs.mkdirSync(outDir, { recursive: true });
19
+ }
20
+
21
+ const command = `bun build ${entry} --compile ${minifyFlag} --outfile ${output}`.trim();
22
+
23
+ try {
24
+ const stdout = execSync(command, { encoding: 'utf-8', stdio: 'pipe' });
25
+ return { command, success: true, output: stdout };
26
+ } catch (err: any) {
27
+ return { command, success: false, output: err.message };
28
+ }
29
+ }
@@ -0,0 +1,284 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface CreateAppOptions {
5
+ name: string;
6
+ template?: 'standard' | 'minimal';
7
+ targetDir?: string;
8
+ }
9
+
10
+ export function createAppProject(options: CreateAppOptions): { projectDir: string; filesCreated: string[] } {
11
+ const projectName = options.name || 'my-fire-api';
12
+ const baseDir = options.targetDir || path.resolve(process.cwd(), projectName);
13
+
14
+ if (!fs.existsSync(baseDir)) {
15
+ fs.mkdirSync(baseDir, { recursive: true });
16
+ }
17
+
18
+ const filesCreated: string[] = [];
19
+
20
+ const writeFile = (relPath: string, content: string) => {
21
+ const fullPath = path.join(baseDir, relPath);
22
+ const dir = path.dirname(fullPath);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ fs.writeFileSync(fullPath, content.trim() + '\n', 'utf-8');
27
+ filesCreated.push(relPath);
28
+ };
29
+
30
+ // 1. package.json
31
+ writeFile('package.json', JSON.stringify({
32
+ name: projectName,
33
+ version: '1.0.0',
34
+ description: 'High-performance API built with FireAPI',
35
+ type: 'module',
36
+ main: 'src/index.ts',
37
+ scripts: {
38
+ dev: 'bun --watch src/index.ts',
39
+ start: 'bun src/index.ts',
40
+ test: 'bun test',
41
+ compile: 'fire compile ./dist/server',
42
+ docker: 'fire docker',
43
+ routes: 'fire routes',
44
+ },
45
+ dependencies: {
46
+ 'fire-api': '^1.0.0',
47
+ },
48
+ devDependencies: {
49
+ '@types/node': '^20.0.0',
50
+ typescript: '^5.0.0',
51
+ },
52
+ }, null, 2));
53
+
54
+ // 2. tsconfig.json
55
+ writeFile('tsconfig.json', JSON.stringify({
56
+ compilerOptions: {
57
+ target: 'ESNext',
58
+ module: 'ESNext',
59
+ moduleResolution: 'bundler',
60
+ strict: true,
61
+ skipLibCheck: true,
62
+ esModuleInterop: true,
63
+ allowSyntheticDefaultImports: true,
64
+ forceConsistentCasingInFileNames: true,
65
+ outDir: './dist',
66
+ },
67
+ include: ['src/**/*', 'test/**/*'],
68
+ }, null, 2));
69
+
70
+ // 3. .env.example & .env
71
+ const envContent = `PORT=3000
72
+ NODE_ENV=development
73
+ JWT_SECRET=super_secret_jwt_key_change_me_in_prod
74
+ DB_DRIVER=sqlite
75
+ DB_URL=./data.db
76
+ CORS_ORIGIN=*
77
+ `;
78
+ writeFile('.env.example', envContent);
79
+ writeFile('.env', envContent);
80
+
81
+ // 4. .gitignore
82
+ writeFile('.gitignore', `node_modules/
83
+ dist/
84
+ data.db
85
+ *.log
86
+ .env
87
+ .DS_Store
88
+ `);
89
+
90
+ // 5. README.md
91
+ writeFile('README.md', `# 🔥 ${projectName}
92
+
93
+ High-performance, modern Web Standard API built with **FireAPI**.
94
+
95
+ ## 🚀 Quick Start
96
+
97
+ \`\`\`bash
98
+ # 1. Install dependencies
99
+ bun install
100
+ # or
101
+ pnpm install
102
+
103
+ # 2. Run development server (with auto-reload)
104
+ bun dev
105
+
106
+ # 3. View Interactive API Docs
107
+ http://localhost:3000/docs
108
+
109
+ # 4. View Dev Dashboard (FireStudio)
110
+ http://localhost:3000/studio
111
+
112
+ # 5. Run Tests
113
+ bun test
114
+ \`\`\`
115
+ `);
116
+
117
+ // 6. src/index.ts
118
+ writeFile('src/index.ts', `import { createApp, enableStudio } from 'fire-api';
119
+ import { userRouter } from './modules/users/users.router';
120
+ import { authRouter } from './modules/auth/auth.router';
121
+
122
+ const port = Number(process.env.PORT) || 3000;
123
+
124
+ const app = createApp({
125
+ port,
126
+ database: {
127
+ driver: (process.env.DB_DRIVER as any) || 'sqlite',
128
+ url: process.env.DB_URL || ':memory:',
129
+ },
130
+ security: {
131
+ cors: true,
132
+ rateLimit: { max: 500, window: '1m' },
133
+ },
134
+ health: true, // Auto /healthz & /readyz
135
+ docs: '/docs', // Scalar Docs at /docs & OpenAPI at /openapi.json
136
+ });
137
+
138
+ // Enable Embedded Dev Studio at /studio
139
+ enableStudio(app, { path: '/studio' });
140
+
141
+ // Mount Domain Modules
142
+ app.use(authRouter);
143
+ app.use(userRouter);
144
+
145
+ // Start Server
146
+ app.listen(port, () => {
147
+ console.log(\`🔥 \${projectName} running at http://localhost:\${port}\`);
148
+ console.log(\`📖 Interactive Docs: http://localhost:\${port}/docs\`);
149
+ console.log(\`🖥️ FireStudio: http://localhost:\${port}/studio\`);
150
+ console.log(\`🩺 Health Probes: http://localhost:\${port}/healthz\`);
151
+ });
152
+
153
+ export type AppRouter = typeof app;
154
+ export default app;
155
+ `);
156
+
157
+ // 7. src/modules/users/users.schema.ts
158
+ writeFile('src/modules/users/users.schema.ts', `import { s, type Infer } from 'fire-api';
159
+
160
+ export const UserSchema = s.object({
161
+ id: s.string().optional(),
162
+ name: s.string().min(2).description('Full name of the user'),
163
+ email: s.string().email().description('Unique email address'),
164
+ role: s.enum(['admin', 'user', 'moderator']).default('user'),
165
+ createdAt: s.string().optional(),
166
+ });
167
+
168
+ export const CreateUserSchema = s.object({
169
+ name: s.string().min(2),
170
+ email: s.string().email(),
171
+ role: s.enum(['admin', 'user', 'moderator']).default('user'),
172
+ });
173
+
174
+ export type User = Infer<typeof UserSchema>;
175
+ export type CreateUserInput = Infer<typeof CreateUserSchema>;
176
+ `);
177
+
178
+ // 8. src/modules/users/users.router.ts
179
+ writeFile('src/modules/users/users.router.ts', `import { createRouter } from 'fire-api';
180
+ import { CreateUserSchema } from './users.schema';
181
+
182
+ export const userRouter = createRouter('/users');
183
+
184
+ // GET /users - List all users
185
+ userRouter.get('/', {
186
+ ai: { description: 'Retrieve all users', tags: ['Users'] },
187
+ handler: async (ctx) => {
188
+ const users = await ctx.db.users.find();
189
+ return ctx.ok(users);
190
+ },
191
+ });
192
+
193
+ // GET /users/:id - Get user by id with relational posts
194
+ userRouter.get('/:id', {
195
+ ai: { description: 'Get a user by ID', tags: ['Users'] },
196
+ handler: async (ctx) => {
197
+ const user = await ctx.db.users.include('posts').findById(ctx.params.id);
198
+ if (!user) return ctx.notFound('User not found');
199
+ return ctx.ok(user);
200
+ },
201
+ });
202
+
203
+ // POST /users - Create user with validation & idempotency shield
204
+ userRouter.post('/', {
205
+ schema: { body: CreateUserSchema },
206
+ idempotent: true,
207
+ ai: { description: 'Create a new user', tags: ['Users'] },
208
+ handler: async (ctx) => {
209
+ const existing = await ctx.db.users.findOne({ email: ctx.body.email });
210
+ if (existing) {
211
+ return ctx.badRequest('Email is already registered');
212
+ }
213
+
214
+ const newUser = await ctx.db.users.insert(ctx.body);
215
+ await ctx.emit('user.created', newUser);
216
+
217
+ return ctx.created(newUser);
218
+ },
219
+ });
220
+
221
+ // DELETE /users/:id - Delete user cleanly
222
+ userRouter.delete('/:id', {
223
+ ai: { description: 'Delete a user by ID', tags: ['Users'] },
224
+ handler: async (ctx) => {
225
+ await ctx.db.users.delete(ctx.params.id);
226
+ return ctx.ok({ deleted: true });
227
+ },
228
+ });
229
+ `);
230
+
231
+ // 9. src/modules/auth/auth.router.ts
232
+ writeFile('src/modules/auth/auth.router.ts', `import { createAuth } from 'fire-api';
233
+
234
+ // Turnkey Web Crypto PBKDF2 + JWT Authentication Sub-Router
235
+ export const authRouter = createAuth({
236
+ jwtSecret: process.env.JWT_SECRET || 'dev_secret_key_123',
237
+ tokenExpiresIn: '7d',
238
+ });
239
+ `);
240
+
241
+ // 10. test/app.test.ts
242
+ writeFile('test/app.test.ts', `import { describe, expect, it } from 'bun:test';
243
+ import app from '../src/index';
244
+
245
+ describe('${projectName} API E2E Suite', () => {
246
+ it('responds with 200 on /healthz probe', async () => {
247
+ const res = await app.handle(new Request('http://localhost:3000/healthz'));
248
+ expect(res.status).toBe(200);
249
+ const body = await res.json();
250
+ expect(body.status).toBe('ok');
251
+ });
252
+
253
+ it('creates and retrieves a user seamlessly', async () => {
254
+ // 1. Create User
255
+ const createRes = await app.handle(
256
+ new Request('http://localhost:3000/users', {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ 'X-Idempotency-Key': 'key-initial-admin',
261
+ },
262
+ body: JSON.stringify({
263
+ name: 'Super Admin',
264
+ email: 'admin@fireapi.dev',
265
+ role: 'admin',
266
+ }),
267
+ })
268
+ );
269
+
270
+ expect(createRes.status).toBe(201);
271
+ const createdUser = await createRes.json();
272
+ expect(createdUser.name).toBe('Super Admin');
273
+
274
+ // 2. Query Users
275
+ const listRes = await app.handle(new Request('http://localhost:3000/users'));
276
+ expect(listRes.status).toBe(200);
277
+ const users = await listRes.json();
278
+ expect(users.length).toBeGreaterThanOrEqual(1);
279
+ });
280
+ });
281
+ `);
282
+
283
+ return { projectDir: baseDir, filesCreated };
284
+ }
@@ -0,0 +1,51 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export function generateDockerFiles(baseDir = process.cwd()): { dockerfilePath: string; dockerignorePath: string } {
5
+ const dockerfilePath = path.join(baseDir, 'Dockerfile');
6
+ const dockerignorePath = path.join(baseDir, '.dockerignore');
7
+
8
+ const dockerfileContent = `# ==============================================
9
+ # 🔥 FireAPI Ultra-Lightweight Production Container (< 25MB)
10
+ # ==============================================
11
+
12
+ # Stage 1: Build & Dependencies
13
+ FROM oven/bun:alpine AS base
14
+ WORKDIR /app
15
+
16
+ RUN addgroup -S appgroup && adduser -S appuser -G appgroup
17
+
18
+ COPY package.json bun.lock* ./
19
+ RUN bun install --production
20
+
21
+ COPY . .
22
+ RUN chown -R appuser:appgroup /app
23
+
24
+ # Stage 2: Distroless Production Runner
25
+ FROM oven/bun:alpine AS runner
26
+ WORKDIR /app
27
+
28
+ COPY --from=base /app /app
29
+
30
+ USER appuser
31
+ EXPOSE 3000
32
+
33
+ ENV NODE_ENV=production
34
+ ENV PORT=3000
35
+
36
+ CMD ["bun", "run", "src/index.ts"]
37
+ `;
38
+
39
+ const dockerignoreContent = `node_modules
40
+ dist
41
+ .git
42
+ .env*
43
+ *.log
44
+ uploads
45
+ `;
46
+
47
+ fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf-8');
48
+ fs.writeFileSync(dockerignorePath, dockerignoreContent, 'utf-8');
49
+
50
+ return { dockerfilePath, dockerignorePath };
51
+ }
@@ -0,0 +1,75 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export function generateResource(resourceName: string, baseDir = process.cwd()): { routeFile: string; schemaFile: string } {
5
+ const cleanName = resourceName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
6
+ const capitalized = cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
7
+ const targetDir = path.join(baseDir, 'src', 'modules', cleanName);
8
+
9
+ if (!fs.existsSync(targetDir)) {
10
+ fs.mkdirSync(targetDir, { recursive: true });
11
+ }
12
+
13
+ const schemaPath = path.join(targetDir, `${cleanName}.schema.ts`);
14
+ const routePath = path.join(targetDir, `${cleanName}.route.ts`);
15
+
16
+ const schemaContent = `import { s } from 'fire-api';
17
+
18
+ export const Create${capitalized}Schema = s.object({
19
+ name: s.string().min(2).description('${capitalized} title or name'),
20
+ status: s.enum(['active', 'draft']).default('active'),
21
+ });
22
+
23
+ export const ${capitalized}IdSchema = s.object({
24
+ id: s.string().min(1),
25
+ });
26
+ `;
27
+
28
+ const routeContent = `import { createRouter } from 'fire-api';
29
+ import { Create${capitalized}Schema, ${capitalized}IdSchema } from './${cleanName}.schema';
30
+
31
+ export const ${cleanName}Router = createRouter('/${cleanName}s');
32
+
33
+ // GET /${cleanName}s
34
+ ${cleanName}Router.get('/', {
35
+ cache: { ttl: 60, tags: ['${cleanName}s'] },
36
+ ai: { description: 'Fetch list of ${cleanName}s', tags: ['${capitalized}'] },
37
+ handler: async (ctx) => {
38
+ return ctx.db.${cleanName}s.find();
39
+ },
40
+ });
41
+
42
+ // POST /${cleanName}s
43
+ ${cleanName}Router.post('/', {
44
+ schema: { body: Create${capitalized}Schema },
45
+ ai: { description: 'Create a new ${cleanName}', tags: ['${capitalized}'] },
46
+ handler: async (ctx) => {
47
+ const created = await ctx.db.${cleanName}s.insert(ctx.body);
48
+ await ctx.cache.invalidate('${cleanName}s');
49
+ return ctx.created(created);
50
+ },
51
+ });
52
+
53
+ // GET /${cleanName}s/:id
54
+ ${cleanName}Router.get('/:id', {
55
+ schema: { params: ${capitalized}IdSchema },
56
+ ai: { description: 'Get a ${cleanName} by unique ID', tags: ['${capitalized}'] },
57
+ handler: async (ctx) => {
58
+ const item = await ctx.db.${cleanName}s.findById(ctx.params.id);
59
+ if (!item) return ctx.notFound('${capitalized} not found');
60
+ return ctx.ok(item);
61
+ },
62
+ });
63
+
64
+ // DELETE /${cleanName}s/:id
65
+ ${cleanName}Router.delete('/:id', async (ctx) => {
66
+ await ctx.db.${cleanName}s.delete(ctx.params.id);
67
+ return { deleted: true };
68
+ });
69
+ `;
70
+
71
+ fs.writeFileSync(schemaPath, schemaContent, 'utf-8');
72
+ fs.writeFileSync(routePath, routeContent, 'utf-8');
73
+
74
+ return { routeFile: routePath, schemaFile: schemaPath };
75
+ }
@@ -0,0 +1,76 @@
1
+ import { Router } from '../core/router';
2
+
3
+ export interface RouteTableRow {
4
+ method: string;
5
+ path: string;
6
+ auth: string;
7
+ roles: string;
8
+ cache: string;
9
+ idempotent: string;
10
+ description: string;
11
+ }
12
+
13
+ export function formatRoutesTable(router: Router): string {
14
+ const routes = router.getRoutes();
15
+
16
+ if (routes.length === 0) {
17
+ return 'No routes registered in application.';
18
+ }
19
+
20
+ const rows: RouteTableRow[] = routes.map((r) => {
21
+ const roles = r.options.roles && r.options.roles.length > 0 ? r.options.roles.join(', ') : '-';
22
+ const cache = r.options.cache ? `${r.options.cache.ttl}s` : '-';
23
+ const idempotent = r.options.idempotent ? '✓' : '-';
24
+ const auth = r.options.security ? 'Required' : (r.options.roles ? 'Role-Guarded' : 'Public');
25
+ const description = r.options.ai?.description || '-';
26
+
27
+ return {
28
+ method: r.method,
29
+ path: r.path,
30
+ auth,
31
+ roles,
32
+ cache,
33
+ idempotent,
34
+ description,
35
+ };
36
+ });
37
+
38
+ // Calculate column widths
39
+ const headers = {
40
+ method: 'METHOD',
41
+ path: 'ROUTE PATH',
42
+ auth: 'AUTH',
43
+ roles: 'ROLES',
44
+ cache: 'CACHE',
45
+ idempotent: 'IDEMPOTENT',
46
+ description: 'DESCRIPTION',
47
+ };
48
+
49
+ const colWidths = {
50
+ method: Math.max(headers.method.length, ...rows.map((r) => r.method.length)),
51
+ path: Math.max(headers.path.length, ...rows.map((r) => r.path.length)),
52
+ auth: Math.max(headers.auth.length, ...rows.map((r) => r.auth.length)),
53
+ roles: Math.max(headers.roles.length, ...rows.map((r) => r.roles.length)),
54
+ cache: Math.max(headers.cache.length, ...rows.map((r) => r.cache.length)),
55
+ idempotent: Math.max(headers.idempotent.length, ...rows.map((r) => r.idempotent.length)),
56
+ description: Math.max(headers.description.length, ...rows.map((r) => r.description.length)),
57
+ };
58
+
59
+ const pad = (str: string, len: number) => str.padEnd(len, ' ');
60
+
61
+ const headerLine = `| ${pad(headers.method, colWidths.method)} | ${pad(headers.path, colWidths.path)} | ${pad(headers.auth, colWidths.auth)} | ${pad(headers.roles, colWidths.roles)} | ${pad(headers.cache, colWidths.cache)} | ${pad(headers.idempotent, colWidths.idempotent)} | ${pad(headers.description, colWidths.description)} |`;
62
+ const sepLine = `|-${'-'.repeat(colWidths.method)}-|-${'-'.repeat(colWidths.path)}-|-${'-'.repeat(colWidths.auth)}-|-${'-'.repeat(colWidths.roles)}-|-${'-'.repeat(colWidths.cache)}-|-${'-'.repeat(colWidths.idempotent)}-|-${'-'.repeat(colWidths.description)}-|`;
63
+
64
+ const bodyLines = rows.map((r) =>
65
+ `| ${pad(r.method, colWidths.method)} | ${pad(r.path, colWidths.path)} | ${pad(r.auth, colWidths.auth)} | ${pad(r.roles, colWidths.roles)} | ${pad(r.cache, colWidths.cache)} | ${pad(r.idempotent, colWidths.idempotent)} | ${pad(r.description, colWidths.description)} |`
66
+ );
67
+
68
+ return [
69
+ `🔥 FireAPI Route Manifest (${routes.length} total endpoints):`,
70
+ sepLine,
71
+ headerLine,
72
+ sepLine,
73
+ ...bodyLines,
74
+ sepLine,
75
+ ].join('\n');
76
+ }
package/src/client.ts ADDED
@@ -0,0 +1,120 @@
1
+ export interface ClientRequestOptions {
2
+ params?: Record<string, string | number>;
3
+ query?: Record<string, any>;
4
+ body?: any;
5
+ headers?: HeadersInit;
6
+ }
7
+
8
+ export interface ClientResponse<T = any> {
9
+ ok: boolean;
10
+ status: number;
11
+ data: T;
12
+ error?: any;
13
+ headers: Headers;
14
+ }
15
+
16
+ export interface ClientOptions {
17
+ fetch?: typeof fetch;
18
+ headers?: HeadersInit;
19
+ }
20
+
21
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
22
+
23
+ function buildUrl(baseUrl: string, segments: string[], options?: ClientRequestOptions): string {
24
+ let path = segments.join('/');
25
+
26
+ // Substitute path params if any segment is :param or contains :param
27
+ if (options?.params) {
28
+ for (const [key, val] of Object.entries(options.params)) {
29
+ path = path.replace(new RegExp(`:${key}\\b`, 'g'), encodeURIComponent(String(val)));
30
+ }
31
+ }
32
+
33
+ // Remove any remaining raw :param if not replaced
34
+ const cleanBase = baseUrl.replace(/\/+$/, '');
35
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
36
+ let fullUrl = `${cleanBase}${cleanPath}`;
37
+
38
+ if (options?.query) {
39
+ const searchParams = new URLSearchParams();
40
+ for (const [k, v] of Object.entries(options.query)) {
41
+ if (v !== undefined && v !== null) {
42
+ if (Array.isArray(v)) {
43
+ v.forEach((item) => searchParams.append(k, String(item)));
44
+ } else {
45
+ searchParams.append(k, String(v));
46
+ }
47
+ }
48
+ }
49
+ const queryStr = searchParams.toString();
50
+ if (queryStr) {
51
+ fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryStr;
52
+ }
53
+ }
54
+
55
+ return fullUrl;
56
+ }
57
+
58
+ export function createClient<TApp = any>(baseUrl = '', clientOptions: ClientOptions = {}): any {
59
+ const fetchFn = clientOptions.fetch || globalThis.fetch;
60
+
61
+ function createProxy(pathSegments: string[]): any {
62
+ return new Proxy(() => {}, {
63
+ get(_target, prop: string) {
64
+ const lowerProp = prop.toLowerCase();
65
+ if (HTTP_METHODS.has(lowerProp)) {
66
+ // This is the terminal HTTP method call, e.g. .get({ params, query, body })
67
+ return async (reqOptions?: ClientRequestOptions): Promise<ClientResponse> => {
68
+ const url = buildUrl(baseUrl, pathSegments, reqOptions);
69
+ const headers = new Headers(clientOptions.headers);
70
+ if (reqOptions?.headers) {
71
+ const extraHeaders = new Headers(reqOptions.headers);
72
+ extraHeaders.forEach((v, k) => headers.set(k, v));
73
+ }
74
+
75
+ let bodyContent: BodyInit | undefined;
76
+ if (reqOptions?.body !== undefined && lowerProp !== 'get' && lowerProp !== 'head') {
77
+ if (!headers.has('Content-Type')) {
78
+ headers.set('Content-Type', 'application/json');
79
+ }
80
+ bodyContent = typeof reqOptions.body === 'string'
81
+ ? reqOptions.body
82
+ : JSON.stringify(reqOptions.body);
83
+ }
84
+
85
+ const response = await fetchFn(url, {
86
+ method: lowerProp.toUpperCase(),
87
+ headers,
88
+ body: bodyContent,
89
+ });
90
+
91
+ let responseData: any = null;
92
+ const contentType = response.headers.get('content-type') || '';
93
+ if (contentType.includes('application/json')) {
94
+ try {
95
+ responseData = await response.json();
96
+ } catch {
97
+ responseData = null;
98
+ }
99
+ } else {
100
+ responseData = await response.text();
101
+ }
102
+
103
+ return {
104
+ ok: response.ok,
105
+ status: response.status,
106
+ data: responseData,
107
+ error: response.ok ? undefined : responseData,
108
+ headers: response.headers,
109
+ };
110
+ };
111
+ }
112
+
113
+ // Sub-path traversal
114
+ return createProxy([...pathSegments, prop]);
115
+ },
116
+ });
117
+ }
118
+
119
+ return createProxy([]);
120
+ }