@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,108 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { hashPassword, verifyPassword } from '../src/auth/crypto';
3
+ import { signJWT, verifyJWT } from '../src/auth/jwt';
4
+ import { createAuth, requireAuth } from '../src/auth/auth';
5
+ import { createApp } from '../src/core/app';
6
+
7
+ describe('FireAPI Turnkey Auth Module (fire-api/auth)', () => {
8
+ it('hashes and verifies passwords securely using Web Crypto', async () => {
9
+ const rawPassword = 'SuperSecretPassword123!';
10
+ const hash = await hashPassword(rawPassword);
11
+
12
+ expect(hash).toContain(':'); // salt:hash format
13
+ expect(await verifyPassword(rawPassword, hash)).toBe(true);
14
+ expect(await verifyPassword('WrongPassword', hash)).toBe(false);
15
+ });
16
+
17
+ it('signs and verifies JWT tokens with expiration', async () => {
18
+ const secret = 'test-secret-key-super-safe';
19
+ const payload = { userId: 'usr_1', role: 'admin' };
20
+
21
+ const token = await signJWT(payload, secret, 3600); // 1 hour
22
+ expect(token.split('.').length).toBe(3);
23
+
24
+ const decoded = await verifyJWT<{ userId: string; role: string }>(token, secret);
25
+ expect(decoded.userId).toBe('usr_1');
26
+ expect(decoded.role).toBe('admin');
27
+ });
28
+
29
+ it('provides turnkey register and login routes', async () => {
30
+ const app = createApp({ database: { driver: 'memory' } });
31
+ const authRouter = createAuth({ jwtSecret: 'jwt-super-secret' });
32
+ app.use('/auth', authRouter);
33
+
34
+ // 1. Test Register
35
+ const regRes = await app.handle(
36
+ new Request('http://localhost:3000/auth/register', {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({
40
+ email: 'yoga@fire.dev',
41
+ password: 'Password123!',
42
+ name: 'Yoga',
43
+ }),
44
+ })
45
+ );
46
+
47
+ expect(regRes.status).toBe(201);
48
+ const regData = await regRes.json();
49
+ expect(regData.user.email).toBe('yoga@fire.dev');
50
+ expect(regData.token).toBeDefined();
51
+
52
+ // 2. Test Login (Success)
53
+ const loginRes = await app.handle(
54
+ new Request('http://localhost:3000/auth/login', {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({
58
+ email: 'yoga@fire.dev',
59
+ password: 'Password123!',
60
+ }),
61
+ })
62
+ );
63
+
64
+ expect(loginRes.status).toBe(200);
65
+ const loginData = await loginRes.json();
66
+ expect(loginData.token).toBeDefined();
67
+
68
+ // 3. Test Login (Invalid password)
69
+ const failRes = await app.handle(
70
+ new Request('http://localhost:3000/auth/login', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ email: 'yoga@fire.dev',
75
+ password: 'WrongPassword',
76
+ }),
77
+ })
78
+ );
79
+
80
+ expect(failRes.status).toBe(401);
81
+ });
82
+
83
+ it('enforces route protection with requireAuth guard', async () => {
84
+ const app = createApp();
85
+ const secret = 'jwt-guard-secret';
86
+
87
+ app.get('/protected', {
88
+ middlewares: [requireAuth(secret)],
89
+ handler: (ctx) => ctx.ok({ secretData: 'access granted', user: ctx.user }),
90
+ });
91
+
92
+ // Without token -> 401
93
+ const unauthRes = await app.handle(new Request('http://localhost:3000/protected'));
94
+ expect(unauthRes.status).toBe(401);
95
+
96
+ // With valid token -> 200
97
+ const token = await signJWT({ id: 'u1', name: 'Dev' }, secret);
98
+ const authRes = await app.handle(
99
+ new Request('http://localhost:3000/protected', {
100
+ headers: { Authorization: `Bearer ${token}` },
101
+ })
102
+ );
103
+ expect(authRes.status).toBe(200);
104
+ const data = await authRes.json();
105
+ expect(data.secretData).toBe('access granted');
106
+ expect(data.user.id).toBe('u1');
107
+ });
108
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { runBenchmark, formatBenchmarkReport } from '../src/cli/bench';
3
+
4
+ describe('FireAPI Built-in Benchmark Harness CLI', () => {
5
+ it('measures throughput and percentiles from mock load', async () => {
6
+ // Simulated fast API endpoint returning in 0.5ms - 1ms
7
+ const mockEndpoint = async () => {
8
+ await new Promise((r) => setTimeout(r, 1));
9
+ return new Response(JSON.stringify({ status: 'ok' }));
10
+ };
11
+
12
+ const stats = await runBenchmark('http://localhost:3000/mock', {
13
+ concurrency: 5,
14
+ totalRequests: 50,
15
+ customFetch: mockEndpoint,
16
+ });
17
+
18
+ expect(stats.totalRequests).toBe(50);
19
+ expect(stats.successfulRequests).toBe(50);
20
+ expect(stats.failedRequests).toBe(0);
21
+ expect(stats.rps).toBeGreaterThan(0);
22
+ expect(stats.latency.p50).toBeGreaterThanOrEqual(0);
23
+ expect(stats.latency.p99).toBeGreaterThanOrEqual(stats.latency.p50);
24
+
25
+ const report = formatBenchmarkReport(stats);
26
+ expect(report).toContain('FireAPI Benchmark Engine');
27
+ expect(report).toContain('Requests/sec:');
28
+ expect(report).toContain('P50:');
29
+ expect(report).toContain('P99:');
30
+ });
31
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { MemoryCacheManager } from '../src/cache/cache';
3
+
4
+ describe('FireAPI FireCache SWR Engine', () => {
5
+ it('stores and retrieves cached values with TTL', async () => {
6
+ const cache = new MemoryCacheManager();
7
+ await cache.set('user:1', { name: 'Yoga' }, 10); // 10 seconds
8
+
9
+ const item = await cache.get('user:1');
10
+ expect(item).toEqual({ name: 'Yoga' });
11
+ });
12
+
13
+ it('expires items when TTL has elapsed', async () => {
14
+ const cache = new MemoryCacheManager();
15
+ // Expire immediately (0.01s)
16
+ await cache.set('temp', 'gone_soon', 0.01);
17
+
18
+ await new Promise((r) => setTimeout(r, 20));
19
+ const item = await cache.get('temp');
20
+ expect(item).toBeNull();
21
+ });
22
+
23
+ it('invalidates cache entries by tags', async () => {
24
+ const cache = new MemoryCacheManager();
25
+ await cache.set('post:1', { title: 'First Post' }, 60, ['posts', 'author:1']);
26
+ await cache.set('post:2', { title: 'Second Post' }, 60, ['posts', 'author:2']);
27
+ await cache.set('user:1', { name: 'Author One' }, 60, ['users']);
28
+
29
+ // Invalidate 'posts' tag
30
+ await cache.invalidate('posts');
31
+
32
+ expect(await cache.get('post:1')).toBeNull();
33
+ expect(await cache.get('post:2')).toBeNull();
34
+ expect(await cache.get('user:1')).toEqual({ name: 'Author One' }); // untouched!
35
+ });
36
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it, afterAll } from 'bun:test';
2
+ import { createAppProject, formatRoutesTable, createRouter } from '../src/index';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ describe('FireAPI CLI Scaffolding & Developer Accelerator (create-app & routes)', () => {
7
+ const tempProjectDir = path.resolve('./temp_scaffold_test_app');
8
+
9
+ afterAll(() => {
10
+ if (fs.existsSync(tempProjectDir)) {
11
+ fs.rmSync(tempProjectDir, { recursive: true, force: true });
12
+ }
13
+ });
14
+
15
+ it('scaffolds a complete, clean, modern FireAPI project with all files', () => {
16
+ const result = createAppProject({
17
+ name: 'test-saas-api',
18
+ targetDir: tempProjectDir,
19
+ });
20
+
21
+ expect(result.filesCreated.length).toBeGreaterThanOrEqual(10);
22
+ expect(fs.existsSync(path.join(tempProjectDir, 'package.json'))).toBe(true);
23
+ expect(fs.existsSync(path.join(tempProjectDir, 'tsconfig.json'))).toBe(true);
24
+ expect(fs.existsSync(path.join(tempProjectDir, '.env'))).toBe(true);
25
+ expect(fs.existsSync(path.join(tempProjectDir, '.gitignore'))).toBe(true);
26
+ expect(fs.existsSync(path.join(tempProjectDir, 'src/index.ts'))).toBe(true);
27
+ expect(fs.existsSync(path.join(tempProjectDir, 'src/modules/users/users.router.ts'))).toBe(true);
28
+ expect(fs.existsSync(path.join(tempProjectDir, 'src/modules/users/users.schema.ts'))).toBe(true);
29
+ expect(fs.existsSync(path.join(tempProjectDir, 'src/modules/auth/auth.router.ts'))).toBe(true);
30
+ expect(fs.existsSync(path.join(tempProjectDir, 'test/app.test.ts'))).toBe(true);
31
+
32
+ // Verify package.json contains correct scripts
33
+ const pkg = JSON.parse(fs.readFileSync(path.join(tempProjectDir, 'package.json'), 'utf-8'));
34
+ expect(pkg.name).toBe('test-saas-api');
35
+ expect(pkg.scripts.dev).toContain('bun');
36
+ expect(pkg.scripts.compile).toContain('fire compile');
37
+ });
38
+
39
+ it('renders a visual terminal route table via formatRoutesTable()', () => {
40
+ const router = createRouter('/api/v1');
41
+
42
+ router.get('/items', {
43
+ cache: { ttl: 60 },
44
+ ai: { description: 'Fetch all catalog items' },
45
+ handler: (ctx) => ctx.ok([]),
46
+ });
47
+
48
+ router.post('/items', {
49
+ idempotent: true,
50
+ roles: ['admin', 'manager'],
51
+ ai: { description: 'Create a new catalog item' },
52
+ handler: (ctx) => ctx.created({}),
53
+ });
54
+
55
+ const output = formatRoutesTable(router);
56
+
57
+ expect(output).toContain('FireAPI Route Manifest');
58
+ expect(output).toContain('GET');
59
+ expect(output).toContain('/api/v1/items');
60
+ expect(output).toContain('POST');
61
+ expect(output).toContain('admin, manager');
62
+ expect(output).toContain('Fetch all catalog items');
63
+ });
64
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createClient } from '../src/client';
3
+
4
+ describe('FireAPI FireClient E2E Type-Safe RPC', () => {
5
+ it('constructs correct HTTP requests from proxy path calls', async () => {
6
+ let capturedUrl = '';
7
+ let capturedMethod = '';
8
+ let capturedBody = '';
9
+
10
+ const mockFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
11
+ capturedUrl = input.toString();
12
+ capturedMethod = init?.method || 'GET';
13
+ capturedBody = init?.body ? String(init.body) : '';
14
+ return new Response(JSON.stringify({ ok: true, id: '123' }), {
15
+ status: 200,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ });
18
+ };
19
+
20
+ const client = createClient('http://localhost:3000', { fetch: mockFetch });
21
+
22
+ // Call GET /api/v1/health
23
+ const res1 = await client.api.v1.health.get();
24
+ expect(capturedUrl).toBe('http://localhost:3000/api/v1/health');
25
+ expect(capturedMethod).toBe('GET');
26
+ expect(res1.ok).toBe(true);
27
+ expect(res1.data).toEqual({ ok: true, id: '123' });
28
+
29
+ // Call POST /users with body
30
+ const res2 = await client.users.post({
31
+ body: { name: 'Alice', email: 'alice@fire.dev' },
32
+ });
33
+ expect(capturedUrl).toBe('http://localhost:3000/users');
34
+ expect(capturedMethod).toBe('POST');
35
+ expect(JSON.parse(capturedBody)).toEqual({ name: 'Alice', email: 'alice@fire.dev' });
36
+
37
+ // Call GET /users/:id with params substitution
38
+ await client.users[':id'].get({ params: { id: 'usr_99' } });
39
+ expect(capturedUrl).toBe('http://localhost:3000/users/usr_99');
40
+ });
41
+
42
+ it('handles query parameters accurately', async () => {
43
+ let capturedUrl = '';
44
+ const mockFetch = async (input: RequestInfo | URL) => {
45
+ capturedUrl = input.toString();
46
+ return new Response(JSON.stringify([]));
47
+ };
48
+
49
+ const client = createClient('http://localhost:3000', { fetch: mockFetch });
50
+ await client.posts.get({ query: { page: '2', limit: '10' } });
51
+
52
+ expect(capturedUrl).toBe('http://localhost:3000/posts?page=2&limit=10');
53
+ });
54
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createApp, createRouter, s } from '../src/index';
3
+
4
+ describe('FireAPI One-Click Postman & Bruno Collection Exporter', () => {
5
+ it('serves Postman Collection v2.1.0 specification at /docs/collection.json', async () => {
6
+ const app = createApp({
7
+ docs: '/docs',
8
+ openapiPath: '/openapi.json',
9
+ });
10
+
11
+ const router = createRouter('/articles');
12
+ router.get('/', {
13
+ ai: { description: 'Get all articles' },
14
+ handler: (ctx) => ctx.ok([]),
15
+ });
16
+
17
+ router.post('/', {
18
+ schema: {
19
+ body: s.object({
20
+ title: s.string().min(5),
21
+ content: s.string().min(10),
22
+ }),
23
+ },
24
+ ai: { description: 'Create an article' },
25
+ handler: (ctx) => ctx.created(ctx.body),
26
+ });
27
+
28
+ app.use(router);
29
+
30
+ const res = await app.handle(new Request('http://localhost:3000/docs/collection.json'));
31
+ expect(res.status).toBe(200);
32
+
33
+ const collection = await res.json();
34
+ expect(collection.info.schema).toContain('v2.1.0');
35
+ expect(Array.isArray(collection.item)).toBe(true);
36
+
37
+ const getArticle = collection.item.find((i: any) => i.name === 'GET /articles');
38
+ expect(getArticle).toBeDefined();
39
+ expect(getArticle.request.method).toBe('GET');
40
+
41
+ const postArticle = collection.item.find((i: any) => i.name === 'POST /articles');
42
+ expect(postArticle).toBeDefined();
43
+ expect(postArticle.request.method).toBe('POST');
44
+ expect(postArticle.request.body).toBeDefined();
45
+ expect(postArticle.request.body.mode).toBe('raw');
46
+
47
+ // Parse the auto-generated mock body inside the collection!
48
+ const mockPayload = JSON.parse(postArticle.request.body.raw);
49
+ expect(typeof mockPayload.title).toBe('string');
50
+ expect(typeof mockPayload.content).toBe('string');
51
+ });
52
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { fromExpress } from '../src/compat/express';
3
+ import { createApp } from '../src/core/app';
4
+
5
+ describe('FireAPI Express Compatibility Bridge (fire-api/compat)', () => {
6
+ it('bridges standard Express middleware that modifies headers or state', async () => {
7
+ const app = createApp();
8
+
9
+ // Standard Express middleware: (req, res, next)
10
+ const expressLogger = (req: any, res: any, next: any) => {
11
+ req.customId = 'exp_123';
12
+ res.setHeader('X-Express-Bridged', 'true');
13
+ next();
14
+ };
15
+
16
+ app.get('/test-compat', {
17
+ middlewares: [fromExpress(expressLogger)],
18
+ handler: (ctx) => {
19
+ return ctx.ok({ bridgedId: (ctx.req as any).customId });
20
+ },
21
+ });
22
+
23
+ const res = await app.handle(new Request('http://localhost:3000/test-compat'));
24
+ expect(res.status).toBe(200);
25
+ expect(res.headers.get('x-express-bridged')).toBe('true');
26
+ const data = await res.json();
27
+ expect(data.bridgedId).toBe('exp_123');
28
+ });
29
+
30
+ it('handles early termination from Express middleware (e.g. res.status().json())', async () => {
31
+ const app = createApp();
32
+
33
+ // Middleware that intercepts and rejects early
34
+ const expressBlocker = (_req: any, res: any, _next: any) => {
35
+ res.status(403).json({ blockedBy: 'express-guard' });
36
+ };
37
+
38
+ app.get('/blocked', {
39
+ middlewares: [fromExpress(expressBlocker)],
40
+ handler: (ctx) => ctx.ok({ secret: 'never reached' }),
41
+ });
42
+
43
+ const res = await app.handle(new Request('http://localhost:3000/blocked'));
44
+ expect(res.status).toBe(403);
45
+ const data = await res.json();
46
+ expect(data.blockedBy).toBe('express-guard');
47
+ });
48
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { compressResponse } from '../src/shield/compress';
3
+
4
+ describe('FireAPI Native HTTP Stream Compression (Gzip / Web Standard)', () => {
5
+ it('compresses JSON response when client accepts gzip and payload exceeds threshold', async () => {
6
+ // Generate JSON payload larger than 1KB
7
+ const largeData = { items: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Item ${i}` })) };
8
+ const rawResponse = new Response(JSON.stringify(largeData), {
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+
12
+ const reqWithGzip = new Request('http://localhost:3000/items', {
13
+ headers: { 'Accept-Encoding': 'gzip, deflate' },
14
+ });
15
+
16
+ const compressed = await compressResponse(rawResponse, reqWithGzip, { threshold: 100 });
17
+
18
+ expect(compressed.headers.get('Content-Encoding')).toBe('gzip');
19
+ expect(compressed.headers.get('Vary')).toContain('Accept-Encoding');
20
+
21
+ // Decompress with DecompressionStream to verify integrity
22
+ const decompressedStream = compressed.body!.pipeThrough(new DecompressionStream('gzip'));
23
+ const text = await new Response(decompressedStream).text();
24
+ expect(JSON.parse(text)).toEqual(largeData);
25
+ });
26
+
27
+ it('skips compression if payload is smaller than threshold', async () => {
28
+ const tinyResponse = new Response(JSON.stringify({ ok: true }), {
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ const reqWithGzip = new Request('http://localhost:3000/tiny', {
32
+ headers: { 'Accept-Encoding': 'gzip' },
33
+ });
34
+
35
+ const result = await compressResponse(tinyResponse, reqWithGzip, { threshold: 1000 });
36
+ expect(result.headers.get('Content-Encoding')).toBeNull();
37
+ });
38
+
39
+ it('skips compression if client does not accept gzip', async () => {
40
+ const rawResponse = new Response(JSON.stringify({ text: 'a'.repeat(2000) }), {
41
+ headers: { 'Content-Type': 'application/json' },
42
+ });
43
+ const reqWithoutGzip = new Request('http://localhost:3000/no-gzip');
44
+
45
+ const result = await compressResponse(rawResponse, reqWithoutGzip, { threshold: 100 });
46
+ expect(result.headers.get('Content-Encoding')).toBeNull();
47
+ });
48
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createContext } from '../src/core/context';
3
+ import { HttpError } from '../src/core/error';
4
+
5
+ describe('FireAPI FireContext & Error Handling', () => {
6
+ it('parses URL, query parameters and route parameters', () => {
7
+ const req = new Request('http://localhost:3000/users/42?filter=active&sort=desc');
8
+ const ctx = createContext({
9
+ req,
10
+ params: { id: '42' },
11
+ });
12
+
13
+ expect(ctx.params.id).toBe('42');
14
+ expect(ctx.query.filter).toBe('active');
15
+ expect(ctx.query.sort).toBe('desc');
16
+ });
17
+
18
+ it('produces standard JSON responses with status codes', async () => {
19
+ const req = new Request('http://localhost:3000/items');
20
+ const ctx = createContext({ req });
21
+
22
+ const okRes = ctx.ok({ success: true });
23
+ expect(okRes.status).toBe(200);
24
+ expect(await okRes.json()).toEqual({ success: true });
25
+
26
+ const createdRes = ctx.created({ id: 'new_1' });
27
+ expect(createdRes.status).toBe(201);
28
+ expect(await createdRes.json()).toEqual({ id: 'new_1' });
29
+
30
+ const notFoundRes = ctx.notFound('Item not found');
31
+ expect(notFoundRes.status).toBe(404);
32
+ expect(await notFoundRes.json()).toEqual({
33
+ statusCode: 404,
34
+ error: 'Not Found',
35
+ message: 'Item not found',
36
+ });
37
+ });
38
+
39
+ it('supports custom HttpError throwing and serialization', () => {
40
+ const err = new HttpError(400, 'Invalid payload', { field: 'email' });
41
+ expect(err.statusCode).toBe(400);
42
+ expect(err.message).toBe('Invalid payload');
43
+ expect(err.details).toEqual({ field: 'email' });
44
+
45
+ const response = err.toResponse();
46
+ expect(response.status).toBe(400);
47
+ });
48
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { MemoryDatabase } from '../src/db/memory-adapter';
3
+ import { createDatabaseHub } from '../src/db/hub';
4
+
5
+ describe('FireAPI FireDB Universal Hub & Memory Adapter', () => {
6
+ it('supports insert, find, findById, and where filtering', async () => {
7
+ const db = new MemoryDatabase();
8
+
9
+ const inserted = await db.table('users').insert({
10
+ name: 'Yoga',
11
+ email: 'yoga@fire.dev',
12
+ role: 'admin',
13
+ });
14
+
15
+ expect(inserted.id).toBeDefined();
16
+ expect(inserted.name).toBe('Yoga');
17
+
18
+ const users = await db.table('users').where('role', 'admin').find();
19
+ expect(users.length).toBe(1);
20
+ expect(users[0].email).toBe('yoga@fire.dev');
21
+
22
+ const userById = await db.table('users').findById(inserted.id);
23
+ expect(userById?.name).toBe('Yoga');
24
+ });
25
+
26
+ it('supports update and delete operations', async () => {
27
+ const db = new MemoryDatabase();
28
+
29
+ const item = await db.table('products').insert({ title: 'MacBook', price: 2000 });
30
+
31
+ // Update
32
+ const updated = await db.table('products').where('id', item.id).update({ price: 1800 });
33
+ expect(updated.price).toBe(1800);
34
+
35
+ // Delete
36
+ const deleted = await db.table('products').where('id', item.id).delete();
37
+ expect(deleted.count).toBe(1);
38
+
39
+ const remaining = await db.table('products').find();
40
+ expect(remaining.length).toBe(0);
41
+ });
42
+
43
+ it('supports transactions in memory adapter', async () => {
44
+ const db = new MemoryDatabase();
45
+
46
+ await db.transaction(async (trx) => {
47
+ await trx.table('logs').insert({ action: 'audit_start' });
48
+ await trx.table('logs').insert({ action: 'audit_end' });
49
+ });
50
+
51
+ const logs = await db.table('logs').find();
52
+ expect(logs.length).toBe(2);
53
+ });
54
+
55
+ it('initializes seamlessly via createDatabaseHub', async () => {
56
+ const hub = createDatabaseHub({ driver: 'memory', autoMock: true });
57
+ expect(hub).toBeDefined();
58
+
59
+ const res = await hub.table('test').insert({ status: 'ready' });
60
+ expect(res.status).toBe('ready');
61
+ });
62
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { generateDockerFiles } from '../src/cli/docker';
5
+
6
+ describe('FireAPI One-Command Distroless Docker Synthesizer', () => {
7
+ it('generates production-hardened multi-stage Dockerfile and .dockerignore', () => {
8
+ const testDir = path.join(process.cwd(), 'temp_test_docker');
9
+ if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
10
+
11
+ const result = generateDockerFiles(testDir);
12
+
13
+ expect(fs.existsSync(result.dockerfilePath)).toBe(true);
14
+ expect(fs.existsSync(result.dockerignorePath)).toBe(true);
15
+
16
+ const dockerfileContent = fs.readFileSync(result.dockerfilePath, 'utf-8');
17
+ expect(dockerfileContent).toContain('FROM oven/bun:alpine AS base');
18
+ expect(dockerfileContent).toContain('USER appuser');
19
+ expect(dockerfileContent).toContain('EXPOSE 3000');
20
+
21
+ const ignoreContent = fs.readFileSync(result.dockerignorePath, 'utf-8');
22
+ expect(ignoreContent).toContain('node_modules');
23
+ expect(ignoreContent).toContain('.git');
24
+
25
+ // Clean up
26
+ fs.rmSync(testDir, { recursive: true, force: true });
27
+ });
28
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createApp, createRouter } from '../src/index';
3
+
4
+ describe('FireAPI Typed In-Process Event Bus (app.on, ctx.emit)', () => {
5
+ it('subscribes to and receives typed events triggered via ctx.emit()', async () => {
6
+ const app = createApp();
7
+ const router = createRouter();
8
+
9
+ let eventReceived: any = null;
10
+
11
+ app.on('order.paid', async (payload) => {
12
+ eventReceived = payload;
13
+ });
14
+
15
+ router.post('/checkout', async (ctx) => {
16
+ await ctx.emit('order.paid', { orderId: 'ORD_999', total: 450 });
17
+ return ctx.ok({ status: 'dispatched' });
18
+ });
19
+
20
+ app.use(router);
21
+
22
+ const res = await app.handle(
23
+ new Request('http://localhost:3000/checkout', { method: 'POST' })
24
+ );
25
+
26
+ expect(res.status).toBe(200);
27
+ expect(eventReceived).not.toBeNull();
28
+ expect(eventReceived.orderId).toBe('ORD_999');
29
+ expect(eventReceived.total).toBe(450);
30
+ });
31
+
32
+ it('supports wildcard listeners (e.g. user.*)', async () => {
33
+ const app = createApp();
34
+ const capturedEvents: string[] = [];
35
+
36
+ app.on('user.*', async (payload) => {
37
+ capturedEvents.push(payload.action);
38
+ });
39
+
40
+ await app.emit('user.registered', { action: 'REGISTER' });
41
+ await app.emit('user.login', { action: 'LOGIN' });
42
+ await app.emit('order.created', { action: 'ORDER' }); // Should NOT match
43
+
44
+ expect(capturedEvents).toEqual(['REGISTER', 'LOGIN']);
45
+ });
46
+
47
+ it('allows unsubscribing from events cleanly', async () => {
48
+ const app = createApp();
49
+ let callCount = 0;
50
+
51
+ const unsubscribe = app.on('ping', () => {
52
+ callCount++;
53
+ });
54
+
55
+ await app.emit('ping', {});
56
+ expect(callCount).toBe(1);
57
+
58
+ unsubscribe();
59
+ await app.emit('ping', {});
60
+ expect(callCount).toBe(1); // Not called after unsubscribe
61
+ });
62
+ });