@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,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';
|
package/src/jobs/cron.ts
ADDED
|
@@ -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
|
+
}
|