@tamayrp14/fire-api 0.0.0-stage → 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/README.md +276 -2
- package/bin/fire.js +139 -0
- package/bin/fire.mjs +670 -0
- package/bun.lock +20 -0
- package/docs/GETTING_STARTED.md +150 -0
- package/docs/LAUNCH_KIT.md +236 -0
- package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
- package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
- package/examples/basic-api/index.ts +177 -0
- package/package.json +68 -4
- package/src/ai/mcp.ts +140 -0
- package/src/auth/auth.ts +110 -0
- package/src/auth/crypto.ts +70 -0
- package/src/auth/jwt.ts +105 -0
- package/src/cache/cache.ts +72 -0
- package/src/cli/bench.ts +118 -0
- package/src/cli/compile.ts +29 -0
- package/src/cli/create-app.ts +284 -0
- package/src/cli/docker.ts +51 -0
- package/src/cli/generator.ts +75 -0
- package/src/cli/routes.ts +76 -0
- package/src/client.ts +120 -0
- package/src/compat/express.ts +139 -0
- package/src/core/app.ts +723 -0
- package/src/core/context.ts +218 -0
- package/src/core/error.ts +42 -0
- package/src/core/router.ts +262 -0
- package/src/db/db-interface.ts +9 -0
- package/src/db/hub.ts +256 -0
- package/src/db/memory-adapter.ts +202 -0
- package/src/db/migrator.ts +100 -0
- package/src/db/sqlite-adapter.ts +58 -0
- package/src/docs/collection.ts +57 -0
- package/src/docs/scalar-ui.ts +25 -0
- package/src/events/event-bus.ts +61 -0
- package/src/index.ts +37 -0
- package/src/jobs/cron.ts +77 -0
- package/src/jobs/queue.ts +108 -0
- package/src/logging/logger.ts +66 -0
- package/src/metrics/metrics.ts +66 -0
- package/src/modules/product/product.route.ts +36 -0
- package/src/modules/product/product.schema.ts +10 -0
- package/src/schema/mock.ts +60 -0
- package/src/schema/openapi.ts +101 -0
- package/src/schema/schema.ts +519 -0
- package/src/schema/validator.ts +5 -0
- package/src/shield/compress.ts +61 -0
- package/src/shield/cors.ts +94 -0
- package/src/shield/headers.ts +55 -0
- package/src/shield/idempotency.ts +76 -0
- package/src/shield/rate-limiter.ts +81 -0
- package/src/shield/sanitizer.ts +23 -0
- package/src/shield/webhook.ts +101 -0
- package/src/static/serve.ts +51 -0
- package/src/storage/storage.ts +176 -0
- package/src/stream/sse.ts +89 -0
- package/src/studio/studio.ts +221 -0
- package/src/types.ts +241 -0
- package/src/ws/ws.ts +118 -0
- package/test/ai-mcp.test.ts +61 -0
- package/test/auth.test.ts +108 -0
- package/test/bench.test.ts +31 -0
- package/test/cache.test.ts +36 -0
- package/test/cli-create.test.ts +64 -0
- package/test/client.test.ts +54 -0
- package/test/collection-export.test.ts +52 -0
- package/test/compat.test.ts +48 -0
- package/test/compress.test.ts +48 -0
- package/test/context.test.ts +48 -0
- package/test/db.test.ts +62 -0
- package/test/docker.test.ts +28 -0
- package/test/events.test.ts +62 -0
- package/test/file-upload.test.ts +116 -0
- package/test/hooks.test.ts +77 -0
- package/test/idempotency.test.ts +65 -0
- package/test/integration.test.ts +111 -0
- package/test/jobs.test.ts +60 -0
- package/test/metrics.test.ts +40 -0
- package/test/mock.test.ts +33 -0
- package/test/multi-tenant.test.ts +80 -0
- package/test/observability.test.ts +95 -0
- package/test/openapi.test.ts +41 -0
- package/test/orm-relations.test.ts +85 -0
- package/test/orm-shortcuts.test.ts +52 -0
- package/test/rbac.test.ts +85 -0
- package/test/router-fastpath.test.ts +42 -0
- package/test/router.test.ts +84 -0
- package/test/schema.test.ts +49 -0
- package/test/seeder-migrator.test.ts +78 -0
- package/test/shield.test.ts +55 -0
- package/test/shorthand-dx.test.ts +31 -0
- package/test/sse.test.ts +43 -0
- package/test/static-serve.test.ts +53 -0
- package/test/storage.test.ts +31 -0
- package/test/studio.test.ts +43 -0
- package/test/tx.test.ts +92 -0
- package/test/webhook.test.ts +113 -0
- package/test/ws.test.ts +54 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { FireCacheManager } from '../types';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
value: any;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
tags: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MemoryCacheManager implements FireCacheManager {
|
|
10
|
+
private cache: Map<string, CacheEntry> = new Map();
|
|
11
|
+
private tagIndex: Map<string, Set<string>> = new Map(); // tag -> Set of keys
|
|
12
|
+
|
|
13
|
+
public async get<T = any>(key: string): Promise<T | null> {
|
|
14
|
+
const entry = this.cache.get(key);
|
|
15
|
+
if (!entry) return null;
|
|
16
|
+
|
|
17
|
+
if (Date.now() > entry.expiresAt) {
|
|
18
|
+
this.deleteKey(key);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return entry.value as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async set(key: string, value: any, ttlSeconds = 60, tags: string[] = []): Promise<void> {
|
|
26
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
27
|
+
const tagSet = new Set(tags);
|
|
28
|
+
|
|
29
|
+
// If key already existed with other tags, clean up old index
|
|
30
|
+
this.deleteKey(key);
|
|
31
|
+
|
|
32
|
+
this.cache.set(key, { value, expiresAt, tags: tagSet });
|
|
33
|
+
|
|
34
|
+
for (const tag of tags) {
|
|
35
|
+
if (!this.tagIndex.has(tag)) {
|
|
36
|
+
this.tagIndex.set(tag, new Set());
|
|
37
|
+
}
|
|
38
|
+
this.tagIndex.get(tag)!.add(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async invalidate(tag: string): Promise<void> {
|
|
43
|
+
const keys = this.tagIndex.get(tag);
|
|
44
|
+
if (!keys) return;
|
|
45
|
+
|
|
46
|
+
for (const key of Array.from(keys)) {
|
|
47
|
+
this.deleteKey(key);
|
|
48
|
+
}
|
|
49
|
+
this.tagIndex.delete(tag);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async clear(): Promise<void> {
|
|
53
|
+
this.cache.clear();
|
|
54
|
+
this.tagIndex.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private deleteKey(key: string): void {
|
|
58
|
+
const entry = this.cache.get(key);
|
|
59
|
+
if (entry) {
|
|
60
|
+
for (const tag of entry.tags) {
|
|
61
|
+
const tagSet = this.tagIndex.get(tag);
|
|
62
|
+
if (tagSet) {
|
|
63
|
+
tagSet.delete(key);
|
|
64
|
+
if (tagSet.size === 0) {
|
|
65
|
+
this.tagIndex.delete(tag);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.cache.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/cli/bench.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export interface BenchmarkOptions {
|
|
2
|
+
concurrency?: number;
|
|
3
|
+
totalRequests?: number;
|
|
4
|
+
durationSec?: number;
|
|
5
|
+
customFetch?: (url: string) => Promise<Response>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BenchmarkStats {
|
|
9
|
+
url: string;
|
|
10
|
+
totalRequests: number;
|
|
11
|
+
successfulRequests: number;
|
|
12
|
+
failedRequests: number;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
rps: number;
|
|
15
|
+
latency: {
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
avg: number;
|
|
19
|
+
p50: number;
|
|
20
|
+
p90: number;
|
|
21
|
+
p95: number;
|
|
22
|
+
p99: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function percentile(sorted: number[], p: number): number {
|
|
27
|
+
if (sorted.length === 0) return 0;
|
|
28
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
29
|
+
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runBenchmark(url: string, options: BenchmarkOptions = {}): Promise<BenchmarkStats> {
|
|
33
|
+
const concurrency = options.concurrency || 10;
|
|
34
|
+
const total = options.totalRequests || 200;
|
|
35
|
+
const fetchFn = options.customFetch || globalThis.fetch;
|
|
36
|
+
|
|
37
|
+
const latencies: number[] = [];
|
|
38
|
+
let successfulRequests = 0;
|
|
39
|
+
let failedRequests = 0;
|
|
40
|
+
let sent = 0;
|
|
41
|
+
|
|
42
|
+
const startTime = performance.now();
|
|
43
|
+
|
|
44
|
+
async function worker() {
|
|
45
|
+
while (sent < total) {
|
|
46
|
+
sent++;
|
|
47
|
+
const reqStart = performance.now();
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetchFn(url);
|
|
50
|
+
const reqDuration = performance.now() - reqStart;
|
|
51
|
+
latencies.push(reqDuration);
|
|
52
|
+
if (res.ok) {
|
|
53
|
+
successfulRequests++;
|
|
54
|
+
} else {
|
|
55
|
+
failedRequests++;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
const reqDuration = performance.now() - reqStart;
|
|
59
|
+
latencies.push(reqDuration);
|
|
60
|
+
failedRequests++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
66
|
+
await Promise.all(workers);
|
|
67
|
+
|
|
68
|
+
const durationMs = performance.now() - startTime;
|
|
69
|
+
const rps = Math.round((latencies.length / (durationMs / 1000)) * 100) / 100;
|
|
70
|
+
|
|
71
|
+
latencies.sort((a, b) => a - b);
|
|
72
|
+
const sum = latencies.reduce((acc, val) => acc + val, 0);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
url,
|
|
76
|
+
totalRequests: latencies.length,
|
|
77
|
+
successfulRequests,
|
|
78
|
+
failedRequests,
|
|
79
|
+
durationMs: Math.round(durationMs),
|
|
80
|
+
rps,
|
|
81
|
+
latency: {
|
|
82
|
+
min: Math.round((latencies[0] || 0) * 100) / 100,
|
|
83
|
+
max: Math.round((latencies[latencies.length - 1] || 0) * 100) / 100,
|
|
84
|
+
avg: Math.round((sum / (latencies.length || 1)) * 100) / 100,
|
|
85
|
+
p50: Math.round(percentile(latencies, 50) * 100) / 100,
|
|
86
|
+
p90: Math.round(percentile(latencies, 90) * 100) / 100,
|
|
87
|
+
p95: Math.round(percentile(latencies, 95) * 100) / 100,
|
|
88
|
+
p99: Math.round(percentile(latencies, 99) * 100) / 100,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatBenchmarkReport(stats: BenchmarkStats): string {
|
|
94
|
+
const successRate = stats.totalRequests > 0
|
|
95
|
+
? ((stats.successfulRequests / stats.totalRequests) * 100).toFixed(1)
|
|
96
|
+
: '0';
|
|
97
|
+
|
|
98
|
+
return `
|
|
99
|
+
==================================================
|
|
100
|
+
🔥 FireAPI Benchmark Engine
|
|
101
|
+
==================================================
|
|
102
|
+
Target: ${stats.url}
|
|
103
|
+
Duration: ${stats.durationMs} ms
|
|
104
|
+
Total Reqs: ${stats.totalRequests}
|
|
105
|
+
Success Rate: ${successRate}% (${stats.successfulRequests} ok / ${stats.failedRequests} fail)
|
|
106
|
+
|
|
107
|
+
🚀 Requests/sec: ${stats.rps.toLocaleString()} req/s
|
|
108
|
+
|
|
109
|
+
Latency (ms):
|
|
110
|
+
Min: ${stats.latency.min} ms
|
|
111
|
+
P50: ${stats.latency.p50} ms
|
|
112
|
+
P90: ${stats.latency.p90} ms
|
|
113
|
+
P95: ${stats.latency.p95} ms
|
|
114
|
+
P99: ${stats.latency.p99} ms
|
|
115
|
+
Max: ${stats.latency.max} ms
|
|
116
|
+
==================================================
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
@@ -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
|
+
}
|