@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.
Files changed (99) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bin/fire.mjs +670 -0
  4. package/bun.lock +20 -0
  5. package/docs/GETTING_STARTED.md +150 -0
  6. package/docs/LAUNCH_KIT.md +236 -0
  7. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  8. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  9. package/examples/basic-api/index.ts +177 -0
  10. package/package.json +68 -4
  11. package/src/ai/mcp.ts +140 -0
  12. package/src/auth/auth.ts +110 -0
  13. package/src/auth/crypto.ts +70 -0
  14. package/src/auth/jwt.ts +105 -0
  15. package/src/cache/cache.ts +72 -0
  16. package/src/cli/bench.ts +118 -0
  17. package/src/cli/compile.ts +29 -0
  18. package/src/cli/create-app.ts +284 -0
  19. package/src/cli/docker.ts +51 -0
  20. package/src/cli/generator.ts +75 -0
  21. package/src/cli/routes.ts +76 -0
  22. package/src/client.ts +120 -0
  23. package/src/compat/express.ts +139 -0
  24. package/src/core/app.ts +723 -0
  25. package/src/core/context.ts +218 -0
  26. package/src/core/error.ts +42 -0
  27. package/src/core/router.ts +262 -0
  28. package/src/db/db-interface.ts +9 -0
  29. package/src/db/hub.ts +256 -0
  30. package/src/db/memory-adapter.ts +202 -0
  31. package/src/db/migrator.ts +100 -0
  32. package/src/db/sqlite-adapter.ts +58 -0
  33. package/src/docs/collection.ts +57 -0
  34. package/src/docs/scalar-ui.ts +25 -0
  35. package/src/events/event-bus.ts +61 -0
  36. package/src/index.ts +37 -0
  37. package/src/jobs/cron.ts +77 -0
  38. package/src/jobs/queue.ts +108 -0
  39. package/src/logging/logger.ts +66 -0
  40. package/src/metrics/metrics.ts +66 -0
  41. package/src/modules/product/product.route.ts +36 -0
  42. package/src/modules/product/product.schema.ts +10 -0
  43. package/src/schema/mock.ts +60 -0
  44. package/src/schema/openapi.ts +101 -0
  45. package/src/schema/schema.ts +519 -0
  46. package/src/schema/validator.ts +5 -0
  47. package/src/shield/compress.ts +61 -0
  48. package/src/shield/cors.ts +94 -0
  49. package/src/shield/headers.ts +55 -0
  50. package/src/shield/idempotency.ts +76 -0
  51. package/src/shield/rate-limiter.ts +81 -0
  52. package/src/shield/sanitizer.ts +23 -0
  53. package/src/shield/webhook.ts +101 -0
  54. package/src/static/serve.ts +51 -0
  55. package/src/storage/storage.ts +176 -0
  56. package/src/stream/sse.ts +89 -0
  57. package/src/studio/studio.ts +221 -0
  58. package/src/types.ts +241 -0
  59. package/src/ws/ws.ts +118 -0
  60. package/test/ai-mcp.test.ts +61 -0
  61. package/test/auth.test.ts +108 -0
  62. package/test/bench.test.ts +31 -0
  63. package/test/cache.test.ts +36 -0
  64. package/test/cli-create.test.ts +64 -0
  65. package/test/client.test.ts +54 -0
  66. package/test/collection-export.test.ts +52 -0
  67. package/test/compat.test.ts +48 -0
  68. package/test/compress.test.ts +48 -0
  69. package/test/context.test.ts +48 -0
  70. package/test/db.test.ts +62 -0
  71. package/test/docker.test.ts +28 -0
  72. package/test/events.test.ts +62 -0
  73. package/test/file-upload.test.ts +116 -0
  74. package/test/hooks.test.ts +77 -0
  75. package/test/idempotency.test.ts +65 -0
  76. package/test/integration.test.ts +111 -0
  77. package/test/jobs.test.ts +60 -0
  78. package/test/metrics.test.ts +40 -0
  79. package/test/mock.test.ts +33 -0
  80. package/test/multi-tenant.test.ts +80 -0
  81. package/test/observability.test.ts +95 -0
  82. package/test/openapi.test.ts +41 -0
  83. package/test/orm-relations.test.ts +85 -0
  84. package/test/orm-shortcuts.test.ts +52 -0
  85. package/test/rbac.test.ts +85 -0
  86. package/test/router-fastpath.test.ts +42 -0
  87. package/test/router.test.ts +84 -0
  88. package/test/schema.test.ts +49 -0
  89. package/test/seeder-migrator.test.ts +78 -0
  90. package/test/shield.test.ts +55 -0
  91. package/test/shorthand-dx.test.ts +31 -0
  92. package/test/sse.test.ts +43 -0
  93. package/test/static-serve.test.ts +53 -0
  94. package/test/storage.test.ts +31 -0
  95. package/test/studio.test.ts +43 -0
  96. package/test/tx.test.ts +92 -0
  97. package/test/webhook.test.ts +113 -0
  98. package/test/ws.test.ts +54 -0
  99. 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
+ }
@@ -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
+ }