rest_api_faker 0.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/dist/cli.js ADDED
@@ -0,0 +1,1838 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/cli.ts
26
+ var import_yargs = __toESM(require("yargs"));
27
+ var import_helpers = require("yargs/helpers");
28
+ var import_fs3 = require("fs");
29
+ var import_path3 = require("path");
30
+ var import_chokidar = require("chokidar");
31
+
32
+ // src/database.ts
33
+ var import_lowdb = require("lowdb");
34
+ var import_node = require("lowdb/node");
35
+ var import_fs = require("fs");
36
+ var import_path = require("path");
37
+ var import_url = require("url");
38
+ var Database = class {
39
+ db;
40
+ filePath;
41
+ options;
42
+ /**
43
+ * Creates a new Database instance
44
+ *
45
+ * @param source - Path to JSON file or object with data
46
+ * @param options - Database configuration options
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const db = new Database('db.json', { idField: 'id' });
51
+ * await db.init();
52
+ * ```
53
+ */
54
+ constructor(source, options = {}) {
55
+ this.options = {
56
+ idField: options.idField || "id",
57
+ foreignKeySuffix: options.foreignKeySuffix || "Id"
58
+ };
59
+ if (typeof source === "string") {
60
+ this.filePath = (0, import_path.resolve)(source);
61
+ const adapter = new import_node.JSONFile(this.filePath);
62
+ this.db = new import_lowdb.Low(adapter, {});
63
+ } else {
64
+ this.filePath = "";
65
+ const adapter = new import_node.JSONFile(":memory:");
66
+ this.db = new import_lowdb.Low(adapter, source);
67
+ }
68
+ }
69
+ /**
70
+ * Initialize the database by reading from file or using provided data
71
+ * Supports .json, .js, .ts, and .mjs files
72
+ *
73
+ * @throws Error if file doesn't exist or contains invalid data
74
+ */
75
+ async init() {
76
+ if (this.filePath && !(0, import_fs.existsSync)(this.filePath)) {
77
+ const ext = this.filePath.endsWith(".js") ? ".js" : ".json";
78
+ throw new Error(
79
+ `Database file not found: ${this.filePath}
80
+
81
+ Make sure the file exists and the path is correct.
82
+ Expected format: ${ext === ".js" ? "JavaScript module exporting data" : "JSON file with data structure"}`
83
+ );
84
+ }
85
+ if (this.filePath) {
86
+ const ext = (0, import_path.extname)(this.filePath).toLowerCase();
87
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".ts") {
88
+ try {
89
+ const fileUrl = (0, import_url.pathToFileURL)(this.filePath).href;
90
+ const module2 = await import(fileUrl);
91
+ const data = module2.default ?? module2;
92
+ if (typeof data === "function") {
93
+ const result = await Promise.resolve(data());
94
+ if (typeof result !== "object" || result === null) {
95
+ throw new Error("JavaScript module function must return an object");
96
+ }
97
+ this.db.data = result;
98
+ } else if (typeof data === "object" && data !== null) {
99
+ this.db.data = data;
100
+ } else {
101
+ throw new Error("JavaScript module must export an object or function");
102
+ }
103
+ } catch (error) {
104
+ throw new Error(
105
+ `Failed to load JavaScript module: ${error instanceof Error ? error.message : "Unknown error"}`
106
+ );
107
+ }
108
+ } else {
109
+ await this.db.read();
110
+ }
111
+ } else {
112
+ await this.db.read();
113
+ }
114
+ if (typeof this.db.data !== "object") {
115
+ this.db.data = {};
116
+ }
117
+ for (const key in this.db.data) {
118
+ const value = this.db.data[key];
119
+ if (value !== null && typeof value !== "object") {
120
+ throw new Error(`Invalid data structure: ${key} must be an array or object`);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Get all data from the database
126
+ *
127
+ * @returns Complete database data
128
+ */
129
+ getData() {
130
+ return this.db.data;
131
+ }
132
+ /**
133
+ * Get a specific collection (array) or resource (object)
134
+ *
135
+ * @param name - Name of the collection/resource
136
+ * @returns Collection array, resource object, or undefined
137
+ */
138
+ getCollection(name) {
139
+ return this.db.data[name];
140
+ }
141
+ /**
142
+ * Get a single item from a collection by ID
143
+ *
144
+ * @param collectionName - Name of the collection
145
+ * @param id - ID of the item
146
+ * @returns The item or undefined
147
+ */
148
+ getById(collectionName, id) {
149
+ const collection = this.db.data[collectionName];
150
+ if (!Array.isArray(collection)) {
151
+ return void 0;
152
+ }
153
+ return collection.find((item) => {
154
+ if (typeof item !== "object" || item === null) {
155
+ return false;
156
+ }
157
+ const record = item;
158
+ return record[this.options.idField] === id || record[this.options.idField] === Number(id);
159
+ });
160
+ }
161
+ /**
162
+ * Generate next ID for a collection
163
+ *
164
+ * @param collectionName - Name of the collection
165
+ * @returns Next available ID
166
+ */
167
+ generateId(collectionName) {
168
+ const collection = this.db.data[collectionName];
169
+ if (!Array.isArray(collection)) {
170
+ return 1;
171
+ }
172
+ let maxId = 0;
173
+ for (const item of collection) {
174
+ if (typeof item === "object" && item !== null) {
175
+ const record = item;
176
+ const idValue = record[this.options.idField];
177
+ if (typeof idValue === "number" && idValue > maxId) {
178
+ maxId = idValue;
179
+ }
180
+ }
181
+ }
182
+ return maxId + 1;
183
+ }
184
+ /**
185
+ * Create a new item in a collection
186
+ *
187
+ * @param collectionName - Name of the collection
188
+ * @param data - Data to insert
189
+ * @returns Created item with ID
190
+ * @throws Error if collection is not an array
191
+ */
192
+ async create(collectionName, data) {
193
+ let collection = this.db.data[collectionName];
194
+ if (!collection) {
195
+ collection = [];
196
+ this.db.data[collectionName] = collection;
197
+ }
198
+ if (!Array.isArray(collection)) {
199
+ throw new Error(`Cannot create in ${collectionName}: not a collection`);
200
+ }
201
+ const idValue = data[this.options.idField] !== void 0 ? data[this.options.idField] : this.generateId(collectionName);
202
+ const existingItem = this.getById(collectionName, idValue);
203
+ if (existingItem) {
204
+ throw new Error(`Item with ${this.options.idField}=${String(idValue)} already exists`);
205
+ }
206
+ const newItem = { ...data, [this.options.idField]: idValue };
207
+ collection.push(newItem);
208
+ await this.save();
209
+ return newItem;
210
+ }
211
+ /**
212
+ * Update an item in a collection (full replacement)
213
+ *
214
+ * @param collectionName - Name of the collection
215
+ * @param id - ID of the item
216
+ * @param data - New data (ID field will be preserved)
217
+ * @returns Updated item or undefined if not found
218
+ */
219
+ async update(collectionName, id, data) {
220
+ const collection = this.db.data[collectionName];
221
+ if (!Array.isArray(collection)) {
222
+ return void 0;
223
+ }
224
+ const index = collection.findIndex((item) => {
225
+ if (typeof item !== "object" || item === null) {
226
+ return false;
227
+ }
228
+ const record = item;
229
+ return record[this.options.idField] === id || record[this.options.idField] === Number(id);
230
+ });
231
+ if (index === -1) {
232
+ return void 0;
233
+ }
234
+ const originalItem = collection[index];
235
+ const originalId = originalItem[this.options.idField];
236
+ const updatedItem = { ...data, [this.options.idField]: originalId };
237
+ collection[index] = updatedItem;
238
+ await this.save();
239
+ return updatedItem;
240
+ }
241
+ /**
242
+ * Patch an item in a collection (partial update)
243
+ *
244
+ * @param collectionName - Name of the collection
245
+ * @param id - ID of the item
246
+ * @param data - Partial data to merge (ID field will be ignored)
247
+ * @returns Updated item or undefined if not found
248
+ */
249
+ async patch(collectionName, id, data) {
250
+ const collection = this.db.data[collectionName];
251
+ if (!Array.isArray(collection)) {
252
+ return void 0;
253
+ }
254
+ const index = collection.findIndex((item) => {
255
+ if (typeof item !== "object" || item === null) {
256
+ return false;
257
+ }
258
+ const record = item;
259
+ return record[this.options.idField] === id || record[this.options.idField] === Number(id);
260
+ });
261
+ if (index === -1) {
262
+ return void 0;
263
+ }
264
+ const currentItem = collection[index];
265
+ const { [this.options.idField]: _ignoredId, ...patchData } = data;
266
+ const patchedItem = { ...currentItem, ...patchData };
267
+ collection[index] = patchedItem;
268
+ await this.save();
269
+ return patchedItem;
270
+ }
271
+ /**
272
+ * Delete an item from a collection
273
+ *
274
+ * @param collectionName - Name of the collection
275
+ * @param id - ID of the item
276
+ * @returns true if deleted, false if not found
277
+ */
278
+ async delete(collectionName, id) {
279
+ const collection = this.db.data[collectionName];
280
+ if (!Array.isArray(collection)) {
281
+ return false;
282
+ }
283
+ const index = collection.findIndex((item) => {
284
+ if (typeof item !== "object" || item === null) {
285
+ return false;
286
+ }
287
+ const record = item;
288
+ return record[this.options.idField] === id || record[this.options.idField] === Number(id);
289
+ });
290
+ if (index === -1) {
291
+ return false;
292
+ }
293
+ collection.splice(index, 1);
294
+ await this.save();
295
+ return true;
296
+ }
297
+ /**
298
+ * Update or create a singular resource
299
+ *
300
+ * @param resourceName - Name of the resource
301
+ * @param data - Resource data
302
+ * @returns Updated resource
303
+ */
304
+ async updateSingular(resourceName, data) {
305
+ this.db.data[resourceName] = data;
306
+ await this.save();
307
+ return data;
308
+ }
309
+ /**
310
+ * Save the database to file
311
+ */
312
+ async save() {
313
+ await this.db.write();
314
+ }
315
+ /**
316
+ * Check if a resource is a collection (array) or singular (object)
317
+ *
318
+ * @param name - Name of the resource
319
+ * @returns true if collection, false if singular or doesn't exist
320
+ */
321
+ isCollection(name) {
322
+ return Array.isArray(this.db.data[name]);
323
+ }
324
+ /**
325
+ * Get ID field name
326
+ */
327
+ getIdField() {
328
+ return this.options.idField;
329
+ }
330
+ /**
331
+ * Get foreign key suffix
332
+ */
333
+ getForeignKeySuffix() {
334
+ return this.options.foreignKeySuffix;
335
+ }
336
+ };
337
+
338
+ // src/server.ts
339
+ var import_express3 = __toESM(require("express"));
340
+ var import_cors = __toESM(require("cors"));
341
+ var import_compression = __toESM(require("compression"));
342
+
343
+ // src/router.ts
344
+ var import_express = require("express");
345
+
346
+ // src/logger.ts
347
+ var import_picocolors = __toESM(require("picocolors"));
348
+ var logger = {
349
+ /**
350
+ * Log success message in green
351
+ *
352
+ * @param message - Success message
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * logger.success('Server started successfully');
357
+ * // Output: ✓ Server started successfully (in green)
358
+ * ```
359
+ */
360
+ success(message) {
361
+ console.log(import_picocolors.default.green(`\u2713 ${message}`));
362
+ },
363
+ /**
364
+ * Log error message in red
365
+ *
366
+ * @param message - Error message
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * logger.error('Failed to load file');
371
+ * // Output: ✗ Failed to load file (in red)
372
+ * ```
373
+ */
374
+ error(message) {
375
+ console.error(import_picocolors.default.red(`\u2717 ${message}`));
376
+ },
377
+ /**
378
+ * Log warning message in yellow
379
+ *
380
+ * @param message - Warning message
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * logger.warn('Using default port');
385
+ * // Output: ⚠ Using default port (in yellow)
386
+ * ```
387
+ */
388
+ warn(message) {
389
+ console.warn(import_picocolors.default.yellow(`\u26A0 ${message}`));
390
+ },
391
+ /**
392
+ * Log info message in cyan
393
+ *
394
+ * @param message - Info message
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * logger.info('Watching for changes...');
399
+ * // Output: ℹ Watching for changes... (in cyan)
400
+ * ```
401
+ */
402
+ info(message) {
403
+ console.log(import_picocolors.default.cyan(`\u2139 ${message}`));
404
+ },
405
+ /**
406
+ * Log plain message without color
407
+ *
408
+ * @param message - Message to log
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * logger.log('http://localhost:3000');
413
+ * // Output: http://localhost:3000
414
+ * ```
415
+ */
416
+ log(message) {
417
+ console.log(message);
418
+ },
419
+ /**
420
+ * Log request in gray (for non-quiet mode)
421
+ *
422
+ * @param method - HTTP method
423
+ * @param url - Request URL
424
+ *
425
+ * @example
426
+ * ```typescript
427
+ * logger.request('GET', '/api/users');
428
+ * // Output: [timestamp] GET /api/users (in gray)
429
+ * ```
430
+ */
431
+ request(method, url) {
432
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
433
+ console.log(import_picocolors.default.gray(`[${timestamp}] ${method} ${url}`));
434
+ },
435
+ /**
436
+ * Log formatted banner
437
+ *
438
+ * @param lines - Array of lines to display
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * logger.banner([
443
+ * '🚀 API Faker is running!',
444
+ * '',
445
+ * ' Resources:',
446
+ * ' http://localhost:3000/'
447
+ * ]);
448
+ * ```
449
+ */
450
+ banner(lines) {
451
+ console.log();
452
+ for (const line of lines) {
453
+ if (line.includes("http://") || line.includes("https://")) {
454
+ const colored = line.replace(/(https?:\/\/[^\s]+)/g, (url) => import_picocolors.default.cyan(url));
455
+ console.log(colored);
456
+ } else if (line.startsWith("\u{1F680}")) {
457
+ console.log(import_picocolors.default.bold(line));
458
+ } else {
459
+ console.log(line);
460
+ }
461
+ }
462
+ console.log();
463
+ }
464
+ };
465
+
466
+ // src/query.ts
467
+ function parseQuery(req) {
468
+ const query = req.query;
469
+ const filters = {};
470
+ const operators = {};
471
+ const sort = [];
472
+ const order = [];
473
+ let page;
474
+ let limit;
475
+ let start;
476
+ let end;
477
+ let q;
478
+ for (const [key, value] of Object.entries(query)) {
479
+ const stringValue = typeof value === "string" ? value : Array.isArray(value) && value.length > 0 && typeof value[0] === "string" ? value[0] : null;
480
+ if (stringValue === null && !Array.isArray(value)) {
481
+ continue;
482
+ }
483
+ if (key === "_sort") {
484
+ if (typeof value === "string") {
485
+ sort.push(...value.split(","));
486
+ } else if (Array.isArray(value)) {
487
+ for (const v of value) {
488
+ if (typeof v === "string") {
489
+ sort.push(v);
490
+ }
491
+ }
492
+ }
493
+ continue;
494
+ }
495
+ if (key === "_order") {
496
+ if (typeof value === "string") {
497
+ order.push(...value.split(","));
498
+ } else if (Array.isArray(value)) {
499
+ for (const v of value) {
500
+ if (typeof v === "string") {
501
+ order.push(v);
502
+ }
503
+ }
504
+ }
505
+ continue;
506
+ }
507
+ if (key === "_page" && stringValue) {
508
+ const parsed = parseInt(stringValue, 10);
509
+ if (!isNaN(parsed) && parsed > 0) {
510
+ page = parsed;
511
+ }
512
+ continue;
513
+ }
514
+ if (key === "_limit" && stringValue) {
515
+ const parsed = parseInt(stringValue, 10);
516
+ if (!isNaN(parsed) && parsed > 0) {
517
+ limit = parsed;
518
+ }
519
+ continue;
520
+ }
521
+ if (key === "_start" && stringValue) {
522
+ const parsed = parseInt(stringValue, 10);
523
+ if (!isNaN(parsed) && parsed >= 0) {
524
+ start = parsed;
525
+ }
526
+ continue;
527
+ }
528
+ if (key === "_end" && stringValue) {
529
+ const parsed = parseInt(stringValue, 10);
530
+ if (!isNaN(parsed) && parsed >= 0) {
531
+ end = parsed;
532
+ }
533
+ continue;
534
+ }
535
+ if (key === "q" && stringValue) {
536
+ q = stringValue;
537
+ continue;
538
+ }
539
+ const operatorMatch = key.match(/^(.+)_(gte|lte|ne|like)$/);
540
+ if (operatorMatch && stringValue) {
541
+ const field = operatorMatch[1];
542
+ const operator = operatorMatch[2];
543
+ if (field && operator) {
544
+ if (!operators[field]) {
545
+ operators[field] = {};
546
+ }
547
+ operators[field][operator] = stringValue;
548
+ }
549
+ continue;
550
+ }
551
+ if (typeof value === "string") {
552
+ filters[key] = value;
553
+ } else if (Array.isArray(value)) {
554
+ const stringValues = value.filter((v) => typeof v === "string");
555
+ if (stringValues.length > 0) {
556
+ filters[key] = stringValues;
557
+ }
558
+ }
559
+ }
560
+ const result = {
561
+ filters,
562
+ operators,
563
+ sort,
564
+ order
565
+ };
566
+ if (page !== void 0) result.page = page;
567
+ if (limit !== void 0) result.limit = limit;
568
+ if (start !== void 0) result.start = start;
569
+ if (end !== void 0) result.end = end;
570
+ if (q !== void 0) result.q = q;
571
+ return result;
572
+ }
573
+ function getNestedValue(obj, path) {
574
+ if (typeof obj !== "object" || obj === null) {
575
+ return void 0;
576
+ }
577
+ const keys = path.split(".");
578
+ let current = obj;
579
+ for (const key of keys) {
580
+ if (typeof current !== "object" || current === null || !(key in current)) {
581
+ return void 0;
582
+ }
583
+ current = current[key];
584
+ }
585
+ return current;
586
+ }
587
+ function matchesFilters(item, filters) {
588
+ if (typeof item !== "object" || item === null) {
589
+ return false;
590
+ }
591
+ for (const [key, filterValue] of Object.entries(filters)) {
592
+ const itemValue = getNestedValue(item, key);
593
+ if (Array.isArray(filterValue)) {
594
+ const matched = filterValue.some((val) => String(itemValue) === val);
595
+ if (!matched) {
596
+ return false;
597
+ }
598
+ } else {
599
+ if (String(itemValue) !== filterValue) {
600
+ return false;
601
+ }
602
+ }
603
+ }
604
+ return true;
605
+ }
606
+ function matchesOperators(item, operators) {
607
+ if (typeof item !== "object" || item === null) {
608
+ return false;
609
+ }
610
+ for (const [key, ops] of Object.entries(operators)) {
611
+ const itemValue = getNestedValue(item, key);
612
+ for (const [operator, filterValue] of Object.entries(ops)) {
613
+ if (operator === "gte") {
614
+ if (Number(itemValue) < Number(filterValue)) {
615
+ return false;
616
+ }
617
+ } else if (operator === "lte") {
618
+ if (Number(itemValue) > Number(filterValue)) {
619
+ return false;
620
+ }
621
+ } else if (operator === "ne") {
622
+ if (String(itemValue) === filterValue) {
623
+ return false;
624
+ }
625
+ } else if (operator === "like") {
626
+ const itemStr = String(itemValue).toLowerCase();
627
+ const filterStr = filterValue.toLowerCase();
628
+ if (!itemStr.includes(filterStr)) {
629
+ return false;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ return true;
635
+ }
636
+ function matchesSearch(item, searchText) {
637
+ if (typeof item !== "object" || item === null) {
638
+ return false;
639
+ }
640
+ const lowerSearch = searchText.toLowerCase();
641
+ function searchObject(obj) {
642
+ if (typeof obj === "string") {
643
+ return obj.toLowerCase().includes(lowerSearch);
644
+ }
645
+ if (typeof obj === "object" && obj !== null) {
646
+ return Object.values(obj).some(searchObject);
647
+ }
648
+ return false;
649
+ }
650
+ return searchObject(item);
651
+ }
652
+ function applyQuery(data, options) {
653
+ let result = [...data];
654
+ if (options.q) {
655
+ const searchText = options.q;
656
+ result = result.filter((item) => matchesSearch(item, searchText));
657
+ }
658
+ if (Object.keys(options.filters).length > 0) {
659
+ result = result.filter((item) => matchesFilters(item, options.filters));
660
+ }
661
+ if (Object.keys(options.operators).length > 0) {
662
+ result = result.filter((item) => matchesOperators(item, options.operators));
663
+ }
664
+ const total = result.length;
665
+ if (options.sort.length > 0) {
666
+ const sortFields = options.sort;
667
+ const sortOrders = options.order;
668
+ result.sort((a, b) => {
669
+ for (let i = 0; i < sortFields.length; i++) {
670
+ const field = sortFields[i];
671
+ if (!field) continue;
672
+ const order = sortOrders[i] === "desc" ? -1 : 1;
673
+ const aVal = getNestedValue(a, field);
674
+ const bVal = getNestedValue(b, field);
675
+ if (aVal === bVal) continue;
676
+ if (aVal === void 0 || aVal === null) return 1 * order;
677
+ if (bVal === void 0 || bVal === null) return -1 * order;
678
+ if (aVal < bVal) return -1 * order;
679
+ if (aVal > bVal) return 1 * order;
680
+ }
681
+ return 0;
682
+ });
683
+ }
684
+ if (options.start !== void 0 || options.end !== void 0) {
685
+ const start = options.start ?? 0;
686
+ const end = options.end ?? result.length;
687
+ result = result.slice(start, end);
688
+ } else if (options.page !== void 0 && options.limit !== void 0) {
689
+ const start = (options.page - 1) * options.limit;
690
+ result = result.slice(start, start + options.limit);
691
+ } else if (options.limit !== void 0) {
692
+ result = result.slice(0, options.limit);
693
+ }
694
+ return { data: result, total };
695
+ }
696
+ function generateLinkHeader(req, page, limit, total) {
697
+ const host = req.get("host") ?? "localhost";
698
+ const baseUrl = `${req.protocol}://${host}${req.path}`;
699
+ const query = new URLSearchParams(req.query);
700
+ const lastPage = Math.ceil(total / limit);
701
+ const links = [];
702
+ query.set("_page", "1");
703
+ query.set("_limit", String(limit));
704
+ links.push(`<${baseUrl}?${query.toString()}>; rel="first"`);
705
+ if (page > 1) {
706
+ query.set("_page", String(page - 1));
707
+ links.push(`<${baseUrl}?${query.toString()}>; rel="prev"`);
708
+ }
709
+ if (page < lastPage) {
710
+ query.set("_page", String(page + 1));
711
+ links.push(`<${baseUrl}?${query.toString()}>; rel="next"`);
712
+ }
713
+ query.set("_page", String(lastPage));
714
+ links.push(`<${baseUrl}?${query.toString()}>; rel="last"`);
715
+ return links.join(", ");
716
+ }
717
+
718
+ // src/relationships.ts
719
+ function getForeignKey(collectionName, foreignKeySuffix) {
720
+ const singular = collectionName.endsWith("s") ? collectionName.slice(0, -1) : collectionName;
721
+ return `${singular}${foreignKeySuffix}`;
722
+ }
723
+ function embedChildren(items, parentCollection, childCollection, db, idField, foreignKeySuffix) {
724
+ const children = db.getCollection(childCollection);
725
+ if (!Array.isArray(children)) {
726
+ return items;
727
+ }
728
+ const foreignKey = getForeignKey(parentCollection, foreignKeySuffix);
729
+ return items.map((item) => {
730
+ const matchingChildren = children.filter((child) => {
731
+ if (typeof child !== "object" || child === null) return false;
732
+ const childFk = child[foreignKey];
733
+ const parentId = item[idField];
734
+ return childFk === parentId || String(childFk) === String(parentId);
735
+ });
736
+ return {
737
+ ...item,
738
+ [childCollection]: matchingChildren
739
+ };
740
+ });
741
+ }
742
+ function expandParent(items, _childCollection, parentCollection, db, idField, foreignKeySuffix) {
743
+ const parents = db.getCollection(parentCollection);
744
+ if (!Array.isArray(parents)) {
745
+ return items;
746
+ }
747
+ const foreignKey = getForeignKey(parentCollection, foreignKeySuffix);
748
+ return items.map((item) => {
749
+ const foreignKeyValue = item[foreignKey];
750
+ if (foreignKeyValue === void 0) {
751
+ return item;
752
+ }
753
+ const parent = parents.find((p) => {
754
+ if (typeof p !== "object" || p === null) return false;
755
+ const parentRecord = p;
756
+ const parentId = parentRecord[idField];
757
+ if (typeof foreignKeyValue === "number" || typeof foreignKeyValue === "string") {
758
+ return parentId === foreignKeyValue || String(parentId) === String(foreignKeyValue);
759
+ }
760
+ return false;
761
+ });
762
+ if (!parent || typeof parent !== "object") {
763
+ return item;
764
+ }
765
+ const parentPropName = parentCollection.endsWith("s") ? parentCollection.slice(0, -1) : parentCollection;
766
+ return {
767
+ ...item,
768
+ [parentPropName]: parent
769
+ };
770
+ });
771
+ }
772
+ function applyRelationships(data, resource, embed, expand, db, idField, foreignKeySuffix) {
773
+ let result = data;
774
+ for (const childCollection of embed) {
775
+ result = embedChildren(result, resource, childCollection, db, idField, foreignKeySuffix);
776
+ }
777
+ for (const parentCollection of expand) {
778
+ result = expandParent(result, resource, parentCollection, db, idField, foreignKeySuffix);
779
+ }
780
+ return result;
781
+ }
782
+ function parseRelationships(query) {
783
+ const embed = [];
784
+ const expand = [];
785
+ const embedParam = query._embed;
786
+ if (typeof embedParam === "string") {
787
+ embed.push(...embedParam.split(","));
788
+ } else if (Array.isArray(embedParam)) {
789
+ for (const e of embedParam) {
790
+ if (typeof e === "string") {
791
+ embed.push(e);
792
+ }
793
+ }
794
+ }
795
+ const expandParam = query._expand;
796
+ if (typeof expandParam === "string") {
797
+ expand.push(...expandParam.split(","));
798
+ } else if (Array.isArray(expandParam)) {
799
+ for (const e of expandParam) {
800
+ if (typeof e === "string") {
801
+ expand.push(e);
802
+ }
803
+ }
804
+ }
805
+ return { embed, expand };
806
+ }
807
+
808
+ // src/router.ts
809
+ function getParam(req, name) {
810
+ const value = req.params[name];
811
+ if (value === void 0) {
812
+ throw new Error(`Route parameter '${name}' is missing`);
813
+ }
814
+ return value;
815
+ }
816
+ function createRouter(db, options = {}) {
817
+ const router = (0, import_express.Router)();
818
+ const readOnly = options.readOnly || false;
819
+ const idField = options.idField || "id";
820
+ const foreignKeySuffix = options.foreignKeySuffix || "Id";
821
+ const validateContentType = (req, _res, next) => {
822
+ const contentType = req.get("Content-Type");
823
+ if (!contentType || !contentType.includes("application/json")) {
824
+ logger.warn("Content-Type should be application/json");
825
+ }
826
+ next();
827
+ };
828
+ router.get("/db", (_req, res) => {
829
+ res.json(db.getData());
830
+ });
831
+ router.get("/:resource", (req, res) => {
832
+ const resource = getParam(req, "resource");
833
+ const data = db.getCollection(resource);
834
+ if (data === void 0) {
835
+ res.status(404).json({ error: `Resource '${resource}' not found` });
836
+ return;
837
+ }
838
+ if (Array.isArray(data)) {
839
+ const queryOptions = parseQuery(req);
840
+ const { data: filtered, total } = applyQuery(data, queryOptions);
841
+ const { embed, expand } = parseRelationships(req.query);
842
+ const withRelationships = applyRelationships(
843
+ filtered,
844
+ resource,
845
+ embed,
846
+ expand,
847
+ db,
848
+ idField,
849
+ foreignKeySuffix
850
+ );
851
+ res.set("X-Total-Count", String(total));
852
+ if (queryOptions.page !== void 0 && queryOptions.limit !== void 0) {
853
+ const linkHeader = generateLinkHeader(req, queryOptions.page, queryOptions.limit, total);
854
+ res.set("Link", linkHeader);
855
+ }
856
+ res.json(withRelationships);
857
+ return;
858
+ }
859
+ res.json(data);
860
+ });
861
+ router.get("/:resource/:id", (req, res) => {
862
+ const resource = getParam(req, "resource");
863
+ const id = getParam(req, "id");
864
+ if (!db.isCollection(resource)) {
865
+ res.status(404).json({ error: `Collection '${resource}' not found` });
866
+ return;
867
+ }
868
+ const item = db.getById(resource, id);
869
+ if (!item) {
870
+ res.status(404).json({ error: `Item with id '${id}' not found in '${resource}'` });
871
+ return;
872
+ }
873
+ const { embed, expand } = parseRelationships(req.query);
874
+ if (embed.length > 0 || expand.length > 0) {
875
+ const withRelationships = applyRelationships(
876
+ [item],
877
+ resource,
878
+ embed,
879
+ expand,
880
+ db,
881
+ idField,
882
+ foreignKeySuffix
883
+ );
884
+ res.json(withRelationships[0]);
885
+ return;
886
+ }
887
+ res.json(item);
888
+ });
889
+ router.get("/:parent/:parentId/:children", (req, res) => {
890
+ const parent = getParam(req, "parent");
891
+ const parentId = getParam(req, "parentId");
892
+ const children = getParam(req, "children");
893
+ if (!db.isCollection(parent)) {
894
+ res.status(404).json({ error: `Collection '${parent}' not found` });
895
+ return;
896
+ }
897
+ const parentItem = db.getById(parent, parentId);
898
+ if (!parentItem) {
899
+ res.status(404).json({ error: `Parent item with id '${parentId}' not found in '${parent}'` });
900
+ return;
901
+ }
902
+ const childrenData = db.getCollection(children);
903
+ if (!Array.isArray(childrenData)) {
904
+ res.status(404).json({ error: `Collection '${children}' not found` });
905
+ return;
906
+ }
907
+ const foreignKey = getForeignKey(parent, foreignKeySuffix);
908
+ const filtered = childrenData.filter((child) => {
909
+ if (typeof child !== "object" || child === null) return false;
910
+ const childFk = child[foreignKey];
911
+ return childFk === parentId || (typeof childFk === "number" || typeof childFk === "string" ? String(childFk) === parentId : false);
912
+ });
913
+ const queryOptions = parseQuery(req);
914
+ const { data: result, total } = applyQuery(filtered, queryOptions);
915
+ res.set("X-Total-Count", String(total));
916
+ res.json(result);
917
+ });
918
+ router.post(
919
+ "/:parent/:parentId/:children",
920
+ validateContentType,
921
+ async (req, res) => {
922
+ if (readOnly) {
923
+ return res.status(403).json({ error: "Read-only mode enabled" });
924
+ }
925
+ const parent = getParam(req, "parent");
926
+ const parentId = getParam(req, "parentId");
927
+ const children = getParam(req, "children");
928
+ const data = req.body;
929
+ if (typeof data !== "object") {
930
+ return res.status(400).json({ error: "Request body must be a JSON object" });
931
+ }
932
+ if (!db.isCollection(parent)) {
933
+ return res.status(404).json({ error: `Collection '${parent}' not found` });
934
+ }
935
+ const parentItem = db.getById(parent, parentId);
936
+ if (!parentItem) {
937
+ return res.status(404).json({ error: `Parent item with id '${parentId}' not found in '${parent}'` });
938
+ }
939
+ const foreignKey = getForeignKey(parent, foreignKeySuffix);
940
+ data[foreignKey] = parentId;
941
+ try {
942
+ const created = await db.create(children, data);
943
+ return res.status(201).json(created);
944
+ } catch (error) {
945
+ const message = error instanceof Error ? error.message : "Unknown error";
946
+ return res.status(400).json({ error: message });
947
+ }
948
+ }
949
+ );
950
+ router.post("/:resource", validateContentType, async (req, res) => {
951
+ if (readOnly) {
952
+ return res.status(403).json({ error: "Read-only mode enabled" });
953
+ }
954
+ const resource = getParam(req, "resource");
955
+ const data = req.body;
956
+ if (typeof data !== "object") {
957
+ return res.status(400).json({ error: "Request body must be a JSON object" });
958
+ }
959
+ try {
960
+ if (!db.isCollection(resource) && db.getCollection(resource) !== void 0) {
961
+ const updated = await db.updateSingular(resource, data);
962
+ return res.status(200).json(updated);
963
+ }
964
+ const created = await db.create(resource, data);
965
+ return res.status(201).json(created);
966
+ } catch (error) {
967
+ const message = error instanceof Error ? error.message : "Unknown error";
968
+ return res.status(400).json({ error: message });
969
+ }
970
+ });
971
+ router.put("/:resource/:id", validateContentType, async (req, res) => {
972
+ if (readOnly) {
973
+ return res.status(403).json({ error: "Read-only mode enabled" });
974
+ }
975
+ const resource = getParam(req, "resource");
976
+ const id = getParam(req, "id");
977
+ const data = req.body;
978
+ if (typeof data !== "object") {
979
+ return res.status(400).json({ error: "Request body must be a JSON object" });
980
+ }
981
+ if (!db.isCollection(resource)) {
982
+ return res.status(404).json({ error: `Collection '${resource}' not found` });
983
+ }
984
+ try {
985
+ const updated = await db.update(resource, id, data);
986
+ if (!updated) {
987
+ return res.status(404).json({ error: `Item with id '${id}' not found in '${resource}'` });
988
+ }
989
+ return res.json(updated);
990
+ } catch (error) {
991
+ const message = error instanceof Error ? error.message : "Unknown error";
992
+ return res.status(400).json({ error: message });
993
+ }
994
+ });
995
+ router.patch("/:resource/:id", validateContentType, async (req, res) => {
996
+ if (readOnly) {
997
+ return res.status(403).json({ error: "Read-only mode enabled" });
998
+ }
999
+ const resource = getParam(req, "resource");
1000
+ const id = getParam(req, "id");
1001
+ const data = req.body;
1002
+ if (typeof data !== "object") {
1003
+ return res.status(400).json({ error: "Request body must be a JSON object" });
1004
+ }
1005
+ if (!db.isCollection(resource)) {
1006
+ return res.status(404).json({ error: `Collection '${resource}' not found` });
1007
+ }
1008
+ try {
1009
+ const patched = await db.patch(resource, id, data);
1010
+ if (!patched) {
1011
+ return res.status(404).json({ error: `Item with id '${id}' not found in '${resource}'` });
1012
+ }
1013
+ return res.json(patched);
1014
+ } catch (error) {
1015
+ const message = error instanceof Error ? error.message : "Unknown error";
1016
+ return res.status(400).json({ error: message });
1017
+ }
1018
+ });
1019
+ router.put("/:resource", validateContentType, async (req, res) => {
1020
+ if (readOnly) {
1021
+ return res.status(403).json({ error: "Read-only mode enabled" });
1022
+ }
1023
+ const resource = getParam(req, "resource");
1024
+ const data = req.body;
1025
+ if (typeof data !== "object") {
1026
+ return res.status(400).json({ error: "Request body must be a JSON object" });
1027
+ }
1028
+ if (db.isCollection(resource)) {
1029
+ return res.status(400).json({
1030
+ error: `Cannot PUT to collection '${resource}'. Use POST or PUT /${resource}/:id`
1031
+ });
1032
+ }
1033
+ try {
1034
+ const updated = await db.updateSingular(resource, data);
1035
+ return res.json(updated);
1036
+ } catch (error) {
1037
+ const message = error instanceof Error ? error.message : "Unknown error";
1038
+ return res.status(400).json({ error: message });
1039
+ }
1040
+ });
1041
+ router.patch("/:resource", validateContentType, async (req, res) => {
1042
+ if (readOnly) {
1043
+ return res.status(403).json({ error: "Read-only mode enabled" });
1044
+ }
1045
+ const resource = getParam(req, "resource");
1046
+ const data = req.body;
1047
+ if (typeof data !== "object") {
1048
+ return res.status(400).json({ error: "Request body must be a JSON object" });
1049
+ }
1050
+ if (db.isCollection(resource)) {
1051
+ return res.status(400).json({ error: `Cannot PATCH collection '${resource}'. Use PATCH /${resource}/:id` });
1052
+ }
1053
+ const current = db.getCollection(resource);
1054
+ if (!current || typeof current !== "object") {
1055
+ return res.status(404).json({ error: `Resource '${resource}' not found` });
1056
+ }
1057
+ try {
1058
+ const merged = { ...current, ...data };
1059
+ const updated = await db.updateSingular(resource, merged);
1060
+ return res.json(updated);
1061
+ } catch (error) {
1062
+ const message = error instanceof Error ? error.message : "Unknown error";
1063
+ return res.status(400).json({ error: message });
1064
+ }
1065
+ });
1066
+ router.delete("/:resource/:id", async (req, res) => {
1067
+ if (readOnly) {
1068
+ return res.status(403).json({ error: "Read-only mode enabled" });
1069
+ }
1070
+ const resource = getParam(req, "resource");
1071
+ const id = getParam(req, "id");
1072
+ if (!db.isCollection(resource)) {
1073
+ return res.status(404).json({ error: `Collection '${resource}' not found` });
1074
+ }
1075
+ const deleted = await db.delete(resource, id);
1076
+ if (!deleted) {
1077
+ return res.status(404).json({ error: `Item with id '${id}' not found in '${resource}'` });
1078
+ }
1079
+ return res.status(204).send();
1080
+ });
1081
+ return router;
1082
+ }
1083
+
1084
+ // src/static.ts
1085
+ var import_express2 = __toESM(require("express"));
1086
+ var import_fs2 = require("fs");
1087
+ var import_path2 = require("path");
1088
+ function createStaticMiddleware(options = {}) {
1089
+ const directory = options.directory || "./public";
1090
+ const enabled = options.enabled !== false;
1091
+ const staticPath = (0, import_path2.resolve)(process.cwd(), directory);
1092
+ if (!enabled || !(0, import_fs2.existsSync)(staticPath)) {
1093
+ return (_req, _res, next) => {
1094
+ next();
1095
+ };
1096
+ }
1097
+ return import_express2.default.static(staticPath, {
1098
+ index: "index.html",
1099
+ dotfiles: "ignore",
1100
+ redirect: true
1101
+ });
1102
+ }
1103
+ function createHomepageMiddleware(options = {}) {
1104
+ const directory = options.directory || "./public";
1105
+ const enabled = options.enabled !== false;
1106
+ const staticPath = (0, import_path2.resolve)(process.cwd(), directory);
1107
+ const indexPath = (0, import_path2.resolve)(staticPath, "index.html");
1108
+ return (req, res, next) => {
1109
+ if (req.path !== "/") {
1110
+ next();
1111
+ return;
1112
+ }
1113
+ if (enabled && (0, import_fs2.existsSync)(indexPath)) {
1114
+ next();
1115
+ return;
1116
+ }
1117
+ const host = req.get("host") || "localhost:3000";
1118
+ const protocol = req.protocol;
1119
+ const baseUrl = `${protocol}://${host}`;
1120
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1121
+ res.send(`<!DOCTYPE html>
1122
+ <html lang="en">
1123
+ <head>
1124
+ <meta charset="UTF-8">
1125
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1126
+ <title>API Faker</title>
1127
+ <style>
1128
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1129
+ body {
1130
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1131
+ line-height: 1.6;
1132
+ color: #333;
1133
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1134
+ min-height: 100vh;
1135
+ padding: 2rem;
1136
+ }
1137
+ .container {
1138
+ max-width: 800px;
1139
+ margin: 0 auto;
1140
+ background: white;
1141
+ border-radius: 12px;
1142
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
1143
+ padding: 3rem;
1144
+ }
1145
+ h1 {
1146
+ color: #667eea;
1147
+ font-size: 2.5rem;
1148
+ margin-bottom: 1rem;
1149
+ display: flex;
1150
+ align-items: center;
1151
+ gap: 0.5rem;
1152
+ }
1153
+ h2 {
1154
+ color: #555;
1155
+ font-size: 1.5rem;
1156
+ margin-top: 2rem;
1157
+ margin-bottom: 1rem;
1158
+ border-bottom: 2px solid #667eea;
1159
+ padding-bottom: 0.5rem;
1160
+ }
1161
+ .intro {
1162
+ color: #666;
1163
+ font-size: 1.1rem;
1164
+ margin-bottom: 2rem;
1165
+ }
1166
+ .endpoint {
1167
+ background: #f8f9fa;
1168
+ border-left: 4px solid #667eea;
1169
+ padding: 1rem;
1170
+ margin-bottom: 1rem;
1171
+ border-radius: 4px;
1172
+ }
1173
+ .endpoint a {
1174
+ color: #667eea;
1175
+ text-decoration: none;
1176
+ font-family: 'Monaco', 'Courier New', monospace;
1177
+ font-weight: bold;
1178
+ }
1179
+ .endpoint a:hover {
1180
+ text-decoration: underline;
1181
+ }
1182
+ .endpoint p {
1183
+ color: #666;
1184
+ margin-top: 0.5rem;
1185
+ font-size: 0.95rem;
1186
+ }
1187
+ .info-box {
1188
+ background: #e3f2fd;
1189
+ border-left: 4px solid #2196f3;
1190
+ padding: 1rem;
1191
+ margin-top: 2rem;
1192
+ border-radius: 4px;
1193
+ }
1194
+ .info-box p {
1195
+ color: #1565c0;
1196
+ margin: 0;
1197
+ }
1198
+ code {
1199
+ background: #f5f5f5;
1200
+ padding: 0.2rem 0.4rem;
1201
+ border-radius: 3px;
1202
+ font-family: 'Monaco', 'Courier New', monospace;
1203
+ font-size: 0.9em;
1204
+ }
1205
+ </style>
1206
+ </head>
1207
+ <body>
1208
+ <div class="container">
1209
+ <h1>\u{1F680} API Faker</h1>
1210
+ <p class="intro">
1211
+ Your JSON REST API is up and running! Use the endpoints below to interact with your data.
1212
+ </p>
1213
+
1214
+ <h2>\u{1F4E1} Endpoints</h2>
1215
+ <div class="endpoint">
1216
+ <a href="${baseUrl}/db" target="_blank">${baseUrl}/db</a>
1217
+ <p>View the full database</p>
1218
+ </div>
1219
+
1220
+ <h2>\u{1F4A1} Tips</h2>
1221
+ <div class="info-box">
1222
+ <p>
1223
+ Use query parameters to filter, sort, and paginate your data.
1224
+ Examples: <code>?_sort=name&_order=asc</code>, <code>?_page=1&_limit=10</code>
1225
+ </p>
1226
+ </div>
1227
+ </div>
1228
+ </body>
1229
+ </html>`);
1230
+ };
1231
+ }
1232
+
1233
+ // src/loader.ts
1234
+ var import_node_url = require("url");
1235
+ async function loadModule(filePath) {
1236
+ try {
1237
+ const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
1238
+ const module2 = await import(fileUrl);
1239
+ return module2;
1240
+ } catch (error) {
1241
+ const message = error instanceof Error ? error.message : String(error);
1242
+ throw new Error(
1243
+ `Failed to load module '${filePath}': ${message}
1244
+
1245
+ Make sure:
1246
+ - The file exists and is a valid JavaScript file
1247
+ - The file doesn't have syntax errors
1248
+ - All dependencies are installed`
1249
+ );
1250
+ }
1251
+ }
1252
+ async function loadMiddlewares(filePath) {
1253
+ const module2 = await loadModule(filePath);
1254
+ let middlewareExport;
1255
+ if (typeof module2 === "object" && module2 !== null && "default" in module2) {
1256
+ const defaultExport = module2.default;
1257
+ if (typeof defaultExport === "object" && defaultExport !== null && "default" in defaultExport) {
1258
+ middlewareExport = defaultExport.default;
1259
+ } else {
1260
+ middlewareExport = defaultExport;
1261
+ }
1262
+ } else {
1263
+ middlewareExport = module2;
1264
+ }
1265
+ const middlewares = Array.isArray(middlewareExport) ? middlewareExport : [middlewareExport];
1266
+ for (let i = 0; i < middlewares.length; i++) {
1267
+ const mw = middlewares[i];
1268
+ if (typeof mw !== "function") {
1269
+ const indexStr = String(i);
1270
+ throw new Error(
1271
+ `Middleware file '${filePath}' at index ${indexStr} is not a function, got ${typeof mw}`
1272
+ );
1273
+ }
1274
+ }
1275
+ return middlewares;
1276
+ }
1277
+
1278
+ // src/rewriter.ts
1279
+ var import_promises = require("fs/promises");
1280
+ async function loadRewriteRules(filePath) {
1281
+ try {
1282
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
1283
+ const rules = JSON.parse(content);
1284
+ if (typeof rules !== "object" || rules === null || Array.isArray(rules)) {
1285
+ throw new Error("Routes file must contain a JSON object with route mappings");
1286
+ }
1287
+ for (const [key, value] of Object.entries(rules)) {
1288
+ if (typeof value !== "string") {
1289
+ throw new Error(`Route mapping for '${key}' must be a string, got ${typeof value}`);
1290
+ }
1291
+ }
1292
+ return rules;
1293
+ } catch (error) {
1294
+ if (error instanceof Error && error.message.includes("Routes file must contain")) {
1295
+ throw error;
1296
+ }
1297
+ const message = error instanceof Error ? error.message : String(error);
1298
+ throw new Error(
1299
+ `Failed to load routes from '${filePath}': ${message}
1300
+
1301
+ Expected format:
1302
+ {
1303
+ "/api/*": "/$1",
1304
+ "/users/:id": "/api/users/:id"
1305
+ }
1306
+
1307
+ See examples/routes.json for more examples.`
1308
+ );
1309
+ }
1310
+ }
1311
+ function patternToRegex(pattern) {
1312
+ const params = [];
1313
+ let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
1314
+ regexPattern = regexPattern.replace(/:([^/]+)/g, (_match, param) => {
1315
+ params.push(param);
1316
+ return "([^/]+)";
1317
+ });
1318
+ regexPattern = regexPattern.replace(/\*/g, () => {
1319
+ params.push(`$${String(params.length + 1)}`);
1320
+ return "(.*)";
1321
+ });
1322
+ return {
1323
+ regex: new RegExp(`^${regexPattern}$`),
1324
+ params
1325
+ };
1326
+ }
1327
+ function rewriteUrl(url, fromPattern, toPattern) {
1328
+ const { regex, params } = patternToRegex(fromPattern);
1329
+ const match = url.match(regex);
1330
+ if (!match) {
1331
+ return null;
1332
+ }
1333
+ const captures = match.slice(1);
1334
+ let result = toPattern;
1335
+ for (let i = 0; i < captures.length; i++) {
1336
+ const value = captures[i];
1337
+ if (value !== void 0) {
1338
+ result = result.replace(`$${String(i + 1)}`, value);
1339
+ }
1340
+ }
1341
+ for (let i = 0; i < params.length; i++) {
1342
+ const param = params[i];
1343
+ const value = captures[i];
1344
+ if (param && !param.startsWith("$") && value !== void 0) {
1345
+ result = result.replace(`:${param}`, value);
1346
+ }
1347
+ }
1348
+ return result;
1349
+ }
1350
+ function createRewriterMiddleware(rules) {
1351
+ return (req, _res, next) => {
1352
+ for (const [from, to] of Object.entries(rules)) {
1353
+ const rewritten = rewriteUrl(req.url, from, to);
1354
+ if (rewritten !== null) {
1355
+ req.url = rewritten;
1356
+ break;
1357
+ }
1358
+ }
1359
+ next();
1360
+ };
1361
+ }
1362
+
1363
+ // src/server.ts
1364
+ async function createServer(db, options = {}) {
1365
+ const app = (0, import_express3.default)();
1366
+ if (!options.noCors) {
1367
+ app.use((0, import_cors.default)());
1368
+ }
1369
+ if (!options.noGzip) {
1370
+ app.use((0, import_compression.default)());
1371
+ }
1372
+ app.use(import_express3.default.json());
1373
+ if (options.delay && options.delay > 0) {
1374
+ const delay = options.delay;
1375
+ app.use((_req, _res, next) => {
1376
+ setTimeout(() => {
1377
+ next();
1378
+ }, delay);
1379
+ });
1380
+ }
1381
+ if (!options.quiet) {
1382
+ app.use((req, _res, next) => {
1383
+ logger.request(req.method, req.url);
1384
+ next();
1385
+ });
1386
+ }
1387
+ if (options.middlewares) {
1388
+ try {
1389
+ const middlewares = await loadMiddlewares(options.middlewares);
1390
+ for (const middleware of middlewares) {
1391
+ app.use(middleware);
1392
+ }
1393
+ if (!options.quiet) {
1394
+ logger.success(`Loaded custom middlewares from ${options.middlewares}`);
1395
+ }
1396
+ } catch (error) {
1397
+ logger.error(
1398
+ `Failed to load middlewares: ${error instanceof Error ? error.message : String(error)}`
1399
+ );
1400
+ throw error;
1401
+ }
1402
+ }
1403
+ app.use(createHomepageMiddleware(options));
1404
+ app.use(createStaticMiddleware(options));
1405
+ if (options.routes) {
1406
+ try {
1407
+ const rules = await loadRewriteRules(options.routes);
1408
+ const rewriter = createRewriterMiddleware(rules);
1409
+ app.use(rewriter);
1410
+ if (!options.quiet) {
1411
+ logger.success(`Loaded route rewrite rules from ${options.routes}`);
1412
+ }
1413
+ } catch (error) {
1414
+ logger.error(
1415
+ `Failed to load routes: ${error instanceof Error ? error.message : String(error)}`
1416
+ );
1417
+ throw error;
1418
+ }
1419
+ }
1420
+ app.get("/db", (_req, res) => {
1421
+ res.json(db.getData());
1422
+ });
1423
+ const router = createRouter(db, options);
1424
+ app.use(router);
1425
+ app.use((_req, res) => {
1426
+ res.status(404).json({ error: "Not Found" });
1427
+ });
1428
+ return app;
1429
+ }
1430
+ function startServer(app, options = {}) {
1431
+ const port = options.port || 3e3;
1432
+ const host = options.host || "localhost";
1433
+ return app.listen(port, host, () => {
1434
+ if (!options.quiet) {
1435
+ logger.banner([
1436
+ "\u{1F680} API Faker is running!",
1437
+ "",
1438
+ " Resources:",
1439
+ ` http://${host}:${String(port)}/`,
1440
+ "",
1441
+ " Home:",
1442
+ ` http://${host}:${String(port)}`
1443
+ ]);
1444
+ }
1445
+ });
1446
+ }
1447
+
1448
+ // src/config.ts
1449
+ var import_node_fs = require("fs");
1450
+ var import_node_path = require("path");
1451
+ function findClosestMatch(input, candidates) {
1452
+ const levenshtein = (a, b) => {
1453
+ const matrix = [];
1454
+ for (let i = 0; i <= b.length; i++) {
1455
+ matrix[i] = [i];
1456
+ }
1457
+ for (let j = 0; j <= a.length; j++) {
1458
+ matrix[0][j] = j;
1459
+ }
1460
+ for (let i = 1; i <= b.length; i++) {
1461
+ for (let j = 1; j <= a.length; j++) {
1462
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
1463
+ matrix[i][j] = matrix[i - 1][j - 1];
1464
+ } else {
1465
+ matrix[i][j] = Math.min(
1466
+ matrix[i - 1][j - 1] + 1,
1467
+ matrix[i][j - 1] + 1,
1468
+ matrix[i - 1][j] + 1
1469
+ );
1470
+ }
1471
+ }
1472
+ }
1473
+ return matrix[b.length][a.length];
1474
+ };
1475
+ let closestMatch = null;
1476
+ let minDistance = Infinity;
1477
+ for (const candidate of candidates) {
1478
+ const distance = levenshtein(input.toLowerCase(), candidate.toLowerCase());
1479
+ if (distance < minDistance && distance <= 3) {
1480
+ minDistance = distance;
1481
+ closestMatch = candidate;
1482
+ }
1483
+ }
1484
+ return closestMatch;
1485
+ }
1486
+ function loadConfig(configPath) {
1487
+ const resolvedPath = (0, import_node_path.resolve)(configPath);
1488
+ if (!(0, import_node_fs.existsSync)(resolvedPath)) {
1489
+ return null;
1490
+ }
1491
+ try {
1492
+ const content = (0, import_node_fs.readFileSync)(resolvedPath, "utf-8");
1493
+ const config = JSON.parse(content);
1494
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
1495
+ throw new Error("Config file must contain a JSON object");
1496
+ }
1497
+ validateConfig(config);
1498
+ return config;
1499
+ } catch (error) {
1500
+ if (error instanceof Error && error.message.includes("Config file must contain")) {
1501
+ throw error;
1502
+ }
1503
+ throw new Error(
1504
+ `Failed to load config from '${configPath}': ${error instanceof Error ? error.message : String(error)}`
1505
+ );
1506
+ }
1507
+ }
1508
+ function validateConfig(config) {
1509
+ const validOptions = /* @__PURE__ */ new Set([
1510
+ "port",
1511
+ "host",
1512
+ "watch",
1513
+ "routes",
1514
+ "middlewares",
1515
+ "static",
1516
+ "readOnly",
1517
+ "noCors",
1518
+ "noGzip",
1519
+ "snapshots",
1520
+ "delay",
1521
+ "id",
1522
+ "foreignKeySuffix",
1523
+ "quiet"
1524
+ ]);
1525
+ for (const key of Object.keys(config)) {
1526
+ if (!validOptions.has(key)) {
1527
+ const suggestion = findClosestMatch(key, Array.from(validOptions));
1528
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : "";
1529
+ throw new Error(
1530
+ `Unknown config option: '${key}'.${didYouMean} Valid options are: ${Array.from(validOptions).join(", ")}`
1531
+ );
1532
+ }
1533
+ }
1534
+ if ("port" in config && (typeof config.port !== "number" || config.port < 0 || config.port > 65535)) {
1535
+ throw new Error("Config option 'port' must be a number between 0 and 65535");
1536
+ }
1537
+ if ("host" in config && typeof config.host !== "string") {
1538
+ throw new Error("Config option 'host' must be a string");
1539
+ }
1540
+ if ("watch" in config && typeof config.watch !== "boolean") {
1541
+ throw new Error("Config option 'watch' must be a boolean");
1542
+ }
1543
+ if ("routes" in config && typeof config.routes !== "string") {
1544
+ throw new Error("Config option 'routes' must be a string");
1545
+ }
1546
+ if ("middlewares" in config && typeof config.middlewares !== "string") {
1547
+ throw new Error("Config option 'middlewares' must be a string");
1548
+ }
1549
+ if ("static" in config && typeof config.static !== "string") {
1550
+ throw new Error("Config option 'static' must be a string");
1551
+ }
1552
+ if ("readOnly" in config && typeof config.readOnly !== "boolean") {
1553
+ throw new Error("Config option 'readOnly' must be a boolean");
1554
+ }
1555
+ if ("noCors" in config && typeof config.noCors !== "boolean") {
1556
+ throw new Error("Config option 'noCors' must be a boolean");
1557
+ }
1558
+ if ("noGzip" in config && typeof config.noGzip !== "boolean") {
1559
+ throw new Error("Config option 'noGzip' must be a boolean");
1560
+ }
1561
+ if ("snapshots" in config && typeof config.snapshots !== "string") {
1562
+ throw new Error("Config option 'snapshots' must be a string");
1563
+ }
1564
+ if ("delay" in config && (typeof config.delay !== "number" || config.delay < 0)) {
1565
+ throw new Error("Config option 'delay' must be a non-negative number");
1566
+ }
1567
+ if ("id" in config && typeof config.id !== "string") {
1568
+ throw new Error("Config option 'id' must be a string");
1569
+ }
1570
+ if ("foreignKeySuffix" in config && typeof config.foreignKeySuffix !== "string") {
1571
+ throw new Error("Config option 'foreignKeySuffix' must be a string");
1572
+ }
1573
+ if ("quiet" in config && typeof config.quiet !== "boolean") {
1574
+ throw new Error("Config option 'quiet' must be a boolean");
1575
+ }
1576
+ }
1577
+ function mergeConfig(cliConfig, fileConfig) {
1578
+ if (!fileConfig) {
1579
+ return cliConfig;
1580
+ }
1581
+ return {
1582
+ ...fileConfig,
1583
+ ...cliConfig
1584
+ };
1585
+ }
1586
+
1587
+ // src/cli.ts
1588
+ function getVersion() {
1589
+ try {
1590
+ const packageJson = JSON.parse(
1591
+ (0, import_fs3.readFileSync)((0, import_path3.resolve)(__dirname, "../package.json"), "utf-8")
1592
+ );
1593
+ return packageJson.version;
1594
+ } catch {
1595
+ return "0.0.0";
1596
+ }
1597
+ }
1598
+ function parseCli() {
1599
+ const argv = (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName("api-faker").usage("Usage: $0 [options] <source>").example("$0 db.json", "Start API Faker with db.json").example("$0 file.js", "Start API Faker with a JS file").example("$0 http://example.com/db.json", "Start API Faker with a remote schema").option("config", {
1600
+ alias: "c",
1601
+ type: "string",
1602
+ description: "Path to config file",
1603
+ default: "api-faker.json"
1604
+ }).option("port", {
1605
+ alias: "p",
1606
+ type: "number",
1607
+ description: "Set port",
1608
+ default: 3e3
1609
+ }).option("host", {
1610
+ alias: "H",
1611
+ type: "string",
1612
+ description: "Set host",
1613
+ default: "localhost"
1614
+ }).option("watch", {
1615
+ alias: "w",
1616
+ type: "boolean",
1617
+ description: "Watch file(s)",
1618
+ default: false
1619
+ }).option("routes", {
1620
+ alias: "r",
1621
+ type: "string",
1622
+ description: "Path to routes file"
1623
+ }).option("middlewares", {
1624
+ alias: "m",
1625
+ type: "string",
1626
+ description: "Path to middleware file"
1627
+ }).option("static", {
1628
+ alias: "s",
1629
+ type: "string",
1630
+ description: "Set static files directory",
1631
+ default: "./public"
1632
+ }).option("no-static", {
1633
+ type: "boolean",
1634
+ description: "Disable static file serving",
1635
+ default: false
1636
+ }).option("read-only", {
1637
+ alias: "ro",
1638
+ type: "boolean",
1639
+ description: "Allow only GET requests",
1640
+ default: false
1641
+ }).option("no-cors", {
1642
+ alias: "nc",
1643
+ type: "boolean",
1644
+ description: "Disable Cross-Origin Resource Sharing",
1645
+ default: false
1646
+ }).option("no-gzip", {
1647
+ alias: "ng",
1648
+ type: "boolean",
1649
+ description: "Disable GZIP Content-Encoding",
1650
+ default: false
1651
+ }).option("snapshots", {
1652
+ alias: "S",
1653
+ type: "string",
1654
+ description: "Set snapshots directory",
1655
+ default: "."
1656
+ }).option("delay", {
1657
+ alias: "d",
1658
+ type: "number",
1659
+ description: "Add delay to responses (ms)"
1660
+ }).option("id", {
1661
+ alias: "i",
1662
+ type: "string",
1663
+ description: "Set database id property",
1664
+ default: "id"
1665
+ }).option("foreignKeySuffix", {
1666
+ alias: "fks",
1667
+ type: "string",
1668
+ description: "Set foreign key suffix",
1669
+ default: "Id"
1670
+ }).option("quiet", {
1671
+ alias: "q",
1672
+ type: "boolean",
1673
+ description: "Suppress log messages from output",
1674
+ default: false
1675
+ }).help("help", "Show help").alias("h", "help").version(getVersion()).alias("v", "version").epilogue("For more information, visit https://github.com/hamidmayeli/api-faker").parseSync();
1676
+ let fileConfig = null;
1677
+ try {
1678
+ fileConfig = loadConfig(argv.config);
1679
+ } catch (error) {
1680
+ logger.error(
1681
+ `Failed to load config file: ${error instanceof Error ? error.message : String(error)}`
1682
+ );
1683
+ process.exit(1);
1684
+ }
1685
+ const cliConfig = {};
1686
+ if (argv.port !== 3e3) cliConfig.port = argv.port;
1687
+ if (argv.host !== "localhost") cliConfig.host = argv.host;
1688
+ if (argv.watch) cliConfig.watch = argv.watch;
1689
+ if (argv.routes) cliConfig.routes = argv.routes;
1690
+ if (argv.middlewares) cliConfig.middlewares = argv.middlewares;
1691
+ if (argv.static !== "./public") cliConfig.static = argv.static;
1692
+ if (argv["read-only"]) cliConfig.readOnly = argv["read-only"];
1693
+ if (argv["no-cors"]) cliConfig.noCors = argv["no-cors"];
1694
+ if (argv["no-gzip"]) cliConfig.noGzip = argv["no-gzip"];
1695
+ if (argv.snapshots !== ".") cliConfig.snapshots = argv.snapshots;
1696
+ if (argv.delay !== void 0) cliConfig.delay = argv.delay;
1697
+ if (argv.id !== "id") cliConfig.id = argv.id;
1698
+ if (argv.foreignKeySuffix !== "Id") cliConfig.foreignKeySuffix = argv.foreignKeySuffix;
1699
+ if (argv.quiet) cliConfig.quiet = argv.quiet;
1700
+ const merged = mergeConfig(cliConfig, fileConfig);
1701
+ return {
1702
+ source: argv._[0],
1703
+ port: merged.port ?? 3e3,
1704
+ host: merged.host ?? "localhost",
1705
+ watch: merged.watch ?? false,
1706
+ routes: merged.routes,
1707
+ middlewares: merged.middlewares,
1708
+ static: merged.static ?? "./public",
1709
+ noStatic: argv["no-static"],
1710
+ readOnly: merged.readOnly ?? false,
1711
+ noCors: merged.noCors ?? false,
1712
+ noGzip: merged.noGzip ?? false,
1713
+ snapshots: merged.snapshots ?? ".",
1714
+ delay: merged.delay,
1715
+ id: merged.id ?? "id",
1716
+ foreignKeySuffix: merged.foreignKeySuffix ?? "Id",
1717
+ quiet: merged.quiet ?? false,
1718
+ config: argv.config
1719
+ };
1720
+ }
1721
+ async function main() {
1722
+ const config = parseCli();
1723
+ if (!config.quiet) {
1724
+ logger.log(`
1725
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1726
+ \u2551 \u2551
1727
+ \u2551 API Faker v${getVersion().padEnd(19)}\u2551
1728
+ \u2551 \u2551
1729
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1730
+ `);
1731
+ }
1732
+ if (!config.source) {
1733
+ logger.error("No source file specified");
1734
+ logger.info('Run "api-faker --help" for usage information');
1735
+ process.exit(1);
1736
+ }
1737
+ if (!config.quiet) {
1738
+ logger.info(`Source: ${config.source}`);
1739
+ logger.info(`Port: ${String(config.port)}`);
1740
+ logger.info(`Host: ${config.host}`);
1741
+ logger.log("");
1742
+ logger.info("Loading database...");
1743
+ }
1744
+ try {
1745
+ const db = new Database(config.source, {
1746
+ idField: config.id,
1747
+ foreignKeySuffix: config.foreignKeySuffix
1748
+ });
1749
+ await db.init();
1750
+ if (!config.quiet) {
1751
+ const data = db.getData();
1752
+ const resources = Object.keys(data);
1753
+ logger.success(`Loaded ${String(resources.length)} resource(s): ${resources.join(", ")}`);
1754
+ logger.log("");
1755
+ }
1756
+ const serverOptions = {
1757
+ port: config.port,
1758
+ host: config.host,
1759
+ readOnly: config.readOnly,
1760
+ noCors: config.noCors,
1761
+ noGzip: config.noGzip,
1762
+ quiet: config.quiet,
1763
+ idField: config.id,
1764
+ foreignKeySuffix: config.foreignKeySuffix,
1765
+ enabled: !config.noStatic
1766
+ };
1767
+ if (config.static) {
1768
+ serverOptions.directory = config.static;
1769
+ }
1770
+ if (config.routes) {
1771
+ serverOptions.routes = config.routes;
1772
+ }
1773
+ if (config.middlewares) {
1774
+ serverOptions.middlewares = config.middlewares;
1775
+ }
1776
+ if (config.delay !== void 0) {
1777
+ serverOptions.delay = config.delay;
1778
+ }
1779
+ const app = await createServer(db, serverOptions);
1780
+ const server = startServer(app, {
1781
+ port: config.port,
1782
+ host: config.host,
1783
+ quiet: config.quiet
1784
+ });
1785
+ if (config.watch && config.source && (0, import_fs3.existsSync)(config.source)) {
1786
+ const watcher = (0, import_chokidar.watch)(config.source, {
1787
+ ignoreInitial: true,
1788
+ persistent: true
1789
+ });
1790
+ watcher.on("change", (path) => {
1791
+ if (!config.quiet) {
1792
+ logger.log("");
1793
+ logger.info(`File changed: ${path}`);
1794
+ logger.info("Reloading database...");
1795
+ }
1796
+ db.init().then(() => {
1797
+ if (!config.quiet) {
1798
+ const data = db.getData();
1799
+ const resources = Object.keys(data);
1800
+ logger.success(
1801
+ `Reloaded ${String(resources.length)} resource(s): ${resources.join(", ")}`
1802
+ );
1803
+ }
1804
+ }).catch((error) => {
1805
+ logger.error(
1806
+ `Failed to reload database: ${error instanceof Error ? error.message : "Unknown error"}`
1807
+ );
1808
+ });
1809
+ });
1810
+ watcher.on("error", (error) => {
1811
+ logger.error(`Watcher error: ${error instanceof Error ? error.message : String(error)}`);
1812
+ });
1813
+ if (!config.quiet) {
1814
+ logger.info(`Watching ${config.source} for changes...`);
1815
+ logger.log("");
1816
+ }
1817
+ process.on("SIGINT", () => {
1818
+ if (!config.quiet) {
1819
+ logger.log("");
1820
+ logger.info("Shutting down...");
1821
+ }
1822
+ watcher.close().catch(() => {
1823
+ });
1824
+ server.close(() => {
1825
+ process.exit(0);
1826
+ });
1827
+ });
1828
+ }
1829
+ } catch (error) {
1830
+ logger.error(error instanceof Error ? error.message : "Unknown error");
1831
+ process.exit(1);
1832
+ }
1833
+ }
1834
+ main().catch((error) => {
1835
+ logger.error(`Fatal error: ${error instanceof Error ? error.message : "Unknown error"}`);
1836
+ process.exit(1);
1837
+ });
1838
+ //# sourceMappingURL=cli.js.map