@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,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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Middleware } from '../types';
|
|
2
|
+
|
|
3
|
+
export type ExpressMiddleware = (
|
|
4
|
+
req: any,
|
|
5
|
+
res: any,
|
|
6
|
+
next: (err?: any) => void
|
|
7
|
+
) => void | Promise<void>;
|
|
8
|
+
|
|
9
|
+
export function fromExpress(expressMiddleware: ExpressMiddleware): Middleware {
|
|
10
|
+
return async (ctx, next) => {
|
|
11
|
+
let statusCode = 200;
|
|
12
|
+
const responseHeaders = new Headers();
|
|
13
|
+
let earlyResponse: Response | null = null;
|
|
14
|
+
let nextCalled = false;
|
|
15
|
+
|
|
16
|
+
// Use a clean proxy to avoid assigning to readonly properties on native Request
|
|
17
|
+
const customProps: Record<string, any> = {
|
|
18
|
+
url: new URL(ctx.req.url).pathname,
|
|
19
|
+
path: new URL(ctx.req.url).pathname,
|
|
20
|
+
method: ctx.req.method,
|
|
21
|
+
headers: Object.fromEntries(ctx.req.headers.entries()),
|
|
22
|
+
params: ctx.params,
|
|
23
|
+
query: ctx.query,
|
|
24
|
+
body: ctx.body,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const simulatedReq = new Proxy(ctx.req, {
|
|
28
|
+
get(target, prop: string) {
|
|
29
|
+
if (prop in customProps) {
|
|
30
|
+
return customProps[prop];
|
|
31
|
+
}
|
|
32
|
+
const val = (target as any)[prop];
|
|
33
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
34
|
+
},
|
|
35
|
+
set(_target, prop: string, value: any) {
|
|
36
|
+
customProps[prop] = value;
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
has(_target, prop: string) {
|
|
40
|
+
return prop in customProps || prop in _target;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Point ctx.req to proxy so handlers have access to properties attached by Express middleware
|
|
45
|
+
(ctx as any).req = simulatedReq;
|
|
46
|
+
|
|
47
|
+
// Simulated Express res
|
|
48
|
+
const simulatedRes: any = {
|
|
49
|
+
statusCode: 200,
|
|
50
|
+
setHeader(name: string, value: string) {
|
|
51
|
+
responseHeaders.set(name, value);
|
|
52
|
+
return this;
|
|
53
|
+
},
|
|
54
|
+
getHeader(name: string) {
|
|
55
|
+
return responseHeaders.get(name);
|
|
56
|
+
},
|
|
57
|
+
removeHeader(name: string) {
|
|
58
|
+
responseHeaders.delete(name);
|
|
59
|
+
return this;
|
|
60
|
+
},
|
|
61
|
+
status(code: number) {
|
|
62
|
+
statusCode = code;
|
|
63
|
+
this.statusCode = code;
|
|
64
|
+
return this;
|
|
65
|
+
},
|
|
66
|
+
json(data: any) {
|
|
67
|
+
responseHeaders.set('Content-Type', 'application/json');
|
|
68
|
+
earlyResponse = new Response(JSON.stringify(data), {
|
|
69
|
+
status: statusCode,
|
|
70
|
+
headers: responseHeaders,
|
|
71
|
+
});
|
|
72
|
+
return this;
|
|
73
|
+
},
|
|
74
|
+
send(body: any) {
|
|
75
|
+
if (typeof body === 'object' && body !== null) {
|
|
76
|
+
return this.json(body);
|
|
77
|
+
}
|
|
78
|
+
if (!responseHeaders.has('Content-Type')) {
|
|
79
|
+
responseHeaders.set('Content-Type', 'text/html; charset=utf-8');
|
|
80
|
+
}
|
|
81
|
+
earlyResponse = new Response(String(body ?? ''), {
|
|
82
|
+
status: statusCode,
|
|
83
|
+
headers: responseHeaders,
|
|
84
|
+
});
|
|
85
|
+
return this;
|
|
86
|
+
},
|
|
87
|
+
end(chunk?: any) {
|
|
88
|
+
if (chunk) {
|
|
89
|
+
return this.send(chunk);
|
|
90
|
+
}
|
|
91
|
+
earlyResponse = new Response(null, {
|
|
92
|
+
status: statusCode,
|
|
93
|
+
headers: responseHeaders,
|
|
94
|
+
});
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return new Promise<Response>((resolve, reject) => {
|
|
100
|
+
try {
|
|
101
|
+
const nextFn = async (err?: any) => {
|
|
102
|
+
nextCalled = true;
|
|
103
|
+
if (err) {
|
|
104
|
+
reject(err);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const downstreamResponse = await next();
|
|
109
|
+
const mergedHeaders = new Headers(downstreamResponse.headers);
|
|
110
|
+
responseHeaders.forEach((v, k) => mergedHeaders.set(k, v));
|
|
111
|
+
|
|
112
|
+
resolve(
|
|
113
|
+
new Response(downstreamResponse.body, {
|
|
114
|
+
status: downstreamResponse.status,
|
|
115
|
+
statusText: downstreamResponse.statusText,
|
|
116
|
+
headers: mergedHeaders,
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
reject(e);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = expressMiddleware(simulatedReq, simulatedRes, nextFn);
|
|
125
|
+
|
|
126
|
+
if (result && typeof (result as any).catch === 'function') {
|
|
127
|
+
(result as any).catch(reject);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if response was sent synchronously without calling next()
|
|
131
|
+
if (!nextCalled && earlyResponse) {
|
|
132
|
+
resolve(earlyResponse);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
reject(err);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}
|