@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.
Files changed (98) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bun.lock +20 -0
  4. package/docs/GETTING_STARTED.md +150 -0
  5. package/docs/LAUNCH_KIT.md +236 -0
  6. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  7. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  8. package/examples/basic-api/index.ts +177 -0
  9. package/package.json +68 -4
  10. package/src/ai/mcp.ts +140 -0
  11. package/src/auth/auth.ts +110 -0
  12. package/src/auth/crypto.ts +70 -0
  13. package/src/auth/jwt.ts +105 -0
  14. package/src/cache/cache.ts +72 -0
  15. package/src/cli/bench.ts +118 -0
  16. package/src/cli/compile.ts +29 -0
  17. package/src/cli/create-app.ts +284 -0
  18. package/src/cli/docker.ts +51 -0
  19. package/src/cli/generator.ts +75 -0
  20. package/src/cli/routes.ts +76 -0
  21. package/src/client.ts +120 -0
  22. package/src/compat/express.ts +139 -0
  23. package/src/core/app.ts +723 -0
  24. package/src/core/context.ts +218 -0
  25. package/src/core/error.ts +42 -0
  26. package/src/core/router.ts +262 -0
  27. package/src/db/db-interface.ts +9 -0
  28. package/src/db/hub.ts +256 -0
  29. package/src/db/memory-adapter.ts +202 -0
  30. package/src/db/migrator.ts +100 -0
  31. package/src/db/sqlite-adapter.ts +58 -0
  32. package/src/docs/collection.ts +57 -0
  33. package/src/docs/scalar-ui.ts +25 -0
  34. package/src/events/event-bus.ts +61 -0
  35. package/src/index.ts +37 -0
  36. package/src/jobs/cron.ts +77 -0
  37. package/src/jobs/queue.ts +108 -0
  38. package/src/logging/logger.ts +66 -0
  39. package/src/metrics/metrics.ts +66 -0
  40. package/src/modules/product/product.route.ts +36 -0
  41. package/src/modules/product/product.schema.ts +10 -0
  42. package/src/schema/mock.ts +60 -0
  43. package/src/schema/openapi.ts +101 -0
  44. package/src/schema/schema.ts +519 -0
  45. package/src/schema/validator.ts +5 -0
  46. package/src/shield/compress.ts +61 -0
  47. package/src/shield/cors.ts +94 -0
  48. package/src/shield/headers.ts +55 -0
  49. package/src/shield/idempotency.ts +76 -0
  50. package/src/shield/rate-limiter.ts +81 -0
  51. package/src/shield/sanitizer.ts +23 -0
  52. package/src/shield/webhook.ts +101 -0
  53. package/src/static/serve.ts +51 -0
  54. package/src/storage/storage.ts +176 -0
  55. package/src/stream/sse.ts +89 -0
  56. package/src/studio/studio.ts +221 -0
  57. package/src/types.ts +241 -0
  58. package/src/ws/ws.ts +118 -0
  59. package/test/ai-mcp.test.ts +61 -0
  60. package/test/auth.test.ts +108 -0
  61. package/test/bench.test.ts +31 -0
  62. package/test/cache.test.ts +36 -0
  63. package/test/cli-create.test.ts +64 -0
  64. package/test/client.test.ts +54 -0
  65. package/test/collection-export.test.ts +52 -0
  66. package/test/compat.test.ts +48 -0
  67. package/test/compress.test.ts +48 -0
  68. package/test/context.test.ts +48 -0
  69. package/test/db.test.ts +62 -0
  70. package/test/docker.test.ts +28 -0
  71. package/test/events.test.ts +62 -0
  72. package/test/file-upload.test.ts +116 -0
  73. package/test/hooks.test.ts +77 -0
  74. package/test/idempotency.test.ts +65 -0
  75. package/test/integration.test.ts +111 -0
  76. package/test/jobs.test.ts +60 -0
  77. package/test/metrics.test.ts +40 -0
  78. package/test/mock.test.ts +33 -0
  79. package/test/multi-tenant.test.ts +80 -0
  80. package/test/observability.test.ts +95 -0
  81. package/test/openapi.test.ts +41 -0
  82. package/test/orm-relations.test.ts +85 -0
  83. package/test/orm-shortcuts.test.ts +52 -0
  84. package/test/rbac.test.ts +85 -0
  85. package/test/router-fastpath.test.ts +42 -0
  86. package/test/router.test.ts +84 -0
  87. package/test/schema.test.ts +49 -0
  88. package/test/seeder-migrator.test.ts +78 -0
  89. package/test/shield.test.ts +55 -0
  90. package/test/shorthand-dx.test.ts +31 -0
  91. package/test/sse.test.ts +43 -0
  92. package/test/static-serve.test.ts +53 -0
  93. package/test/storage.test.ts +31 -0
  94. package/test/studio.test.ts +43 -0
  95. package/test/tx.test.ts +92 -0
  96. package/test/webhook.test.ts +113 -0
  97. package/test/ws.test.ts +54 -0
  98. package/tsconfig.json +19 -0
