@tamayrp14/fire-api 1.0.0 → 1.0.1
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/bin/fire.mjs +670 -0
- package/package.json +4 -4
package/bin/fire.mjs
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/fire.js
|
|
4
|
+
import * as path5 from "path";
|
|
5
|
+
|
|
6
|
+
// src/cli/generator.ts
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
function generateResource(resourceName, baseDir = process.cwd()) {
|
|
10
|
+
const cleanName = resourceName.toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
11
|
+
const capitalized = cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
|
12
|
+
const targetDir = path.join(baseDir, "src", "modules", cleanName);
|
|
13
|
+
if (!fs.existsSync(targetDir)) {
|
|
14
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
const schemaPath = path.join(targetDir, `${cleanName}.schema.ts`);
|
|
17
|
+
const routePath = path.join(targetDir, `${cleanName}.route.ts`);
|
|
18
|
+
const schemaContent = `import { s } from 'fire-api';
|
|
19
|
+
|
|
20
|
+
export const Create${capitalized}Schema = s.object({
|
|
21
|
+
name: s.string().min(2).description('${capitalized} title or name'),
|
|
22
|
+
status: s.enum(['active', 'draft']).default('active'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const ${capitalized}IdSchema = s.object({
|
|
26
|
+
id: s.string().min(1),
|
|
27
|
+
});
|
|
28
|
+
`;
|
|
29
|
+
const routeContent = `import { createRouter } from 'fire-api';
|
|
30
|
+
import { Create${capitalized}Schema, ${capitalized}IdSchema } from './${cleanName}.schema';
|
|
31
|
+
|
|
32
|
+
export const ${cleanName}Router = createRouter('/${cleanName}s');
|
|
33
|
+
|
|
34
|
+
// GET /${cleanName}s
|
|
35
|
+
${cleanName}Router.get('/', {
|
|
36
|
+
cache: { ttl: 60, tags: ['${cleanName}s'] },
|
|
37
|
+
ai: { description: 'Fetch list of ${cleanName}s', tags: ['${capitalized}'] },
|
|
38
|
+
handler: async (ctx) => {
|
|
39
|
+
return ctx.db.${cleanName}s.find();
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// POST /${cleanName}s
|
|
44
|
+
${cleanName}Router.post('/', {
|
|
45
|
+
schema: { body: Create${capitalized}Schema },
|
|
46
|
+
ai: { description: 'Create a new ${cleanName}', tags: ['${capitalized}'] },
|
|
47
|
+
handler: async (ctx) => {
|
|
48
|
+
const created = await ctx.db.${cleanName}s.insert(ctx.body);
|
|
49
|
+
await ctx.cache.invalidate('${cleanName}s');
|
|
50
|
+
return ctx.created(created);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// GET /${cleanName}s/:id
|
|
55
|
+
${cleanName}Router.get('/:id', {
|
|
56
|
+
schema: { params: ${capitalized}IdSchema },
|
|
57
|
+
ai: { description: 'Get a ${cleanName} by unique ID', tags: ['${capitalized}'] },
|
|
58
|
+
handler: async (ctx) => {
|
|
59
|
+
const item = await ctx.db.${cleanName}s.findById(ctx.params.id);
|
|
60
|
+
if (!item) return ctx.notFound('${capitalized} not found');
|
|
61
|
+
return ctx.ok(item);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// DELETE /${cleanName}s/:id
|
|
66
|
+
${cleanName}Router.delete('/:id', async (ctx) => {
|
|
67
|
+
await ctx.db.${cleanName}s.delete(ctx.params.id);
|
|
68
|
+
return { deleted: true };
|
|
69
|
+
});
|
|
70
|
+
`;
|
|
71
|
+
fs.writeFileSync(schemaPath, schemaContent, "utf-8");
|
|
72
|
+
fs.writeFileSync(routePath, routeContent, "utf-8");
|
|
73
|
+
return { routeFile: routePath, schemaFile: schemaPath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/cli/bench.ts
|
|
77
|
+
function percentile(sorted, p) {
|
|
78
|
+
if (sorted.length === 0)
|
|
79
|
+
return 0;
|
|
80
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
81
|
+
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
|
|
82
|
+
}
|
|
83
|
+
async function runBenchmark(url, options = {}) {
|
|
84
|
+
const concurrency = options.concurrency || 10;
|
|
85
|
+
const total = options.totalRequests || 200;
|
|
86
|
+
const fetchFn = options.customFetch || globalThis.fetch;
|
|
87
|
+
const latencies = [];
|
|
88
|
+
let successfulRequests = 0;
|
|
89
|
+
let failedRequests = 0;
|
|
90
|
+
let sent = 0;
|
|
91
|
+
const startTime = performance.now();
|
|
92
|
+
async function worker() {
|
|
93
|
+
while (sent < total) {
|
|
94
|
+
sent++;
|
|
95
|
+
const reqStart = performance.now();
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetchFn(url);
|
|
98
|
+
const reqDuration = performance.now() - reqStart;
|
|
99
|
+
latencies.push(reqDuration);
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
successfulRequests++;
|
|
102
|
+
} else {
|
|
103
|
+
failedRequests++;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
const reqDuration = performance.now() - reqStart;
|
|
107
|
+
latencies.push(reqDuration);
|
|
108
|
+
failedRequests++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
113
|
+
await Promise.all(workers);
|
|
114
|
+
const durationMs = performance.now() - startTime;
|
|
115
|
+
const rps = Math.round(latencies.length / (durationMs / 1000) * 100) / 100;
|
|
116
|
+
latencies.sort((a, b) => a - b);
|
|
117
|
+
const sum = latencies.reduce((acc, val) => acc + val, 0);
|
|
118
|
+
return {
|
|
119
|
+
url,
|
|
120
|
+
totalRequests: latencies.length,
|
|
121
|
+
successfulRequests,
|
|
122
|
+
failedRequests,
|
|
123
|
+
durationMs: Math.round(durationMs),
|
|
124
|
+
rps,
|
|
125
|
+
latency: {
|
|
126
|
+
min: Math.round((latencies[0] || 0) * 100) / 100,
|
|
127
|
+
max: Math.round((latencies[latencies.length - 1] || 0) * 100) / 100,
|
|
128
|
+
avg: Math.round(sum / (latencies.length || 1) * 100) / 100,
|
|
129
|
+
p50: Math.round(percentile(latencies, 50) * 100) / 100,
|
|
130
|
+
p90: Math.round(percentile(latencies, 90) * 100) / 100,
|
|
131
|
+
p95: Math.round(percentile(latencies, 95) * 100) / 100,
|
|
132
|
+
p99: Math.round(percentile(latencies, 99) * 100) / 100
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function formatBenchmarkReport(stats) {
|
|
137
|
+
const successRate = stats.totalRequests > 0 ? (stats.successfulRequests / stats.totalRequests * 100).toFixed(1) : "0";
|
|
138
|
+
return `
|
|
139
|
+
==================================================
|
|
140
|
+
\uD83D\uDD25 FireAPI Benchmark Engine
|
|
141
|
+
==================================================
|
|
142
|
+
Target: ${stats.url}
|
|
143
|
+
Duration: ${stats.durationMs} ms
|
|
144
|
+
Total Reqs: ${stats.totalRequests}
|
|
145
|
+
Success Rate: ${successRate}% (${stats.successfulRequests} ok / ${stats.failedRequests} fail)
|
|
146
|
+
|
|
147
|
+
\uD83D\uDE80 Requests/sec: ${stats.rps.toLocaleString()} req/s
|
|
148
|
+
|
|
149
|
+
Latency (ms):
|
|
150
|
+
Min: ${stats.latency.min} ms
|
|
151
|
+
P50: ${stats.latency.p50} ms
|
|
152
|
+
P90: ${stats.latency.p90} ms
|
|
153
|
+
P95: ${stats.latency.p95} ms
|
|
154
|
+
P99: ${stats.latency.p99} ms
|
|
155
|
+
Max: ${stats.latency.max} ms
|
|
156
|
+
==================================================
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/cli/docker.ts
|
|
161
|
+
import * as fs2 from "node:fs";
|
|
162
|
+
import * as path2 from "node:path";
|
|
163
|
+
function generateDockerFiles(baseDir = process.cwd()) {
|
|
164
|
+
const dockerfilePath = path2.join(baseDir, "Dockerfile");
|
|
165
|
+
const dockerignorePath = path2.join(baseDir, ".dockerignore");
|
|
166
|
+
const dockerfileContent = `# ==============================================
|
|
167
|
+
# \uD83D\uDD25 FireAPI Ultra-Lightweight Production Container (< 25MB)
|
|
168
|
+
# ==============================================
|
|
169
|
+
|
|
170
|
+
# Stage 1: Build & Dependencies
|
|
171
|
+
FROM oven/bun:alpine AS base
|
|
172
|
+
WORKDIR /app
|
|
173
|
+
|
|
174
|
+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
|
175
|
+
|
|
176
|
+
COPY package.json bun.lock* ./
|
|
177
|
+
RUN bun install --production
|
|
178
|
+
|
|
179
|
+
COPY . .
|
|
180
|
+
RUN chown -R appuser:appgroup /app
|
|
181
|
+
|
|
182
|
+
# Stage 2: Distroless Production Runner
|
|
183
|
+
FROM oven/bun:alpine AS runner
|
|
184
|
+
WORKDIR /app
|
|
185
|
+
|
|
186
|
+
COPY --from=base /app /app
|
|
187
|
+
|
|
188
|
+
USER appuser
|
|
189
|
+
EXPOSE 3000
|
|
190
|
+
|
|
191
|
+
ENV NODE_ENV=production
|
|
192
|
+
ENV PORT=3000
|
|
193
|
+
|
|
194
|
+
CMD ["bun", "run", "src/index.ts"]
|
|
195
|
+
`;
|
|
196
|
+
const dockerignoreContent = `node_modules
|
|
197
|
+
dist
|
|
198
|
+
.git
|
|
199
|
+
.env*
|
|
200
|
+
*.log
|
|
201
|
+
uploads
|
|
202
|
+
`;
|
|
203
|
+
fs2.writeFileSync(dockerfilePath, dockerfileContent, "utf-8");
|
|
204
|
+
fs2.writeFileSync(dockerignorePath, dockerignoreContent, "utf-8");
|
|
205
|
+
return { dockerfilePath, dockerignorePath };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/cli/compile.ts
|
|
209
|
+
import { execSync } from "child_process";
|
|
210
|
+
import * as fs3 from "fs";
|
|
211
|
+
import * as path3 from "path";
|
|
212
|
+
function compileBinary(options = {}) {
|
|
213
|
+
const entry = options.entry || (fs3.existsSync("./src/index.ts") ? "./src/index.ts" : "./index.ts");
|
|
214
|
+
const output = options.output || "./dist/server";
|
|
215
|
+
const minifyFlag = options.minify !== false ? "--minify" : "";
|
|
216
|
+
const outDir = path3.dirname(output);
|
|
217
|
+
if (!fs3.existsSync(outDir)) {
|
|
218
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
const command = `bun build ${entry} --compile ${minifyFlag} --outfile ${output}`.trim();
|
|
221
|
+
try {
|
|
222
|
+
const stdout = execSync(command, { encoding: "utf-8", stdio: "pipe" });
|
|
223
|
+
return { command, success: true, output: stdout };
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { command, success: false, output: err.message };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/cli/create-app.ts
|
|
230
|
+
import * as fs4 from "fs";
|
|
231
|
+
import * as path4 from "path";
|
|
232
|
+
function createAppProject(options) {
|
|
233
|
+
const projectName = options.name || "my-fire-api";
|
|
234
|
+
const baseDir = options.targetDir || path4.resolve(process.cwd(), projectName);
|
|
235
|
+
if (!fs4.existsSync(baseDir)) {
|
|
236
|
+
fs4.mkdirSync(baseDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
const filesCreated = [];
|
|
239
|
+
const writeFile = (relPath, content) => {
|
|
240
|
+
const fullPath = path4.join(baseDir, relPath);
|
|
241
|
+
const dir = path4.dirname(fullPath);
|
|
242
|
+
if (!fs4.existsSync(dir)) {
|
|
243
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
fs4.writeFileSync(fullPath, content.trim() + `
|
|
246
|
+
`, "utf-8");
|
|
247
|
+
filesCreated.push(relPath);
|
|
248
|
+
};
|
|
249
|
+
writeFile("package.json", JSON.stringify({
|
|
250
|
+
name: projectName,
|
|
251
|
+
version: "1.0.0",
|
|
252
|
+
description: "High-performance API built with FireAPI",
|
|
253
|
+
type: "module",
|
|
254
|
+
main: "src/index.ts",
|
|
255
|
+
scripts: {
|
|
256
|
+
dev: "bun --watch src/index.ts",
|
|
257
|
+
start: "bun src/index.ts",
|
|
258
|
+
test: "bun test",
|
|
259
|
+
compile: "fire compile ./dist/server",
|
|
260
|
+
docker: "fire docker",
|
|
261
|
+
routes: "fire routes"
|
|
262
|
+
},
|
|
263
|
+
dependencies: {
|
|
264
|
+
"fire-api": "^1.0.0"
|
|
265
|
+
},
|
|
266
|
+
devDependencies: {
|
|
267
|
+
"@types/node": "^20.0.0",
|
|
268
|
+
typescript: "^5.0.0"
|
|
269
|
+
}
|
|
270
|
+
}, null, 2));
|
|
271
|
+
writeFile("tsconfig.json", JSON.stringify({
|
|
272
|
+
compilerOptions: {
|
|
273
|
+
target: "ESNext",
|
|
274
|
+
module: "ESNext",
|
|
275
|
+
moduleResolution: "bundler",
|
|
276
|
+
strict: true,
|
|
277
|
+
skipLibCheck: true,
|
|
278
|
+
esModuleInterop: true,
|
|
279
|
+
allowSyntheticDefaultImports: true,
|
|
280
|
+
forceConsistentCasingInFileNames: true,
|
|
281
|
+
outDir: "./dist"
|
|
282
|
+
},
|
|
283
|
+
include: ["src/**/*", "test/**/*"]
|
|
284
|
+
}, null, 2));
|
|
285
|
+
const envContent = `PORT=3000
|
|
286
|
+
NODE_ENV=development
|
|
287
|
+
JWT_SECRET=super_secret_jwt_key_change_me_in_prod
|
|
288
|
+
DB_DRIVER=sqlite
|
|
289
|
+
DB_URL=./data.db
|
|
290
|
+
CORS_ORIGIN=*
|
|
291
|
+
`;
|
|
292
|
+
writeFile(".env.example", envContent);
|
|
293
|
+
writeFile(".env", envContent);
|
|
294
|
+
writeFile(".gitignore", `node_modules/
|
|
295
|
+
dist/
|
|
296
|
+
data.db
|
|
297
|
+
*.log
|
|
298
|
+
.env
|
|
299
|
+
.DS_Store
|
|
300
|
+
`);
|
|
301
|
+
writeFile("README.md", `# \uD83D\uDD25 ${projectName}
|
|
302
|
+
|
|
303
|
+
High-performance, modern Web Standard API built with **FireAPI**.
|
|
304
|
+
|
|
305
|
+
## \uD83D\uDE80 Quick Start
|
|
306
|
+
|
|
307
|
+
\`\`\`bash
|
|
308
|
+
# 1. Install dependencies
|
|
309
|
+
bun install
|
|
310
|
+
# or
|
|
311
|
+
pnpm install
|
|
312
|
+
|
|
313
|
+
# 2. Run development server (with auto-reload)
|
|
314
|
+
bun dev
|
|
315
|
+
|
|
316
|
+
# 3. View Interactive API Docs
|
|
317
|
+
http://localhost:3000/docs
|
|
318
|
+
|
|
319
|
+
# 4. View Dev Dashboard (FireStudio)
|
|
320
|
+
http://localhost:3000/studio
|
|
321
|
+
|
|
322
|
+
# 5. Run Tests
|
|
323
|
+
bun test
|
|
324
|
+
\`\`\`
|
|
325
|
+
`);
|
|
326
|
+
writeFile("src/index.ts", `import { createApp, enableStudio } from 'fire-api';
|
|
327
|
+
import { userRouter } from './modules/users/users.router';
|
|
328
|
+
import { authRouter } from './modules/auth/auth.router';
|
|
329
|
+
|
|
330
|
+
const port = Number(process.env.PORT) || 3000;
|
|
331
|
+
|
|
332
|
+
const app = createApp({
|
|
333
|
+
port,
|
|
334
|
+
database: {
|
|
335
|
+
driver: (process.env.DB_DRIVER as any) || 'sqlite',
|
|
336
|
+
url: process.env.DB_URL || ':memory:',
|
|
337
|
+
},
|
|
338
|
+
security: {
|
|
339
|
+
cors: true,
|
|
340
|
+
rateLimit: { max: 500, window: '1m' },
|
|
341
|
+
},
|
|
342
|
+
health: true, // Auto /healthz & /readyz
|
|
343
|
+
docs: '/docs', // Scalar Docs at /docs & OpenAPI at /openapi.json
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Enable Embedded Dev Studio at /studio
|
|
347
|
+
enableStudio(app, { path: '/studio' });
|
|
348
|
+
|
|
349
|
+
// Mount Domain Modules
|
|
350
|
+
app.use(authRouter);
|
|
351
|
+
app.use(userRouter);
|
|
352
|
+
|
|
353
|
+
// Start Server
|
|
354
|
+
app.listen(port, () => {
|
|
355
|
+
console.log(\`\uD83D\uDD25 \${projectName} running at http://localhost:\${port}\`);
|
|
356
|
+
console.log(\`\uD83D\uDCD6 Interactive Docs: http://localhost:\${port}/docs\`);
|
|
357
|
+
console.log(\`\uD83D\uDDA5️ FireStudio: http://localhost:\${port}/studio\`);
|
|
358
|
+
console.log(\`\uD83E\uDE7A Health Probes: http://localhost:\${port}/healthz\`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
export type AppRouter = typeof app;
|
|
362
|
+
export default app;
|
|
363
|
+
`);
|
|
364
|
+
writeFile("src/modules/users/users.schema.ts", `import { s, type Infer } from 'fire-api';
|
|
365
|
+
|
|
366
|
+
export const UserSchema = s.object({
|
|
367
|
+
id: s.string().optional(),
|
|
368
|
+
name: s.string().min(2).description('Full name of the user'),
|
|
369
|
+
email: s.string().email().description('Unique email address'),
|
|
370
|
+
role: s.enum(['admin', 'user', 'moderator']).default('user'),
|
|
371
|
+
createdAt: s.string().optional(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
export const CreateUserSchema = s.object({
|
|
375
|
+
name: s.string().min(2),
|
|
376
|
+
email: s.string().email(),
|
|
377
|
+
role: s.enum(['admin', 'user', 'moderator']).default('user'),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
export type User = Infer<typeof UserSchema>;
|
|
381
|
+
export type CreateUserInput = Infer<typeof CreateUserSchema>;
|
|
382
|
+
`);
|
|
383
|
+
writeFile("src/modules/users/users.router.ts", `import { createRouter } from 'fire-api';
|
|
384
|
+
import { CreateUserSchema } from './users.schema';
|
|
385
|
+
|
|
386
|
+
export const userRouter = createRouter('/users');
|
|
387
|
+
|
|
388
|
+
// GET /users - List all users
|
|
389
|
+
userRouter.get('/', {
|
|
390
|
+
ai: { description: 'Retrieve all users', tags: ['Users'] },
|
|
391
|
+
handler: async (ctx) => {
|
|
392
|
+
const users = await ctx.db.users.find();
|
|
393
|
+
return ctx.ok(users);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// GET /users/:id - Get user by id with relational posts
|
|
398
|
+
userRouter.get('/:id', {
|
|
399
|
+
ai: { description: 'Get a user by ID', tags: ['Users'] },
|
|
400
|
+
handler: async (ctx) => {
|
|
401
|
+
const user = await ctx.db.users.include('posts').findById(ctx.params.id);
|
|
402
|
+
if (!user) return ctx.notFound('User not found');
|
|
403
|
+
return ctx.ok(user);
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// POST /users - Create user with validation & idempotency shield
|
|
408
|
+
userRouter.post('/', {
|
|
409
|
+
schema: { body: CreateUserSchema },
|
|
410
|
+
idempotent: true,
|
|
411
|
+
ai: { description: 'Create a new user', tags: ['Users'] },
|
|
412
|
+
handler: async (ctx) => {
|
|
413
|
+
const existing = await ctx.db.users.findOne({ email: ctx.body.email });
|
|
414
|
+
if (existing) {
|
|
415
|
+
return ctx.badRequest('Email is already registered');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const newUser = await ctx.db.users.insert(ctx.body);
|
|
419
|
+
await ctx.emit('user.created', newUser);
|
|
420
|
+
|
|
421
|
+
return ctx.created(newUser);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// DELETE /users/:id - Delete user cleanly
|
|
426
|
+
userRouter.delete('/:id', {
|
|
427
|
+
ai: { description: 'Delete a user by ID', tags: ['Users'] },
|
|
428
|
+
handler: async (ctx) => {
|
|
429
|
+
await ctx.db.users.delete(ctx.params.id);
|
|
430
|
+
return ctx.ok({ deleted: true });
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
`);
|
|
434
|
+
writeFile("src/modules/auth/auth.router.ts", `import { createAuth } from 'fire-api';
|
|
435
|
+
|
|
436
|
+
// Turnkey Web Crypto PBKDF2 + JWT Authentication Sub-Router
|
|
437
|
+
export const authRouter = createAuth({
|
|
438
|
+
jwtSecret: process.env.JWT_SECRET || 'dev_secret_key_123',
|
|
439
|
+
tokenExpiresIn: '7d',
|
|
440
|
+
});
|
|
441
|
+
`);
|
|
442
|
+
writeFile("test/app.test.ts", `import { describe, expect, it } from 'bun:test';
|
|
443
|
+
import app from '../src/index';
|
|
444
|
+
|
|
445
|
+
describe('${projectName} API E2E Suite', () => {
|
|
446
|
+
it('responds with 200 on /healthz probe', async () => {
|
|
447
|
+
const res = await app.handle(new Request('http://localhost:3000/healthz'));
|
|
448
|
+
expect(res.status).toBe(200);
|
|
449
|
+
const body = await res.json();
|
|
450
|
+
expect(body.status).toBe('ok');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('creates and retrieves a user seamlessly', async () => {
|
|
454
|
+
// 1. Create User
|
|
455
|
+
const createRes = await app.handle(
|
|
456
|
+
new Request('http://localhost:3000/users', {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: {
|
|
459
|
+
'Content-Type': 'application/json',
|
|
460
|
+
'X-Idempotency-Key': 'key-initial-admin',
|
|
461
|
+
},
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
name: 'Super Admin',
|
|
464
|
+
email: 'admin@fireapi.dev',
|
|
465
|
+
role: 'admin',
|
|
466
|
+
}),
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(createRes.status).toBe(201);
|
|
471
|
+
const createdUser = await createRes.json();
|
|
472
|
+
expect(createdUser.name).toBe('Super Admin');
|
|
473
|
+
|
|
474
|
+
// 2. Query Users
|
|
475
|
+
const listRes = await app.handle(new Request('http://localhost:3000/users'));
|
|
476
|
+
expect(listRes.status).toBe(200);
|
|
477
|
+
const users = await listRes.json();
|
|
478
|
+
expect(users.length).toBeGreaterThanOrEqual(1);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
`);
|
|
482
|
+
return { projectDir: baseDir, filesCreated };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/cli/routes.ts
|
|
486
|
+
function formatRoutesTable(router) {
|
|
487
|
+
const routes = router.getRoutes();
|
|
488
|
+
if (routes.length === 0) {
|
|
489
|
+
return "No routes registered in application.";
|
|
490
|
+
}
|
|
491
|
+
const rows = routes.map((r) => {
|
|
492
|
+
const roles = r.options.roles && r.options.roles.length > 0 ? r.options.roles.join(", ") : "-";
|
|
493
|
+
const cache = r.options.cache ? `${r.options.cache.ttl}s` : "-";
|
|
494
|
+
const idempotent = r.options.idempotent ? "✓" : "-";
|
|
495
|
+
const auth = r.options.security ? "Required" : r.options.roles ? "Role-Guarded" : "Public";
|
|
496
|
+
const description = r.options.ai?.description || "-";
|
|
497
|
+
return {
|
|
498
|
+
method: r.method,
|
|
499
|
+
path: r.path,
|
|
500
|
+
auth,
|
|
501
|
+
roles,
|
|
502
|
+
cache,
|
|
503
|
+
idempotent,
|
|
504
|
+
description
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
const headers = {
|
|
508
|
+
method: "METHOD",
|
|
509
|
+
path: "ROUTE PATH",
|
|
510
|
+
auth: "AUTH",
|
|
511
|
+
roles: "ROLES",
|
|
512
|
+
cache: "CACHE",
|
|
513
|
+
idempotent: "IDEMPOTENT",
|
|
514
|
+
description: "DESCRIPTION"
|
|
515
|
+
};
|
|
516
|
+
const colWidths = {
|
|
517
|
+
method: Math.max(headers.method.length, ...rows.map((r) => r.method.length)),
|
|
518
|
+
path: Math.max(headers.path.length, ...rows.map((r) => r.path.length)),
|
|
519
|
+
auth: Math.max(headers.auth.length, ...rows.map((r) => r.auth.length)),
|
|
520
|
+
roles: Math.max(headers.roles.length, ...rows.map((r) => r.roles.length)),
|
|
521
|
+
cache: Math.max(headers.cache.length, ...rows.map((r) => r.cache.length)),
|
|
522
|
+
idempotent: Math.max(headers.idempotent.length, ...rows.map((r) => r.idempotent.length)),
|
|
523
|
+
description: Math.max(headers.description.length, ...rows.map((r) => r.description.length))
|
|
524
|
+
};
|
|
525
|
+
const pad = (str, len) => str.padEnd(len, " ");
|
|
526
|
+
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)} |`;
|
|
527
|
+
const sepLine = `|-${"-".repeat(colWidths.method)}-|-${"-".repeat(colWidths.path)}-|-${"-".repeat(colWidths.auth)}-|-${"-".repeat(colWidths.roles)}-|-${"-".repeat(colWidths.cache)}-|-${"-".repeat(colWidths.idempotent)}-|-${"-".repeat(colWidths.description)}-|`;
|
|
528
|
+
const bodyLines = rows.map((r) => `| ${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)} |`);
|
|
529
|
+
return [
|
|
530
|
+
`\uD83D\uDD25 FireAPI Route Manifest (${routes.length} total endpoints):`,
|
|
531
|
+
sepLine,
|
|
532
|
+
headerLine,
|
|
533
|
+
sepLine,
|
|
534
|
+
...bodyLines,
|
|
535
|
+
sepLine
|
|
536
|
+
].join(`
|
|
537
|
+
`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// bin/fire.js
|
|
541
|
+
var [, , cmd, subcmd, target] = process.argv;
|
|
542
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
543
|
+
console.log(`
|
|
544
|
+
\uD83D\uDD25 FireAPI CLI - Next-Gen Modern API Toolkit
|
|
545
|
+
|
|
546
|
+
Usage:
|
|
547
|
+
npx fire-api <command> [options]
|
|
548
|
+
|
|
549
|
+
Commands:
|
|
550
|
+
create, new <name> Scaffold a complete, production-grade FireAPI project
|
|
551
|
+
routes [entry] Display visual terminal table of all registered routes
|
|
552
|
+
g, generate resource <name> Generate a new resource module (route + schema)
|
|
553
|
+
bench <url> Run ultra-fast load benchmark against an endpoint
|
|
554
|
+
docker Generate production-hardened multi-stage Dockerfile
|
|
555
|
+
compile [output] Compile standalone single-binary application
|
|
556
|
+
info Display FireAPI engine version and environment
|
|
557
|
+
help Show this help message
|
|
558
|
+
|
|
559
|
+
Examples:
|
|
560
|
+
npx fire-api create my-store-api
|
|
561
|
+
npx fire-api routes
|
|
562
|
+
npx fire-api g resource product
|
|
563
|
+
npx fire-api bench http://localhost:3000/books
|
|
564
|
+
npx fire-api compile ./dist/api-server
|
|
565
|
+
npx fire-api docker
|
|
566
|
+
`);
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
if (cmd === "create" || cmd === "new" || cmd === "init") {
|
|
570
|
+
const appName = subcmd || "my-fire-api";
|
|
571
|
+
console.log(`
|
|
572
|
+
\uD83D\uDD25 Scaffolding brand new FireAPI application in ./${appName}...
|
|
573
|
+
`);
|
|
574
|
+
const result = createAppProject({ name: appName });
|
|
575
|
+
console.log(`✨ Created ${result.filesCreated.length} files successfully:`);
|
|
576
|
+
result.filesCreated.forEach((f) => console.log(` \uD83D\uDCC4 ${f}`));
|
|
577
|
+
console.log(`
|
|
578
|
+
\uD83C\uDF89 FireAPI application ready!
|
|
579
|
+
To start developing:
|
|
580
|
+
|
|
581
|
+
cd ${appName}
|
|
582
|
+
bun install # (or pnpm install / npm install)
|
|
583
|
+
bun dev # Launches dev server with live reload at http://localhost:3000
|
|
584
|
+
|
|
585
|
+
\uD83D\uDCD6 Interactive Docs: http://localhost:3000/docs
|
|
586
|
+
\uD83D\uDDA5️ FireStudio: http://localhost:3000/studio
|
|
587
|
+
\uD83E\uDE7A Health Probes: http://localhost:3000/healthz
|
|
588
|
+
`);
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
591
|
+
if (cmd === "routes" || cmd === "route:list") {
|
|
592
|
+
const entryPath = subcmd || "./src/index.ts";
|
|
593
|
+
const fullPath = path5.resolve(process.cwd(), entryPath);
|
|
594
|
+
try {
|
|
595
|
+
const mod = await import(fullPath);
|
|
596
|
+
const app = mod.default || mod.app;
|
|
597
|
+
if (app && app.router) {
|
|
598
|
+
console.log(`
|
|
599
|
+
` + formatRoutesTable(app.router) + `
|
|
600
|
+
`);
|
|
601
|
+
} else {
|
|
602
|
+
console.error("❌ Could not find exported FireApp instance in " + entryPath);
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.error("❌ Error inspecting routes:", err.message);
|
|
606
|
+
}
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}
|
|
609
|
+
if (cmd === "info") {
|
|
610
|
+
console.log(`
|
|
611
|
+
\uD83D\uDD25 FireAPI Framework v1.0.0
|
|
612
|
+
⚡ Engine: Bun / Node.js 20+ Native Web Standards
|
|
613
|
+
\uD83D\uDEE1️ Security Shield: Enabled by default
|
|
614
|
+
\uD83D\uDDC4️ Database Hub: SQLite, Postgres, MySQL, Mongo, In-Memory
|
|
615
|
+
\uD83E\uDD16 AI/MCP: Native Model Context Protocol Support
|
|
616
|
+
⚡ JIT Fast-Path & Gzip Compression: Enabled
|
|
617
|
+
\uD83D\uDD12 RBAC & Policy Gates: Enabled
|
|
618
|
+
`);
|
|
619
|
+
process.exit(0);
|
|
620
|
+
}
|
|
621
|
+
if (cmd === "docker") {
|
|
622
|
+
const result = generateDockerFiles();
|
|
623
|
+
console.log(`
|
|
624
|
+
✨ Successfully generated production Docker container files!`);
|
|
625
|
+
console.log(` \uD83D\uDC33 Dockerfile: ${result.dockerfilePath}`);
|
|
626
|
+
console.log(` \uD83D\uDCC4 .dockerignore: ${result.dockerignorePath}
|
|
627
|
+
`);
|
|
628
|
+
process.exit(0);
|
|
629
|
+
} else if (cmd === "compile") {
|
|
630
|
+
console.log("⚡ Compiling standalone native binary with Bun...");
|
|
631
|
+
const res = compileBinary({ output: subcmd || "./dist/server" });
|
|
632
|
+
if (res.success) {
|
|
633
|
+
console.log(`✨ Binary successfully compiled! Output: ${subcmd || "./dist/server"}`);
|
|
634
|
+
process.exit(0);
|
|
635
|
+
} else {
|
|
636
|
+
console.error(`❌ Compilation failed: ${res.output}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
} else if (cmd === "bench") {
|
|
640
|
+
const url = subcmd;
|
|
641
|
+
if (!url) {
|
|
642
|
+
console.error("❌ Please specify a target URL, e.g.: npx fire-api bench http://localhost:3000/books");
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
console.log(`\uD83D\uDD25 Launching benchmark against ${url}...`);
|
|
646
|
+
runBenchmark(url, { concurrency: 20, totalRequests: 500 }).then((stats) => {
|
|
647
|
+
console.log(formatBenchmarkReport(stats));
|
|
648
|
+
process.exit(0);
|
|
649
|
+
}).catch((err) => {
|
|
650
|
+
console.error("Benchmark error:", err);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
});
|
|
653
|
+
} else if (cmd === "g" || cmd === "generate") {
|
|
654
|
+
if (subcmd === "resource") {
|
|
655
|
+
if (!target) {
|
|
656
|
+
console.error("❌ Please specify a resource name, e.g.: npx fire-api g resource product");
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
const result = generateResource(target);
|
|
660
|
+
console.log(`
|
|
661
|
+
✨ Successfully generated ${target} resource!`);
|
|
662
|
+
console.log(` \uD83D\uDCC4 Schema: ${result.schemaFile}`);
|
|
663
|
+
console.log(` \uD83D\uDEE3️ Route: ${result.routeFile}
|
|
664
|
+
`);
|
|
665
|
+
process.exit(0);
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
console.error(`Unknown command: ${cmd}. Run "npx fire-api --help" for available commands.`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamayrp14/fire-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Ultra-lightweight, blazing-fast, type-safe API framework with universal DB, security shield, turnkey auth, express compat, dev studio, and native AI/MCP integration",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"fire-api": "./bin/fire.
|
|
10
|
-
"fire": "./bin/fire.
|
|
9
|
+
"fire-api": "./bin/fire.mjs",
|
|
10
|
+
"fire": "./bin/fire.mjs"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "bun test",
|
|
48
|
-
"build": "bun build ./src/index.ts ./src/client.ts ./src/auth/auth.ts ./src/compat/express.ts ./src/studio/studio.ts --outdir ./dist --target node && tsc --module ESNext --moduleResolution bundler --declaration --emitDeclarationOnly --outDir dist",
|
|
48
|
+
"build": "bun build ./bin/fire.js --outfile ./bin/fire.mjs --target node --format esm && chmod +x ./bin/fire.mjs && bun build ./src/index.ts ./src/client.ts ./src/auth/auth.ts ./src/compat/express.ts ./src/studio/studio.ts --outdir ./dist --target node && tsc --module ESNext --moduleResolution bundler --declaration --emitDeclarationOnly --outDir dist",
|
|
49
49
|
"prepublishOnly": "bun run build"
|
|
50
50
|
},
|
|
51
51
|
"keywords": [
|