@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,61 @@
1
+ export type EventListener<T = any> = (payload: T) => Promise<void> | void;
2
+
3
+ export class EventBus {
4
+ private listeners = new Map<string, Array<EventListener>>();
5
+
6
+ public on<T = any>(event: string, listener: EventListener<T>): () => void {
7
+ if (!this.listeners.has(event)) {
8
+ this.listeners.set(event, []);
9
+ }
10
+ this.listeners.get(event)!.push(listener);
11
+
12
+ return () => this.off(event, listener);
13
+ }
14
+
15
+ public off(event: string, listener: EventListener): void {
16
+ const list = this.listeners.get(event);
17
+ if (!list) return;
18
+ const idx = list.indexOf(listener);
19
+ if (idx !== -1) list.splice(idx, 1);
20
+ }
21
+
22
+ public async emit<T = any>(event: string, payload: T): Promise<void> {
23
+ const promises: Promise<void>[] = [];
24
+
25
+ // 1. Exact match
26
+ const exactListeners = this.listeners.get(event) || [];
27
+ for (const fn of exactListeners) {
28
+ try {
29
+ const res = fn(payload);
30
+ if (res instanceof Promise) promises.push(res);
31
+ } catch (err) {
32
+ console.error(`[EventBus] Error in listener for event "${event}":`, err);
33
+ }
34
+ }
35
+
36
+ // 2. Wildcard match (e.g. "order.*" matches "order.paid")
37
+ for (const [pattern, fns] of this.listeners.entries()) {
38
+ if (pattern.includes('*') && this.matchWildcard(pattern, event)) {
39
+ for (const fn of fns) {
40
+ try {
41
+ const res = fn(payload);
42
+ if (res instanceof Promise) promises.push(res);
43
+ } catch (err) {
44
+ console.error(`[EventBus] Error in wildcard listener for pattern "${pattern}":`, err);
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ await Promise.allSettled(promises);
51
+ }
52
+
53
+ private matchWildcard(pattern: string, event: string): boolean {
54
+ const regexStr = '^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
55
+ return new RegExp(regexStr).test(event);
56
+ }
57
+
58
+ public clear(): void {
59
+ this.listeners.clear();
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { FireApp, createApp } from './core/app';
2
+ export { Router, createRouter } from './core/router';
3
+ export { createContext } from './core/context';
4
+ export { HttpError } from './core/error';
5
+ export { s, BaseSchema } from './schema/schema';
6
+ export type { Infer } from './schema/schema';
7
+ export { validate } from './schema/validator';
8
+ export { generateMock } from './schema/mock';
9
+ export { createClient } from './client';
10
+ export { createMCPServer, MCPServer } from './ai/mcp';
11
+ export { MemoryDatabase } from './db/memory-adapter';
12
+ export { SQLiteDatabase } from './db/sqlite-adapter';
13
+ export { FireMigrator } from './db/migrator';
14
+ export type { Migration } from './db/migrator';
15
+ export { MemoryCacheManager } from './cache/cache';
16
+ export { createAuth, requireAuth } from './auth/auth';
17
+ export { fromExpress } from './compat/express';
18
+ export { enableStudio } from './studio/studio';
19
+ export { createSSEResponse, formatSSEMessage } from './stream/sse';
20
+ export { FireQueueManager } from './jobs/queue';
21
+ export { FireCronScheduler } from './jobs/cron';
22
+ export { WebSocketHub } from './ws/ws';
23
+ export { EventBus } from './events/event-bus';
24
+ export { createStorage, MemoryStorageAdapter, LocalDiskStorageAdapter } from './storage/storage';
25
+ export { MetricsCollector } from './metrics/metrics';
26
+ export { FireLogger } from './logging/logger';
27
+ export { compressResponse } from './shield/compress';
28
+ export { IdempotencyManager } from './shield/idempotency';
29
+ export { computeHMAC, verifyHMAC, constantTimeEqual, WebhookVerifier } from './shield/webhook';
30
+ export { runBenchmark, formatBenchmarkReport } from './cli/bench';
31
+ export { generateDockerFiles } from './cli/docker';
32
+ export { compileBinary } from './cli/compile';
33
+ export { createAppProject } from './cli/create-app';
34
+ export { formatRoutesTable } from './cli/routes';
35
+ export { generatePostmanCollection } from './docs/collection';
36
+ export { createStaticFileResponse } from './static/serve';
37
+ export * from './types';
@@ -0,0 +1,77 @@
1
+ export interface CronTask {
2
+ stop(): void;
3
+ isRunning: boolean;
4
+ }
5
+
6
+ export function parseInterval(expression: string): number {
7
+ const match = expression.trim().toLowerCase().match(/^(\d+)(ms|s|m|h|d)?$/);
8
+ if (match) {
9
+ const val = parseInt(match[1], 10);
10
+ const unit = match[2] || 's';
11
+ switch (unit) {
12
+ case 'ms': return val;
13
+ case 's': return val * 1000;
14
+ case 'm': return val * 60 * 1000;
15
+ case 'h': return val * 3600 * 1000;
16
+ case 'd': return val * 86400 * 1000;
17
+ default: return val * 1000;
18
+ }
19
+ }
20
+
21
+ // Common cron aliases
22
+ switch (expression.trim().toLowerCase()) {
23
+ case '@hourly': return 3600 * 1000;
24
+ case '@daily':
25
+ case '@midnight': return 86400 * 1000;
26
+ case '@weekly': return 7 * 86400 * 1000;
27
+ default:
28
+ // Default to 1 minute for unknown cron pattern in lightweight engine
29
+ return 60 * 1000;
30
+ }
31
+ }
32
+
33
+ export class FireCronScheduler {
34
+ private activeTimers: Set<any> = new Set();
35
+
36
+ public schedule(expression: string, task: () => Promise<void> | void): CronTask {
37
+ const intervalMs = parseInterval(expression);
38
+ let isRunning = true;
39
+
40
+ const timer = setInterval(() => {
41
+ if (!isRunning) return;
42
+ try {
43
+ Promise.resolve(task()).catch((e) => {
44
+ console.error('[FireCron Error]', e);
45
+ });
46
+ } catch (err) {
47
+ console.error('[FireCron Error]', err);
48
+ }
49
+ }, intervalMs);
50
+
51
+ // Don't keep Node process alive if only cron timers remain
52
+ if (timer && typeof (timer as any).unref === 'function') {
53
+ (timer as any).unref();
54
+ }
55
+
56
+ this.activeTimers.add(timer);
57
+
58
+ const cronTask: CronTask = {
59
+ isRunning: true,
60
+ stop: () => {
61
+ isRunning = false;
62
+ cronTask.isRunning = false;
63
+ clearInterval(timer);
64
+ this.activeTimers.delete(timer);
65
+ },
66
+ };
67
+
68
+ return cronTask;
69
+ }
70
+
71
+ public stopAll(): void {
72
+ for (const timer of this.activeTimers) {
73
+ clearInterval(timer);
74
+ }
75
+ this.activeTimers.clear();
76
+ }
77
+ }
@@ -0,0 +1,108 @@
1
+ import { FireQueue, FireQueueJob } from '../types';
2
+
3
+ export class FireQueueManager implements FireQueue {
4
+ private jobs: Map<string, FireQueueJob> = new Map();
5
+ private handlers: Map<string, (data: any, job: FireQueueJob) => Promise<void> | void> = new Map();
6
+ private pendingQueue: string[] = [];
7
+ private isProcessing = false;
8
+ private concurrency: number;
9
+ private activeWorkers = 0;
10
+
11
+ constructor(options: { concurrency?: number } = {}) {
12
+ this.concurrency = options.concurrency || 5;
13
+ }
14
+
15
+ public handle<T = any>(
16
+ name: string,
17
+ worker: (data: T, job: FireQueueJob<T>) => Promise<void> | void
18
+ ): void {
19
+ this.handlers.set(name, worker);
20
+ }
21
+
22
+ public async dispatch<T = any>(
23
+ name: string,
24
+ data: T,
25
+ options: { maxAttempts?: number; delayMs?: number; retryDelayMs?: number } = {}
26
+ ): Promise<FireQueueJob<T>> {
27
+ const id = `job_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
28
+ const job: FireQueueJob<T> = {
29
+ id,
30
+ name,
31
+ data,
32
+ attempts: 0,
33
+ maxAttempts: options.maxAttempts || 3,
34
+ createdAt: Date.now(),
35
+ status: 'pending',
36
+ };
37
+
38
+ (job as any).retryDelayMs = options.retryDelayMs ?? 50;
39
+
40
+ this.jobs.set(id, job);
41
+
42
+ if (options.delayMs && options.delayMs > 0) {
43
+ setTimeout(() => {
44
+ this.pendingQueue.push(id);
45
+ this.processQueue();
46
+ }, options.delayMs);
47
+ } else {
48
+ this.pendingQueue.push(id);
49
+ queueMicrotask(() => this.processQueue());
50
+ }
51
+
52
+ return job;
53
+ }
54
+
55
+ private async processQueue(): Promise<void> {
56
+ if (this.isProcessing) return;
57
+ this.isProcessing = true;
58
+
59
+ try {
60
+ while (this.pendingQueue.length > 0 && this.activeWorkers < this.concurrency) {
61
+ const jobId = this.pendingQueue.shift();
62
+ if (!jobId) break;
63
+
64
+ const job = this.jobs.get(jobId);
65
+ if (!job || job.status === 'completed' || job.status === 'failed') continue;
66
+
67
+ const handler = this.handlers.get(job.name);
68
+ if (!handler) {
69
+ // No handler registered yet; keep job pending or delay
70
+ this.pendingQueue.push(jobId);
71
+ break;
72
+ }
73
+
74
+ this.activeWorkers++;
75
+ job.status = 'processing';
76
+ job.attempts++;
77
+
78
+ Promise.resolve(handler(job.data, job))
79
+ .then(() => {
80
+ job.status = 'completed';
81
+ })
82
+ .catch((err) => {
83
+ if (job.attempts < job.maxAttempts) {
84
+ job.status = 'pending';
85
+ const baseDelay = (job as any).retryDelayMs || 50;
86
+ const backoffMs = Math.min(baseDelay * Math.pow(2, job.attempts - 1), 5000);
87
+ setTimeout(() => {
88
+ this.pendingQueue.push(jobId);
89
+ this.processQueue();
90
+ }, backoffMs);
91
+ } else {
92
+ job.status = 'failed';
93
+ }
94
+ })
95
+ .finally(() => {
96
+ this.activeWorkers--;
97
+ this.processQueue();
98
+ });
99
+ }
100
+ } finally {
101
+ this.isProcessing = false;
102
+ }
103
+ }
104
+
105
+ public getJob(id: string): FireQueueJob | undefined {
106
+ return this.jobs.get(id);
107
+ }
108
+ }
@@ -0,0 +1,66 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ export interface LogRecord {
4
+ level: LogLevel;
5
+ time: number;
6
+ reqId?: string;
7
+ msg: string;
8
+ [key: string]: any;
9
+ }
10
+
11
+ export class FireLogger {
12
+ private customSink?: (record: LogRecord) => void;
13
+
14
+ constructor(private contextReqId?: string) {}
15
+
16
+ public setSink(sink: (record: LogRecord) => void): void {
17
+ this.customSink = sink;
18
+ }
19
+
20
+ private emitLog(level: LogLevel, msg: string, data?: Record<string, any>): LogRecord {
21
+ const record: LogRecord = {
22
+ level,
23
+ time: Date.now(),
24
+ ...(this.contextReqId ? { reqId: this.contextReqId } : {}),
25
+ msg,
26
+ ...(data || {}),
27
+ };
28
+
29
+ if (this.customSink) {
30
+ this.customSink(record);
31
+ } else {
32
+ const line = JSON.stringify(record);
33
+ if (level === 'error') {
34
+ console.error(line);
35
+ } else if (level === 'warn') {
36
+ console.warn(line);
37
+ } else {
38
+ console.log(line);
39
+ }
40
+ }
41
+
42
+ return record;
43
+ }
44
+
45
+ public debug(msg: string, data?: Record<string, any>): LogRecord {
46
+ return this.emitLog('debug', msg, data);
47
+ }
48
+
49
+ public info(msg: string, data?: Record<string, any>): LogRecord {
50
+ return this.emitLog('info', msg, data);
51
+ }
52
+
53
+ public warn(msg: string, data?: Record<string, any>): LogRecord {
54
+ return this.emitLog('warn', msg, data);
55
+ }
56
+
57
+ public error(msg: string, data?: Record<string, any>): LogRecord {
58
+ return this.emitLog('error', msg, data);
59
+ }
60
+
61
+ public child(reqId: string): FireLogger {
62
+ const logger = new FireLogger(reqId);
63
+ if (this.customSink) logger.setSink(this.customSink);
64
+ return logger;
65
+ }
66
+ }
@@ -0,0 +1,66 @@
1
+ export class MetricsCollector {
2
+ private requestCounters: Map<string, number> = new Map();
3
+ private requestDurations: Map<string, { count: number; sum: number }> = new Map();
4
+ private dbQueriesCount = 0;
5
+ private startTime: number = Date.now();
6
+
7
+ public recordRequest(method: string, path: string, status: number, durationMs: number): void {
8
+ // 1. Counter: http_requests_total
9
+ const counterKey = `method="${method}",path="${path}",status="${status}"`;
10
+ const currentCount = this.requestCounters.get(counterKey) || 0;
11
+ this.requestCounters.set(counterKey, currentCount + 1);
12
+
13
+ // 2. Summary: http_request_duration_ms
14
+ const durationKey = `method="${method}",path="${path}"`;
15
+ const currentDuration = this.requestDurations.get(durationKey) || { count: 0, sum: 0 };
16
+ this.requestDurations.set(durationKey, {
17
+ count: currentDuration.count + 1,
18
+ sum: currentDuration.sum + durationMs,
19
+ });
20
+ }
21
+
22
+ public recordDbQuery(): void {
23
+ this.dbQueriesCount++;
24
+ }
25
+
26
+ public formatPrometheus(): string {
27
+ const lines: string[] = [];
28
+
29
+ // HTTP Requests Total
30
+ lines.push('# HELP http_requests_total Total number of HTTP requests processed');
31
+ lines.push('# TYPE http_requests_total counter');
32
+ for (const [labels, count] of this.requestCounters.entries()) {
33
+ lines.push(`http_requests_total{${labels}} ${count}`);
34
+ }
35
+
36
+ // HTTP Request Duration
37
+ lines.push('# HELP http_request_duration_ms Total request processing time in milliseconds');
38
+ lines.push('# TYPE http_request_duration_ms summary');
39
+ for (const [labels, stats] of this.requestDurations.entries()) {
40
+ lines.push(`http_request_duration_ms_sum{${labels}} ${stats.sum.toFixed(3)}`);
41
+ lines.push(`http_request_duration_ms_count{${labels}} ${stats.count}`);
42
+ }
43
+
44
+ // DB Queries Total
45
+ lines.push('# HELP fire_db_queries_total Total database queries executed');
46
+ lines.push('# TYPE fire_db_queries_total counter');
47
+ lines.push(`fire_db_queries_total ${this.dbQueriesCount}`);
48
+
49
+ // Process Memory
50
+ let memoryBytes = 0;
51
+ if (typeof process !== 'undefined' && process.memoryUsage) {
52
+ memoryBytes = process.memoryUsage().rss;
53
+ }
54
+ lines.push('# HELP process_resident_memory_bytes Resident memory size in bytes');
55
+ lines.push('# TYPE process_resident_memory_bytes gauge');
56
+ lines.push(`process_resident_memory_bytes ${memoryBytes}`);
57
+
58
+ // Process Uptime
59
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1000);
60
+ lines.push('# HELP process_uptime_seconds Process uptime in seconds');
61
+ lines.push('# TYPE process_uptime_seconds gauge');
62
+ lines.push(`process_uptime_seconds ${uptimeSeconds}`);
63
+
64
+ return lines.join('\n') + '\n';
65
+ }
66
+ }
@@ -0,0 +1,36 @@
1
+ import { createRouter } from '../../index';
2
+ import { CreateProductSchema, ProductIdSchema } from './product.schema';
3
+
4
+ export const productRouter = createRouter('/products');
5
+
6
+ // GET /products
7
+ productRouter.get('/', {
8
+ cache: { ttl: 60, tags: ['products'] },
9
+ ai: { description: 'Fetch list of products', tags: ['Product'] },
10
+ handler: async (ctx) => {
11
+ const items = await ctx.db.table('products').find();
12
+ return ctx.ok(items);
13
+ },
14
+ });
15
+
16
+ // POST /products
17
+ productRouter.post('/', {
18
+ schema: { body: CreateProductSchema },
19
+ ai: { description: 'Create a new product', tags: ['Product'] },
20
+ handler: async (ctx) => {
21
+ const created = await ctx.db.table('products').insert(ctx.body);
22
+ await ctx.cache.invalidate('products');
23
+ return ctx.created(created);
24
+ },
25
+ });
26
+
27
+ // GET /products/:id
28
+ productRouter.get('/:id', {
29
+ schema: { params: ProductIdSchema },
30
+ ai: { description: 'Get a product by unique ID', tags: ['Product'] },
31
+ handler: async (ctx) => {
32
+ const item = await ctx.db.table('products').findById(ctx.params.id);
33
+ if (!item) return ctx.notFound('Product not found');
34
+ return ctx.ok(item);
35
+ },
36
+ });
@@ -0,0 +1,10 @@
1
+ import { s } from '../../index';
2
+
3
+ export const CreateProductSchema = s.object({
4
+ name: s.string().min(2).description('Product title or name'),
5
+ status: s.enum(['active', 'draft']).default('active'),
6
+ });
7
+
8
+ export const ProductIdSchema = s.object({
9
+ id: s.string().min(1),
10
+ });
@@ -0,0 +1,60 @@
1
+ import {
2
+ ArraySchema,
3
+ BaseSchema,
4
+ BooleanSchema,
5
+ EnumSchema,
6
+ NumberSchema,
7
+ ObjectSchema,
8
+ StringSchema,
9
+ } from './schema';
10
+
11
+ export function generateMock(schema: BaseSchema): any {
12
+ if (schema.defaultValue !== undefined) {
13
+ return schema.defaultValue;
14
+ }
15
+
16
+ if (schema instanceof StringSchema) {
17
+ if (schema.isEmail) {
18
+ return `user_${Math.random().toString(36).slice(2, 6)}@example.com`;
19
+ }
20
+ const min = schema.minLength || 5;
21
+ const base = 'sample_text';
22
+ if (base.length < min) {
23
+ return base + '_'.repeat(min - base.length);
24
+ }
25
+ if (schema.maxLength && base.length > schema.maxLength) {
26
+ return base.slice(0, schema.maxLength);
27
+ }
28
+ return base;
29
+ }
30
+
31
+ if (schema instanceof NumberSchema) {
32
+ const min = schema.minVal !== undefined ? schema.minVal : 1;
33
+ const max = schema.maxVal !== undefined ? schema.maxVal : 100;
34
+ const val = min + (max - min) / 2;
35
+ return schema.isInteger ? Math.round(val) : Math.round(val * 100) / 100;
36
+ }
37
+
38
+ if (schema instanceof BooleanSchema) {
39
+ return true;
40
+ }
41
+
42
+ if (schema instanceof EnumSchema) {
43
+ return schema.values[0] || 'default';
44
+ }
45
+
46
+ if (schema instanceof ArraySchema) {
47
+ const count = schema.minItems || 2;
48
+ return Array.from({ length: count }, () => generateMock(schema.itemSchema));
49
+ }
50
+
51
+ if (schema instanceof ObjectSchema) {
52
+ const obj: Record<string, any> = {};
53
+ for (const [key, propSchema] of Object.entries(schema.shape)) {
54
+ obj[key] = generateMock(propSchema as BaseSchema);
55
+ }
56
+ return obj;
57
+ }
58
+
59
+ return 'mock_data';
60
+ }
@@ -0,0 +1,101 @@
1
+ import { Router } from '../core/router';
2
+ import { BaseSchema } from './schema';
3
+
4
+ export interface OpenAPIOptions {
5
+ title?: string;
6
+ version?: string;
7
+ description?: string;
8
+ }
9
+
10
+ export function generateOpenAPISpec(router: Router, options: OpenAPIOptions = {}): Record<string, any> {
11
+ const spec: Record<string, any> = {
12
+ openapi: '3.1.0',
13
+ info: {
14
+ title: options.title || 'FireAPI Documentation',
15
+ version: options.version || '1.0.0',
16
+ description: options.description || 'Auto-generated API documentation powered by FireAPI',
17
+ },
18
+ paths: {},
19
+ };
20
+
21
+ for (const route of router.routesList) {
22
+ // Convert :id to {id} for OpenAPI path specification
23
+ const openApiPath = route.path.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
24
+
25
+ if (!spec.paths[openApiPath]) {
26
+ spec.paths[openApiPath] = {};
27
+ }
28
+
29
+ const methodLower = route.method.toLowerCase();
30
+ const routeSchema = route.options.schema;
31
+ const ai = route.options.ai;
32
+
33
+ const operation: Record<string, any> = {
34
+ summary: ai?.description || `${route.method} ${openApiPath}`,
35
+ description: ai?.description,
36
+ parameters: [],
37
+ responses: {
38
+ '200': {
39
+ description: 'Successful Response',
40
+ content: {
41
+ 'application/json': {
42
+ schema: routeSchema?.response && routeSchema.response instanceof BaseSchema
43
+ ? routeSchema.response.toOpenAPI()
44
+ : { type: 'object' },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ if (ai?.tags) {
52
+ operation.tags = ai.tags;
53
+ }
54
+
55
+ // Path parameters
56
+ if (routeSchema?.params && routeSchema.params instanceof BaseSchema) {
57
+ const openApiParams = routeSchema.params.toOpenAPI();
58
+ if (openApiParams.properties) {
59
+ for (const [pKey, pSchema] of Object.entries(openApiParams.properties)) {
60
+ operation.parameters.push({
61
+ name: pKey,
62
+ in: 'path',
63
+ required: true,
64
+ schema: pSchema,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ // Query parameters
71
+ if (routeSchema?.query && routeSchema.query instanceof BaseSchema) {
72
+ const openApiQuery = routeSchema.query.toOpenAPI();
73
+ if (openApiQuery.properties) {
74
+ for (const [qKey, qSchema] of Object.entries(openApiQuery.properties)) {
75
+ operation.parameters.push({
76
+ name: qKey,
77
+ in: 'query',
78
+ required: openApiQuery.required?.includes(qKey) ?? false,
79
+ schema: qSchema,
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ // Request body
86
+ if (routeSchema?.body && routeSchema.body instanceof BaseSchema) {
87
+ operation.requestBody = {
88
+ required: true,
89
+ content: {
90
+ 'application/json': {
91
+ schema: routeSchema.body.toOpenAPI(),
92
+ },
93
+ },
94
+ };
95
+ }
96
+
97
+ spec.paths[openApiPath][methodLower] = operation;
98
+ }
99
+
100
+ return spec;
101
+ }