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/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/api-faker.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1838 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
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
|