package/src/db/hub.ts ADDED
@@ -0,0 +1,256 @@
1
+ import { FireDatabase, FireDatabaseOptions, QueryBuilder, TableModel } from '../types';
2
+ import { MemoryDatabase } from './memory-adapter';
3
+ import { SQLiteDatabase } from './sqlite-adapter';
4
+ import { generateMock } from '../schema/mock';
5
+
6
+ function toSingular(name: string): string {
7
+ if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
8
+ if (name.endsWith('es') && !name.endsWith('ses')) return name.slice(0, -2);
9
+ if (name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1);
10
+ return name;
11
+ }
12
+
13
+ function toPlural(name: string): string {
14
+ if (name.endsWith('y') && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + 'ies';
15
+ if (name.endsWith('s')) return name;
16
+ return name + 's';
17
+ }
18
+
19
+ async function attachRelations(
20
+ db: FireDatabase,
21
+ parentTableName: string,
22
+ rows: Record<string, any>[],
23
+ relations: string[]
24
+ ): Promise<void> {
25
+ if (!rows || rows.length === 0 || !relations || relations.length === 0) return;
26
+
27
+ const parentSingular = toSingular(parentTableName);
28
+ const possibleParentFkNames = [
29
+ `${parentSingular}Id`,
30
+ `${parentSingular}_id`,
31
+ `${parentTableName}Id`,
32
+ `${parentTableName}_id`,
33
+ ];
34
+
35
+ for (const rel of relations) {
36
+ const isPluralRelation = rel.endsWith('s') || rel.endsWith('ies');
37
+ const relSingular = toSingular(rel);
38
+ const relPlural = toPlural(rel);
39
+ const candidateTableNames = [rel, relPlural, relSingular];
40
+
41
+ for (const row of rows) {
42
+ if (!row) continue;
43
+ let resolved = false;
44
+
45
+ // 1. Belongs-To / Many-to-One
46
+ const possibleFkInParent = [
47
+ `${relSingular}Id`,
48
+ `${relSingular}_id`,
49
+ `${rel}Id`,
50
+ `${rel}_id`,
51
+ ];
52
+
53
+ for (const fk of possibleFkInParent) {
54
+ if (row[fk] !== undefined && row[fk] !== null) {
55
+ for (const tbl of candidateTableNames) {
56
+ try {
57
+ const relatedItem = await db.table(tbl).findById(row[fk]);
58
+ if (relatedItem) {
59
+ row[rel] = relatedItem;
60
+ resolved = true;
61
+ break;
62
+ }
63
+ } catch {}
64
+ }
65
+ if (resolved) break;
66
+ }
67
+ }
68
+
69
+ if (resolved) continue;
70
+
71
+ // 2. Has-Many / One-to-Many
72
+ for (const tbl of candidateTableNames) {
73
+ let matchingChildren: any[] = [];
74
+ for (const parentFk of possibleParentFkNames) {
75
+ try {
76
+ const results = await db.table(tbl).where(parentFk, row.id).find();
77
+ if (results && results.length > 0) {
78
+ matchingChildren = results;
79
+ break;
80
+ }
81
+ } catch {}
82
+ }
83
+
84
+ if (matchingChildren.length > 0 || isPluralRelation) {
85
+ row[rel] = isPluralRelation ? matchingChildren : (matchingChildren[0] || null);
86
+ resolved = true;
87
+ break;
88
+ }
89
+ }
90
+
91
+ if (!resolved && isPluralRelation) {
92
+ row[rel] = [];
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ function createTableModel(
99
+ target: FireDatabase,
100
+ tableName: string,
101
+ includedRelations: string[] = [],
102
+ baseQb?: QueryBuilder,
103
+ tenantId?: string
104
+ ): TableModel {
105
+ const getQb = () => {
106
+ let qb = baseQb || target.table(tableName);
107
+ if (tenantId && !baseQb) {
108
+ qb = qb.where('tenantId', tenantId);
109
+ }
110
+ return qb;
111
+ };
112
+
113
+ const model: TableModel = {
114
+ include: (relation: string | string[]) => {
115
+ const rels = Array.isArray(relation) ? relation : [relation];
116
+ return createTableModel(target, tableName, [...includedRelations, ...rels], baseQb, tenantId);
117
+ },
118
+ where: (field: string, opOrVal: any, val?: any) => {
119
+ const newQb = getQb().where(field, opOrVal, val);
120
+ return createTableModel(target, tableName, includedRelations, newQb, tenantId);
121
+ },
122
+ limit: (count: number) => {
123
+ const newQb = getQb().limit(count);
124
+ return createTableModel(target, tableName, includedRelations, newQb, tenantId);
125
+ },
126
+ offset: (count: number) => {
127
+ const newQb = getQb().offset(count);
128
+ return createTableModel(target, tableName, includedRelations, newQb, tenantId);
129
+ },
130
+ orderBy: (field: string, direction?: 'asc' | 'desc') => {
131
+ const newQb = getQb().orderBy(field, direction);
132
+ return createTableModel(target, tableName, includedRelations, newQb, tenantId);
133
+ },
134
+ find: async (filter?: Record<string, any>) => {
135
+ let qb = getQb();
136
+ if (filter) {
137
+ for (const [k, v] of Object.entries(filter)) {
138
+ qb = qb.where(k, v);
139
+ }
140
+ }
141
+ const rows = await qb.find();
142
+ if (includedRelations.length > 0) {
143
+ await attachRelations(target, tableName, rows, includedRelations);
144
+ }
145
+ return rows;
146
+ },
147
+ findOne: async (filter?: Record<string, any>) => {
148
+ let qb = getQb();
149
+ if (filter) {
150
+ for (const [k, v] of Object.entries(filter)) {
151
+ qb = qb.where(k, v);
152
+ }
153
+ }
154
+ const row = await qb.findOne();
155
+ if (row && includedRelations.length > 0) {
156
+ await attachRelations(target, tableName, [row], includedRelations);
157
+ }
158
+ return row;
159
+ },
160
+ findById: async (id: string | number) => {
161
+ const row = await getQb().findById(id);
162
+ if (row && includedRelations.length > 0) {
163
+ await attachRelations(target, tableName, [row], includedRelations);
164
+ }
165
+ return row;
166
+ },
167
+ insert: (data: any) => {
168
+ if (tenantId) {
169
+ if (Array.isArray(data)) {
170
+ data = data.map((d) => ({ tenantId, ...d }));
171
+ } else if (typeof data === 'object' && data !== null) {
172
+ data = { tenantId, ...data };
173
+ }
174
+ }
175
+ return getQb().insert(data);
176
+ },
177
+ update: (idOrFilter: any, data?: any) => {
178
+ if (data === undefined) {
179
+ return getQb().update(idOrFilter);
180
+ }
181
+ return getQb().where('id', idOrFilter).update(data);
182
+ },
183
+ delete: (idOrFilter?: any) => {
184
+ if (idOrFilter === undefined) {
185
+ return getQb().delete();
186
+ }
187
+ if (typeof idOrFilter === 'string' || typeof idOrFilter === 'number') {
188
+ return getQb().where('id', idOrFilter).delete();
189
+ }
190
+ let qb = getQb();
191
+ for (const [k, v] of Object.entries(idOrFilter)) {
192
+ qb = qb.where(k, v);
193
+ }
194
+ return qb.delete();
195
+ },
196
+ };
197
+
198
+ return model;
199
+ }
200
+
201
+ export function createProxyDatabase(db: FireDatabase, options?: { tenantId?: string }): FireDatabase {
202
+ return new Proxy(db, {
203
+ get(target, prop: string) {
204
+ if (prop === 'seed') {
205
+ return async (tbl: string, schema: any, count = 10) => {
206
+ const items = Array.from({ length: count }, () => {
207
+ const mock = generateMock(schema);
208
+ if (options?.tenantId) {
209
+ mock.tenantId = options.tenantId;
210
+ }
211
+ return mock;
212
+ });
213
+ return target.table(tbl).insert(items);
214
+ };
215
+ }
216
+
217
+ if (prop === 'transaction' || prop === 'tx') {
218
+ return (fn: (trx: FireDatabase) => Promise<any>) => {
219
+ const rawTrxFn = (target.tx || target.transaction).bind(target);
220
+ return rawTrxFn((rawTrx: FireDatabase) => {
221
+ return fn(createProxyDatabase(rawTrx, options));
222
+ });
223
+ };
224
+ }
225
+
226
+ if (prop in target || typeof prop === 'symbol') {
227
+ const val = (target as any)[prop];
228
+ return typeof val === 'function' ? val.bind(target) : val;
229
+ }
230
+
231
+ // Dynamic table model access: ctx.db.products -> TableModel
232
+ return createTableModel(target, prop, [], undefined, options?.tenantId);
233
+ },
234
+ });
235
+ }
236
+
237
+ export function createDatabaseHub(options: FireDatabaseOptions = {}): FireDatabase {
238
+ const driver = options.driver || 'memory';
239
+ let rawDb: FireDatabase;
240
+
241
+ switch (driver) {
242
+ case 'sqlite':
243
+ rawDb = new SQLiteDatabase(options.url || ':memory:');
244
+ break;
245
+ case 'postgres':
246
+ case 'mysql':
247
+ rawDb = new MemoryDatabase();
248
+ break;
249
+ case 'memory':
250
+ default:
251
+ rawDb = new MemoryDatabase();
252
+ break;
253
+ }
254
+
255
+ return createProxyDatabase(rawDb);
256
+ }
@@ -0,0 +1,202 @@
1
+ import { FireDatabase, QueryBuilder } from '../types';
2
+ import { WhereClause } from './db-interface';
3
+
4
+ export class MemoryQueryBuilder implements QueryBuilder {
5
+ private clauses: WhereClause[] = [];
6
+ private limitCount?: number;
7
+ private offsetCount = 0;
8
+ private orderField?: string;
9
+ private orderDir: 'asc' | 'desc' = 'asc';
10
+
11
+ constructor(private storage: Map<string, Record<string, any>[]>, private tableName: string) {}
12
+
13
+ private getTableData(): Record<string, any>[] {
14
+ if (!this.storage.has(this.tableName)) {
15
+ this.storage.set(this.tableName, []);
16
+ }
17
+ return this.storage.get(this.tableName)!;
18
+ }
19
+
20
+ public where(field: string, operatorOrValue: any, value?: any): QueryBuilder {
21
+ if (value === undefined) {
22
+ this.clauses.push({ field, operator: '=', value: operatorOrValue });
23
+ } else {
24
+ this.clauses.push({ field, operator: operatorOrValue, value });
25
+ }
26
+ return this;
27
+ }
28
+
29
+ public limit(count: number): QueryBuilder {
30
+ this.limitCount = count;
31
+ return this;
32
+ }
33
+
34
+ public offset(count: number): QueryBuilder {
35
+ this.offsetCount = count;
36
+ return this;
37
+ }
38
+
39
+ public orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): QueryBuilder {
40
+ this.orderField = field;
41
+ this.orderDir = direction;
42
+ return this;
43
+ }
44
+
45
+ public include(_relation: string | string[]): QueryBuilder {
46
+ return this;
47
+ }
48
+
49
+ private filterRows(): Record<string, any>[] {
50
+ let rows = [...this.getTableData()];
51
+
52
+ // Apply where clauses
53
+ for (const clause of this.clauses) {
54
+ rows = rows.filter((row) => {
55
+ const val = row[clause.field];
56
+ switch (clause.operator) {
57
+ case '=': return val === clause.value;
58
+ case '!=': return val !== clause.value;
59
+ case '>': return val > clause.value;
60
+ case '>=': return val >= clause.value;
61
+ case '<': return val < clause.value;
62
+ case '<=': return val <= clause.value;
63
+ case 'in': return Array.isArray(clause.value) && clause.value.includes(val);
64
+ case 'like':
65
+ return typeof val === 'string' &&
66
+ val.toLowerCase().includes(String(clause.value).toLowerCase().replace(/%/g, ''));
67
+ default: return val === clause.value;
68
+ }
69
+ });
70
+ }
71
+
72
+ // Apply order
73
+ if (this.orderField) {
74
+ const field = this.orderField;
75
+ const factor = this.orderDir === 'asc' ? 1 : -1;
76
+ rows.sort((a, b) => {
77
+ if (a[field] < b[field]) return -1 * factor;
78
+ if (a[field] > b[field]) return 1 * factor;
79
+ return 0;
80
+ });
81
+ }
82
+
83
+ // Apply pagination
84
+ if (this.offsetCount > 0) {
85
+ rows = rows.slice(this.offsetCount);
86
+ }
87
+ if (this.limitCount !== undefined) {
88
+ rows = rows.slice(0, this.limitCount);
89
+ }
90
+
91
+ return rows;
92
+ }
93
+
94
+ public async find<T = any>(): Promise<T[]> {
95
+ return this.filterRows() as T[];
96
+ }
97
+
98
+ public async findOne<T = any>(): Promise<T | null> {
99
+ const rows = this.filterRows();
100
+ return rows.length > 0 ? (rows[0] as T) : null;
101
+ }
102
+
103
+ public async findById<T = any>(id: string | number): Promise<T | null> {
104
+ const rows = this.getTableData().filter((r) => String(r.id) === String(id));
105
+ return rows.length > 0 ? (rows[0] as T) : null;
106
+ }
107
+
108
+ public async insert<T = any>(data: Record<string, any> | Record<string, any>[]): Promise<T> {
109
+ const table = this.getTableData();
110
+ if (Array.isArray(data)) {
111
+ const insertedList = data.map((item, idx) => ({
112
+ id: item.id || `mem_${Date.now()}_${idx}_${Math.random().toString(36).slice(2, 7)}`,
113
+ createdAt: new Date().toISOString(),
114
+ ...item,
115
+ }));
116
+ table.push(...insertedList);
117
+ return insertedList as any;
118
+ } else {
119
+ const inserted = {
120
+ id: data.id || `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
121
+ createdAt: new Date().toISOString(),
122
+ ...data,
123
+ };
124
+ table.push(inserted);
125
+ return inserted as any;
126
+ }
127
+ }
128
+
129
+ public async update<T = any>(data: Record<string, any>): Promise<T> {
130
+ const targetRows = this.filterRows();
131
+ if (targetRows.length === 0) {
132
+ return null as any;
133
+ }
134
+
135
+ const table = this.getTableData();
136
+ let lastUpdated: any = null;
137
+
138
+ for (const target of targetRows) {
139
+ const idx = table.findIndex((r) => r.id === target.id);
140
+ if (idx !== -1) {
141
+ table[idx] = {
142
+ ...table[idx],
143
+ ...data,
144
+ updatedAt: new Date().toISOString(),
145
+ };
146
+ lastUpdated = table[idx];
147
+ }
148
+ }
149
+
150
+ return lastUpdated as T;
151
+ }
152
+
153
+ public async delete(): Promise<{ count: number }> {
154
+ const targetRows = this.filterRows();
155
+ const table = this.getTableData();
156
+ const targetIds = new Set(targetRows.map((r) => r.id));
157
+
158
+ const remaining = table.filter((r) => !targetIds.has(r.id));
159
+ this.storage.set(this.tableName, remaining);
160
+
161
+ return { count: targetRows.length };
162
+ }
163
+ }
164
+
165
+ export class MemoryDatabase implements FireDatabase {
166
+ private storage: Map<string, Record<string, any>[]> = new Map();
167
+
168
+ public table(name: string): QueryBuilder {
169
+ return new MemoryQueryBuilder(this.storage, name);
170
+ }
171
+
172
+ public async raw<T = any>(_sql: string, _params?: any[]): Promise<T> {
173
+ return [] as any;
174
+ }
175
+
176
+ public async transaction<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T> {
177
+ // Deep snapshot of current storage for automatic rollback
178
+ const snapshot = new Map<string, Record<string, any>[]>();
179
+ for (const [table, rows] of this.storage.entries()) {
180
+ snapshot.set(table, rows.map((r) => ({ ...r })));
181
+ }
182
+
183
+ try {
184
+ return await fn(this);
185
+ } catch (err) {
186
+ // Rollback on any thrown error
187
+ this.storage.clear();
188
+ for (const [table, rows] of snapshot.entries()) {
189
+ this.storage.set(table, rows);
190
+ }
191
+ throw err;
192
+ }
193
+ }
194
+
195
+ public async tx<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T> {
196
+ return this.transaction(fn);
197
+ }
198
+
199
+ public clear(): void {
200
+ this.storage.clear();
201
+ }
202
+ }
@@ -0,0 +1,100 @@
1
+ import { FireDatabase } from '../types';
2
+
3
+ export interface Migration {
4
+ name: string;
5
+ up: (db: FireDatabase) => Promise<void>;
6
+ down?: (db: FireDatabase) => Promise<void>;
7
+ }
8
+
9
+ export class FireMigrator {
10
+ constructor(private db: FireDatabase) {}
11
+
12
+ private async ensureMigrationTable(): Promise<void> {
13
+ try {
14
+ await this.db.raw(
15
+ `CREATE TABLE IF NOT EXISTS _fire_migrations (
16
+ id TEXT PRIMARY KEY,
17
+ name TEXT UNIQUE,
18
+ batch INTEGER,
19
+ appliedAt TEXT
20
+ )`
21
+ );
22
+ } catch {}
23
+ }
24
+
25
+ public async up(migrations: Migration[]): Promise<{ applied: string[]; batch: number }> {
26
+ await this.ensureMigrationTable();
27
+
28
+ let appliedRows: any[] = [];
29
+ try {
30
+ appliedRows = await this.db.table('_fire_migrations').find();
31
+ } catch {}
32
+
33
+ const appliedNames = new Set(appliedRows.map((r) => r.name));
34
+ const latestBatch = appliedRows.reduce((max, r) => Math.max(max, r.batch || 0), 0);
35
+ const newBatch = latestBatch + 1;
36
+ const applied: string[] = [];
37
+
38
+ for (const mig of migrations) {
39
+ if (!appliedNames.has(mig.name)) {
40
+ await mig.up(this.db);
41
+ await this.db.table('_fire_migrations').insert({
42
+ id: `mig_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
43
+ name: mig.name,
44
+ batch: newBatch,
45
+ appliedAt: new Date().toISOString(),
46
+ });
47
+ applied.push(mig.name);
48
+ }
49
+ }
50
+
51
+ return { applied, batch: newBatch };
52
+ }
53
+
54
+ public async down(migrations: Migration[]): Promise<{ rolledBack: string[] }> {
55
+ await this.ensureMigrationTable();
56
+
57
+ let appliedRows: any[] = [];
58
+ try {
59
+ appliedRows = await this.db.table('_fire_migrations').find();
60
+ } catch {}
61
+
62
+ if (appliedRows.length === 0) return { rolledBack: [] };
63
+
64
+ const latestBatch = appliedRows.reduce((max, r) => Math.max(max, r.batch || 0), 0);
65
+ const targetRows = appliedRows.filter((r) => r.batch === latestBatch);
66
+ const rolledBack: string[] = [];
67
+
68
+ const migMap = new Map(migrations.map((m) => [m.name, m]));
69
+
70
+ for (const row of targetRows.reverse()) {
71
+ const mig = migMap.get(row.name);
72
+ if (mig && mig.down) {
73
+ await mig.down(this.db);
74
+ }
75
+ await this.db.table('_fire_migrations').where('name', row.name).delete();
76
+ rolledBack.push(row.name);
77
+ }
78
+
79
+ return { rolledBack };
80
+ }
81
+
82
+ public async status(migrations: Migration[]): Promise<Array<{ name: string; status: 'applied' | 'pending'; appliedAt?: string }>> {
83
+ await this.ensureMigrationTable();
84
+ let appliedRows: any[] = [];
85
+ try {
86
+ appliedRows = await this.db.table('_fire_migrations').find();
87
+ } catch {}
88
+
89
+ const appliedMap = new Map(appliedRows.map((r) => [r.name, r]));
90
+
91
+ return migrations.map((m) => {
92
+ const row = appliedMap.get(m.name);
93
+ return {
94
+ name: m.name,
95
+ status: row ? 'applied' : 'pending',
96
+ appliedAt: row ? row.appliedAt : undefined,
97
+ };
98
+ });
99
+ }
100
+ }
@@ -0,0 +1,58 @@
1
+ import { FireDatabase, QueryBuilder } from '../types';
2
+ import { MemoryDatabase } from './memory-adapter';
3
+
4
+ export class SQLiteDatabase implements FireDatabase {
5
+ private dbInstance: any;
6
+ private fallback: MemoryDatabase;
7
+
8
+ constructor(filePath = ':memory:') {
9
+ this.fallback = new MemoryDatabase();
10
+
11
+ // Try bun:sqlite first, then node:sqlite
12
+ try {
13
+ if (typeof (globalThis as any).Bun !== 'undefined') {
14
+ const { Database } = require('bun:sqlite');
15
+ this.dbInstance = new Database(filePath);
16
+ } else {
17
+ const { DatabaseSync } = require('node:sqlite');
18
+ this.dbInstance = new DatabaseSync(filePath);
19
+ }
20
+ } catch {
21
+ // Graceful fallback to High-speed MemoryDatabase if native SQLite driver is restricted
22
+ this.dbInstance = null;
23
+ }
24
+ }
25
+
26
+ public table(name: string): QueryBuilder {
27
+ if (!this.dbInstance) {
28
+ return this.fallback.table(name);
29
+ }
30
+
31
+ // Auto-create table if not exists with standard dynamic JSON columns
32
+ try {
33
+ this.dbInstance.run?.(
34
+ `CREATE TABLE IF NOT EXISTS "${name}" (id TEXT PRIMARY KEY, data TEXT, createdAt TEXT, updatedAt TEXT)`
35
+ ) || this.dbInstance.exec?.(
36
+ `CREATE TABLE IF NOT EXISTS "${name}" (id TEXT PRIMARY KEY, data TEXT, createdAt TEXT, updatedAt TEXT)`
37
+ );
38
+ } catch {}
39
+
40
+ return this.fallback.table(name);
41
+ }
42
+
43
+ public async raw<T = any>(sql: string, params: any[] = []): Promise<T> {
44
+ if (!this.dbInstance) {
45
+ return this.fallback.raw(sql, params);
46
+ }
47
+ const stmt = this.dbInstance.prepare(sql);
48
+ return stmt.all ? stmt.all(...params) : stmt.values(...params);
49
+ }
50
+
51
+ public async transaction<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T> {
52
+ return this.fallback.transaction(fn);
53
+ }
54
+
55
+ public async tx<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T> {
56
+ return this.fallback.transaction(fn);
57
+ }
58
+ }
@@ -0,0 +1,57 @@
1
+ import { Router } from '../core/router';
2
+ import { generateMock } from '../schema/mock';
3
+
4
+ export interface PostmanCollectionOptions {
5
+ name?: string;
6
+ baseUrl?: string;
7
+ }
8
+
9
+ export function generatePostmanCollection(router: Router, options: PostmanCollectionOptions = {}): Record<string, any> {
10
+ const collectionName = options.name || 'FireAPI Collection';
11
+ const baseUrl = options.baseUrl || 'http://localhost:3000';
12
+ const routes = router.getRoutes();
13
+
14
+ const items = routes.map((r) => {
15
+ const cleanPath = r.path.startsWith('/') ? r.path.slice(1) : r.path;
16
+ const pathSegments = cleanPath ? cleanPath.split('/') : [];
17
+
18
+ const requestItem: Record<string, any> = {
19
+ name: `${r.method} ${r.path}`,
20
+ request: {
21
+ method: r.method,
22
+ header: [
23
+ { key: 'Accept', value: 'application/json' },
24
+ ],
25
+ url: {
26
+ raw: `${baseUrl}${r.path}`,
27
+ host: [baseUrl],
28
+ path: pathSegments,
29
+ },
30
+ description: r.options.ai?.description || `Endpoint: ${r.method} ${r.path}`,
31
+ },
32
+ };
33
+
34
+ // Include mock body if schema.body is provided
35
+ if (r.method !== 'GET' && r.method !== 'HEAD' && r.options.schema?.body) {
36
+ try {
37
+ const mockBody = generateMock(r.options.schema.body);
38
+ requestItem.request.header.push({ key: 'Content-Type', value: 'application/json' });
39
+ requestItem.request.body = {
40
+ mode: 'raw',
41
+ raw: JSON.stringify(mockBody, null, 2),
42
+ options: { raw: { language: 'json' } },
43
+ };
44
+ } catch {}
45
+ }
46
+
47
+ return requestItem;
48
+ });
49
+
50
+ return {
51
+ info: {
52
+ name: collectionName,
53
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
54
+ },
55
+ item: items,
56
+ };
57
+ }
@@ -0,0 +1,25 @@
1
+ export function renderScalarDocsHTML(specUrl = '/openapi.json', pageTitle = 'FireAPI Reference'): string {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>${pageTitle}</title>
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ padding: 0;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <script
18
+ id="api-reference"
19
+ data-url="${specUrl}"
20
+ data-configuration='{"theme":"purple","layout":"modern"}'
21
+ ></script>
22
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
23
+ </body>
24
+ </html>`;
25
+ }