@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,218 @@
1
+ import { FireCacheManager, FireContext, FireDatabase, FireQueue, FireStorage, UploadedFile } from '../types';
2
+ import { HttpError } from './error';
3
+ import { createSSEResponse } from '../stream/sse';
4
+ import { WebhookVerifier } from '../shield/webhook';
5
+ import { FireLogger } from '../logging/logger';
6
+
7
+ function parseQuery(urlStr: string): Record<string, string | string[]> {
8
+ const url = new URL(urlStr, 'http://localhost');
9
+ const query: Record<string, string | string[]> = {};
10
+ url.searchParams.forEach((val, key) => {
11
+ if (key in query) {
12
+ if (Array.isArray(query[key])) {
13
+ (query[key] as string[]).push(val);
14
+ } else {
15
+ query[key] = [query[key] as string, val];
16
+ }
17
+ } else {
18
+ query[key] = val;
19
+ }
20
+ });
21
+ return query;
22
+ }
23
+
24
+ const dummyDb: FireDatabase = {
25
+ table() {
26
+ throw new Error('Database not initialized. Configure database in createApp({ database: ... })');
27
+ },
28
+ async raw() {
29
+ throw new Error('Database not initialized');
30
+ },
31
+ async transaction() {
32
+ throw new Error('Database not initialized');
33
+ },
34
+ async tx() {
35
+ throw new Error('Database not initialized');
36
+ },
37
+ async seed() {
38
+ throw new Error('Database not initialized');
39
+ },
40
+ };
41
+
42
+ const dummyCache: FireCacheManager = {
43
+ async get() { return null; },
44
+ async set() {},
45
+ async invalidate() {},
46
+ async clear() {},
47
+ };
48
+
49
+ const dummyQueue: FireQueue = {
50
+ async dispatch(name, data) {
51
+ return {
52
+ id: 'dummy',
53
+ name,
54
+ data,
55
+ attempts: 0,
56
+ maxAttempts: 1,
57
+ createdAt: Date.now(),
58
+ status: 'pending',
59
+ };
60
+ },
61
+ handle() {},
62
+ };
63
+
64
+ const dummyStorage: FireStorage = {
65
+ async put(path, content) {
66
+ const size = typeof content === 'string' ? content.length : (content as Uint8Array).byteLength || 0;
67
+ return { path, url: `/uploads/${path}`, size };
68
+ },
69
+ async get() { return null; },
70
+ async getText() { return null; },
71
+ async delete() { return true; },
72
+ async exists() { return false; },
73
+ url(path) { return `/uploads/${path}`; },
74
+ };
75
+
76
+ export interface CreateContextOptions {
77
+ req: Request;
78
+ params?: Record<string, string>;
79
+ db?: FireDatabase;
80
+ cache?: FireCacheManager;
81
+ queue?: FireQueue;
82
+ storage?: FireStorage;
83
+ user?: any;
84
+ body?: any;
85
+ files?: Record<string, UploadedFile>;
86
+ rawBody?: string;
87
+ requestId?: string;
88
+ traceId?: string;
89
+ logger?: any;
90
+ tenant?: { id: string; [key: string]: any };
91
+ emit?: (event: string, payload?: any) => Promise<void>;
92
+ }
93
+
94
+ export function createContext(opts: CreateContextOptions): FireContext {
95
+ const {
96
+ req,
97
+ params = {},
98
+ db = dummyDb,
99
+ cache = dummyCache,
100
+ queue = dummyQueue,
101
+ storage = dummyStorage,
102
+ user = null,
103
+ body = null,
104
+ files = {},
105
+ rawBody = '',
106
+ emit,
107
+ } = opts;
108
+ const query = parseQuery(req.url);
109
+ const state: Record<string, any> = {};
110
+ const webhookVerifier = new WebhookVerifier(req, rawBody);
111
+ const requestId = opts.requestId || req.headers.get('x-request-id') || `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
112
+ const traceId = opts.traceId || req.headers.get('traceparent') || undefined;
113
+ const log = opts.logger || new FireLogger(requestId);
114
+
115
+ const ctx: FireContext = {
116
+ req,
117
+ params,
118
+ query,
119
+ body,
120
+ files,
121
+ user,
122
+ db,
123
+ cache,
124
+ queue,
125
+ storage,
126
+ state,
127
+ rawBody,
128
+ requestId,
129
+ traceId,
130
+ log,
131
+ tenant: opts.tenant,
132
+
133
+ emit: async (event: string, payload?: any) => {
134
+ if (emit) {
135
+ await emit(event, payload);
136
+ }
137
+ },
138
+
139
+ webhook: {
140
+ verifyHMAC: (options) => webhookVerifier.verifyHMAC(options),
141
+ },
142
+
143
+ stream: {
144
+ sse(handler) {
145
+ return createSSEResponse(handler);
146
+ },
147
+ },
148
+
149
+ earlyHints(links: string[]) {
150
+ if (!ctx.state.earlyHints) {
151
+ ctx.state.earlyHints = [];
152
+ }
153
+ ctx.state.earlyHints.push(...links);
154
+ },
155
+
156
+ json(data: any, status = 200, headers?: HeadersInit): Response {
157
+ const respHeaders = new Headers(headers);
158
+ if (!respHeaders.has('Content-Type')) {
159
+ respHeaders.set('Content-Type', 'application/json; charset=utf-8');
160
+ }
161
+ return new Response(JSON.stringify(data), {
162
+ status,
163
+ headers: respHeaders,
164
+ });
165
+ },
166
+
167
+ ok(data: any): Response {
168
+ return ctx.json(data, 200);
169
+ },
170
+
171
+ created(data: any): Response {
172
+ return ctx.json(data, 201);
173
+ },
174
+
175
+ noContent(): Response {
176
+ return new Response(null, { status: 204 });
177
+ },
178
+
179
+ badRequest(errors: any): Response {
180
+ const msg = typeof errors === 'string' ? errors : (errors?.message || 'Bad Request');
181
+ return new HttpError(400, msg, errors).toResponse();
182
+ },
183
+
184
+ unauthorized(message = 'Unauthorized'): Response {
185
+ return new HttpError(401, message).toResponse();
186
+ },
187
+
188
+ forbidden(message = 'Forbidden'): Response {
189
+ return new HttpError(403, message).toResponse();
190
+ },
191
+
192
+ notFound(message = 'Not Found'): Response {
193
+ return new HttpError(404, message).toResponse();
194
+ },
195
+
196
+ internalError(message = 'Internal Server Error'): Response {
197
+ return new HttpError(500, message).toResponse();
198
+ },
199
+
200
+ text(content: string, status = 200, headers?: HeadersInit): Response {
201
+ const respHeaders = new Headers(headers);
202
+ if (!respHeaders.has('Content-Type')) {
203
+ respHeaders.set('Content-Type', 'text/plain; charset=utf-8');
204
+ }
205
+ return new Response(content, { status, headers: respHeaders });
206
+ },
207
+
208
+ html(content: string, status = 200, headers?: HeadersInit): Response {
209
+ const respHeaders = new Headers(headers);
210
+ if (!respHeaders.has('Content-Type')) {
211
+ respHeaders.set('Content-Type', 'text/html; charset=utf-8');
212
+ }
213
+ return new Response(content, { status, headers: respHeaders });
214
+ },
215
+ };
216
+
217
+ return ctx;
218
+ }
@@ -0,0 +1,42 @@
1
+ export class HttpError extends Error {
2
+ public statusCode: number;
3
+ public details?: any;
4
+
5
+ constructor(statusCode: number, message: string, details?: any) {
6
+ super(message);
7
+ this.name = 'HttpError';
8
+ this.statusCode = statusCode;
9
+ this.details = details;
10
+ Object.setPrototypeOf(this, HttpError.prototype);
11
+ }
12
+
13
+ public getStatusText(): string {
14
+ switch (this.statusCode) {
15
+ case 400: return 'Bad Request';
16
+ case 401: return 'Unauthorized';
17
+ case 403: return 'Forbidden';
18
+ case 404: return 'Not Found';
19
+ case 405: return 'Method Not Allowed';
20
+ case 409: return 'Conflict';
21
+ case 422: return 'Unprocessable Entity';
22
+ case 429: return 'Too Many Requests';
23
+ case 500: return 'Internal Server Error';
24
+ default: return 'Error';
25
+ }
26
+ }
27
+
28
+ public toResponse(): Response {
29
+ const payload: Record<string, any> = {
30
+ statusCode: this.statusCode,
31
+ error: this.getStatusText(),
32
+ message: this.message,
33
+ };
34
+ if (this.details !== undefined) {
35
+ payload.details = this.details;
36
+ }
37
+ return new Response(JSON.stringify(payload), {
38
+ status: this.statusCode,
39
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,262 @@
1
+ import { Handler, HttpMethod, Middleware, RouteMatch, RouteOptions } from '../types';
2
+
3
+ interface RadixNode {
4
+ part: string;
5
+ isParam: boolean;
6
+ paramName?: string;
7
+ isWildcard: boolean;
8
+ children: Map<string, RadixNode>;
9
+ paramChild?: RadixNode;
10
+ wildcardChild?: RadixNode;
11
+ handlers: Map<HttpMethod, { options: RouteOptions; middlewares: Middleware[] }>;
12
+ }
13
+
14
+ function createNode(part = ''): RadixNode {
15
+ const isParam = part.startsWith(':');
16
+ const isWildcard = part === '*' || part.startsWith('*');
17
+ return {
18
+ part,
19
+ isParam,
20
+ paramName: isParam ? part.slice(1) : undefined,
21
+ isWildcard,
22
+ children: new Map(),
23
+ handlers: new Map(),
24
+ };
25
+ }
26
+
27
+ export function normalizePath(path: string): string {
28
+ if (!path || path === '/' || path === '') return '/';
29
+ let clean = path.replace(/\/+/g, '/');
30
+ if (clean.length > 1 && clean.endsWith('/')) {
31
+ clean = clean.slice(0, -1);
32
+ }
33
+ if (!clean.startsWith('/')) {
34
+ clean = '/' + clean;
35
+ }
36
+ return clean;
37
+ }
38
+
39
+ export class Router {
40
+ private root: RadixNode = createNode();
41
+ public middlewares: Middleware[] = [];
42
+ public prefix: string;
43
+ public routesList: { method: HttpMethod; path: string; options: RouteOptions }[] = [];
44
+ private fastPathCache: Map<string, RouteMatch> = new Map();
45
+ private maxFastPathEntries = 2000;
46
+
47
+ constructor(prefix = '') {
48
+ this.prefix = prefix ? normalizePath(prefix) : '';
49
+ }
50
+
51
+ public use(middleware: Middleware): this;
52
+ public use(subRouter: Router): this;
53
+ public use(prefix: string, subRouter: Router): this;
54
+ public use(arg1: string | Middleware | Router, arg2?: Router): this {
55
+ if (typeof arg1 === 'function') {
56
+ this.middlewares.push(arg1);
57
+ return this;
58
+ }
59
+
60
+ if (arg1 instanceof Router) {
61
+ for (const route of arg1.routesList) {
62
+ const fullPath = this.prefix && !route.path.startsWith(this.prefix)
63
+ ? normalizePath(`${this.prefix}/${route.path}`)
64
+ : route.path;
65
+ const combinedMiddlewares = [...arg1.middlewares, ...(route.options.middlewares || [])];
66
+ this.addRawRoute(route.method, fullPath, {
67
+ ...route.options,
68
+ middlewares: combinedMiddlewares,
69
+ });
70
+ }
71
+ return this;
72
+ }
73
+
74
+ if (typeof arg1 === 'string' && arg2 instanceof Router) {
75
+ const subPrefix = normalizePath(arg1);
76
+ for (const route of arg2.routesList) {
77
+ const fullPath = route.path.startsWith(subPrefix)
78
+ ? route.path
79
+ : normalizePath(`${subPrefix}/${route.path}`);
80
+ const combinedMiddlewares = [...arg2.middlewares, ...(route.options.middlewares || [])];
81
+ this.addRawRoute(route.method, fullPath, {
82
+ ...route.options,
83
+ middlewares: combinedMiddlewares,
84
+ });
85
+ }
86
+ return this;
87
+ }
88
+
89
+ return this;
90
+ }
91
+
92
+ public addRoute(method: HttpMethod, path: string, options: RouteOptions): this {
93
+ let cleanPath = normalizePath(path);
94
+ if (this.prefix && !cleanPath.startsWith(this.prefix)) {
95
+ cleanPath = normalizePath(`${this.prefix}/${cleanPath}`);
96
+ }
97
+ return this.addRawRoute(method, cleanPath, options);
98
+ }
99
+
100
+ private addRawRoute(method: HttpMethod, fullPath: string, options: RouteOptions): this {
101
+ this.routesList.push({ method, path: fullPath, options });
102
+
103
+ const parts = fullPath.split('/').filter(Boolean);
104
+ let current = this.root;
105
+
106
+ for (const part of parts) {
107
+ if (part === '*' || part.startsWith('*')) {
108
+ if (!current.wildcardChild) {
109
+ current.wildcardChild = createNode(part);
110
+ }
111
+ current = current.wildcardChild;
112
+ break; // Wildcard absorbs remainder
113
+ } else if (part.startsWith(':')) {
114
+ if (!current.paramChild) {
115
+ current.paramChild = createNode(part);
116
+ }
117
+ current = current.paramChild;
118
+ } else {
119
+ if (!current.children.has(part)) {
120
+ current.children.set(part, createNode(part));
121
+ }
122
+ current = current.children.get(part)!;
123
+ }
124
+ }
125
+
126
+ const routeMiddlewares = options.middlewares || [];
127
+ current.handlers.set(method, {
128
+ options,
129
+ middlewares: [...this.middlewares, ...routeMiddlewares],
130
+ });
131
+
132
+ return this;
133
+ }
134
+
135
+ private resolveOptions(optionsOrHandler: RouteOptions | Handler): RouteOptions {
136
+ if (typeof optionsOrHandler === 'function') {
137
+ return { handler: optionsOrHandler };
138
+ }
139
+ return optionsOrHandler;
140
+ }
141
+
142
+ public get(path: string, optionsOrHandler: RouteOptions | Handler): this {
143
+ return this.addRoute('GET', path, this.resolveOptions(optionsOrHandler));
144
+ }
145
+
146
+ public post(path: string, optionsOrHandler: RouteOptions | Handler): this {
147
+ return this.addRoute('POST', path, this.resolveOptions(optionsOrHandler));
148
+ }
149
+
150
+ public put(path: string, optionsOrHandler: RouteOptions | Handler): this {
151
+ return this.addRoute('PUT', path, this.resolveOptions(optionsOrHandler));
152
+ }
153
+
154
+ public patch(path: string, optionsOrHandler: RouteOptions | Handler): this {
155
+ return this.addRoute('PATCH', path, this.resolveOptions(optionsOrHandler));
156
+ }
157
+
158
+ public delete(path: string, optionsOrHandler: RouteOptions | Handler): this {
159
+ return this.addRoute('DELETE', path, this.resolveOptions(optionsOrHandler));
160
+ }
161
+
162
+ public options(path: string, optionsOrHandler: RouteOptions | Handler): this {
163
+ return this.addRoute('OPTIONS', path, this.resolveOptions(optionsOrHandler));
164
+ }
165
+
166
+ public head(path: string, optionsOrHandler: RouteOptions | Handler): this {
167
+ return this.addRoute('HEAD', path, this.resolveOptions(optionsOrHandler));
168
+ }
169
+
170
+ public match(method: HttpMethod, rawPath: string): RouteMatch | null {
171
+ const cleanPath = normalizePath(rawPath);
172
+ const cacheKey = `${method}:${cleanPath}`;
173
+
174
+ const cached = this.fastPathCache.get(cacheKey);
175
+ if (cached) {
176
+ return {
177
+ handler: cached.handler,
178
+ routeOptions: cached.routeOptions,
179
+ params: { ...cached.params },
180
+ middlewares: cached.middlewares,
181
+ };
182
+ }
183
+
184
+ const parts = cleanPath.split('/').filter(Boolean);
185
+ const params: Record<string, string> = {};
186
+
187
+ const result = this.searchNode(this.root, parts, 0, method, params);
188
+ if (!result) return null;
189
+
190
+ const matched: RouteMatch = {
191
+ handler: result.options.handler,
192
+ routeOptions: result.options,
193
+ params,
194
+ middlewares: result.middlewares,
195
+ };
196
+
197
+ if (this.fastPathCache.size >= this.maxFastPathEntries) {
198
+ const keys = Array.from(this.fastPathCache.keys()).slice(0, 400);
199
+ for (const k of keys) this.fastPathCache.delete(k);
200
+ }
201
+ this.fastPathCache.set(cacheKey, matched);
202
+
203
+ return {
204
+ ...matched,
205
+ params: { ...matched.params },
206
+ };
207
+ }
208
+
209
+ private searchNode(
210
+ node: RadixNode,
211
+ parts: string[],
212
+ index: number,
213
+ method: HttpMethod,
214
+ params: Record<string, string>
215
+ ): { options: RouteOptions; middlewares: Middleware[] } | null {
216
+ if (index >= parts.length) {
217
+ const handlerData = node.handlers.get(method);
218
+ if (handlerData) return handlerData;
219
+ if (node.wildcardChild && node.wildcardChild.handlers.has(method)) {
220
+ params['*'] = '';
221
+ return node.wildcardChild.handlers.get(method)!;
222
+ }
223
+ return null;
224
+ }
225
+
226
+ const part = parts[index];
227
+
228
+ // 1. Static match
229
+ const staticChild = node.children.get(part);
230
+ if (staticChild) {
231
+ const match = this.searchNode(staticChild, parts, index + 1, method, params);
232
+ if (match) return match;
233
+ }
234
+
235
+ // 2. Param match
236
+ if (node.paramChild) {
237
+ const match = this.searchNode(node.paramChild, parts, index + 1, method, params);
238
+ if (match) {
239
+ if (node.paramChild.paramName) {
240
+ params[node.paramChild.paramName] = decodeURIComponent(part);
241
+ }
242
+ return match;
243
+ }
244
+ }
245
+
246
+ // 3. Wildcard match
247
+ if (node.wildcardChild && node.wildcardChild.handlers.has(method)) {
248
+ params['*'] = parts.slice(index).map(decodeURIComponent).join('/');
249
+ return node.wildcardChild.handlers.get(method)!;
250
+ }
251
+
252
+ return null;
253
+ }
254
+
255
+ public getRoutes(): Array<{ method: HttpMethod; path: string; options: RouteOptions }> {
256
+ return this.routesList;
257
+ }
258
+ }
259
+
260
+ export function createRouter(prefix = ''): Router {
261
+ return new Router(prefix);
262
+ }
@@ -0,0 +1,9 @@
1
+ import { FireDatabase, QueryBuilder } from '../types';
2
+
3
+ export type { FireDatabase, QueryBuilder };
4
+
5
+ export interface WhereClause {
6
+ field: string;
7
+ operator: '=' | '!=' | '>' | '>=' | '<' | '<=' | 'like' | 'in';
8
+ value: any;
9
+ }