@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.
- package/README.md +276 -2
- package/bin/fire.js +139 -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,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
|
+
}
|