lieko-express 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lieko-express.js +1403 -0
- package/package.json +17 -0
package/lieko-express.js
ADDED
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
const { createServer } = require('http');
|
|
2
|
+
const net = require("net");
|
|
3
|
+
|
|
4
|
+
process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
|
|
5
|
+
|
|
6
|
+
class ValidationError extends Error {
|
|
7
|
+
constructor(errors) {
|
|
8
|
+
super('Validation failed');
|
|
9
|
+
this.name = 'ValidationError';
|
|
10
|
+
this.errors = errors;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class Schema {
|
|
15
|
+
constructor(rules) {
|
|
16
|
+
this.rules = rules;
|
|
17
|
+
this.fields = rules;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validate(data) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
for (const [field, validators] of Object.entries(this.rules)) {
|
|
23
|
+
const value = data[field];
|
|
24
|
+
for (const validator of validators) {
|
|
25
|
+
const error = validator(value, field, data);
|
|
26
|
+
if (error) {
|
|
27
|
+
errors.push(error);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (errors.length > 0) throw new ValidationError(errors);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const validators = {
|
|
38
|
+
required: (message = 'Field is required') => {
|
|
39
|
+
return (value, field) => {
|
|
40
|
+
if (value === undefined || value === null || value === '') {
|
|
41
|
+
return { field, message, type: 'required' };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
requiredTrue: (message = 'Field must be true') => {
|
|
48
|
+
return (value, field) => {
|
|
49
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
return { field, message, type: 'requiredTrue' };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
optional: () => {
|
|
58
|
+
return () => null;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
string: (message = 'Field must be a string') => {
|
|
62
|
+
return (value, field) => {
|
|
63
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
64
|
+
return { field, message, type: 'string' };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
number: (message = 'Field must be a number') => {
|
|
71
|
+
return (value, field) => {
|
|
72
|
+
if (value !== undefined && typeof value !== 'number') {
|
|
73
|
+
return { field, message, type: 'number' };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
boolean: (message = 'Field must be a boolean') => {
|
|
80
|
+
return (value, field) => {
|
|
81
|
+
if (value === undefined || value === null || value === '') return null;
|
|
82
|
+
|
|
83
|
+
const validTrue = ['true', true, 1, '1'];
|
|
84
|
+
const validFalse = ['false', false, 0, '0'];
|
|
85
|
+
|
|
86
|
+
const isValid = validTrue.includes(value) || validFalse.includes(value);
|
|
87
|
+
|
|
88
|
+
if (!isValid) {
|
|
89
|
+
return { field, message, type: 'boolean' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
integer: (message = 'Field must be an integer') => {
|
|
97
|
+
return (value, field) => {
|
|
98
|
+
if (value !== undefined && !Number.isInteger(value)) {
|
|
99
|
+
return { field, message, type: 'integer' };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
positive: (message = 'Field must be positive') => {
|
|
106
|
+
return (value, field) => {
|
|
107
|
+
if (value !== undefined && value <= 0) {
|
|
108
|
+
return { field, message, type: 'positive' };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
negative: (message = 'Field must be negative') => {
|
|
115
|
+
return (value, field) => {
|
|
116
|
+
if (value !== undefined && value >= 0) {
|
|
117
|
+
return { field, message, type: 'negative' };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
email: (message = 'Invalid email format') => {
|
|
124
|
+
return (value, field) => {
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
127
|
+
if (!emailRegex.test(value)) {
|
|
128
|
+
return { field, message, type: 'email' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
min: (minValue, message) => {
|
|
136
|
+
return (value, field) => {
|
|
137
|
+
if (value !== undefined && value !== null) {
|
|
138
|
+
if (typeof value === 'string' && value.length < minValue) {
|
|
139
|
+
return {
|
|
140
|
+
field,
|
|
141
|
+
message: message || `Field must be at least ${minValue} characters`,
|
|
142
|
+
type: 'min'
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === 'number' && value < minValue) {
|
|
146
|
+
return {
|
|
147
|
+
field,
|
|
148
|
+
message: message || `Field must be at least ${minValue}`,
|
|
149
|
+
type: 'min'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
max: (maxValue, message) => {
|
|
158
|
+
return (value, field) => {
|
|
159
|
+
if (value !== undefined && value !== null) {
|
|
160
|
+
if (typeof value === 'string' && value.length > maxValue) {
|
|
161
|
+
return {
|
|
162
|
+
field,
|
|
163
|
+
message: message || `Field must be at most ${maxValue} characters`,
|
|
164
|
+
type: 'max'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'number' && value > maxValue) {
|
|
168
|
+
return {
|
|
169
|
+
field,
|
|
170
|
+
message: message || `Field must be at most ${maxValue}`,
|
|
171
|
+
type: 'max'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
length: (n, message) => {
|
|
180
|
+
return (value, field) => {
|
|
181
|
+
if (typeof value === 'string' && value.length !== n) {
|
|
182
|
+
return {
|
|
183
|
+
field,
|
|
184
|
+
message: message || `Field must be exactly ${n} characters`,
|
|
185
|
+
type: 'length'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
minLength: (minLength, message) => {
|
|
193
|
+
return (value, field) => {
|
|
194
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
195
|
+
if (value.length < minLength) {
|
|
196
|
+
return {
|
|
197
|
+
field,
|
|
198
|
+
message: message || `Field must be at least ${minLength} characters`,
|
|
199
|
+
type: 'minLength'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
maxLength: (maxLength, message) => {
|
|
208
|
+
return (value, field) => {
|
|
209
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
210
|
+
if (value.length > maxLength) {
|
|
211
|
+
return {
|
|
212
|
+
field,
|
|
213
|
+
message: message || `Field must be at most ${maxLength} characters`,
|
|
214
|
+
type: 'maxLength'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
pattern: (regex, message = 'Invalid format') => {
|
|
223
|
+
return (value, field) => {
|
|
224
|
+
if (value !== undefined && value !== null && typeof value === 'string') {
|
|
225
|
+
if (!regex.test(value)) {
|
|
226
|
+
return { field, message, type: 'pattern' };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
oneOf: (allowedValues, message) => {
|
|
234
|
+
return (value, field) => {
|
|
235
|
+
if (value !== undefined && value !== null) {
|
|
236
|
+
if (!allowedValues.includes(value)) {
|
|
237
|
+
return {
|
|
238
|
+
field,
|
|
239
|
+
message: message || `Field must be one of: ${allowedValues.join(', ')}`,
|
|
240
|
+
type: 'oneOf'
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
notOneOf: (values, message) => {
|
|
249
|
+
return (value, field) => {
|
|
250
|
+
if (values.includes(value)) {
|
|
251
|
+
return {
|
|
252
|
+
field,
|
|
253
|
+
message: message || `Field cannot be one of: ${values.join(', ')}`,
|
|
254
|
+
type: 'notOneOf'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
custom: (validatorFn, message = 'Validation failed') => {
|
|
262
|
+
return (value, field, data) => {
|
|
263
|
+
const isValid = validatorFn(value, data);
|
|
264
|
+
if (!isValid) {
|
|
265
|
+
return { field, message, type: 'custom' };
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
equal: (expectedValue, message) => {
|
|
272
|
+
return (value, field) => {
|
|
273
|
+
if (value !== expectedValue) {
|
|
274
|
+
return {
|
|
275
|
+
field,
|
|
276
|
+
message: message || `Field must be equal to ${expectedValue}`,
|
|
277
|
+
type: 'equal'
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
mustBeTrue: (message = 'This field must be accepted') => {
|
|
285
|
+
return (value, field) => {
|
|
286
|
+
const normalized = value === true || value === 'true' || value === '1' || value === 1;
|
|
287
|
+
if (!normalized) {
|
|
288
|
+
return { field, message, type: 'mustBeTrue' };
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
mustBeFalse: (message = 'This field must be declined') => {
|
|
295
|
+
return (value, field) => {
|
|
296
|
+
const normalized = value === false || value === 'false' || value === '0' || value === 0;
|
|
297
|
+
if (!normalized) {
|
|
298
|
+
return { field, message, type: 'mustBeFalse' };
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
date: (message = 'Invalid date') => {
|
|
305
|
+
return (value, field) => {
|
|
306
|
+
if (!value) return null;
|
|
307
|
+
const date = new Date(value);
|
|
308
|
+
if (isNaN(date.getTime())) {
|
|
309
|
+
return { field, message, type: 'date' };
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
before: (limit, message) => {
|
|
316
|
+
return (value, field) => {
|
|
317
|
+
if (!value) return null;
|
|
318
|
+
const d1 = new Date(value);
|
|
319
|
+
const d2 = new Date(limit);
|
|
320
|
+
if (isNaN(d1) || d1 >= d2) {
|
|
321
|
+
return {
|
|
322
|
+
field,
|
|
323
|
+
message: message || `Date must be before ${limit}`,
|
|
324
|
+
type: 'before'
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
after: (limit, message) => {
|
|
332
|
+
return (value, field) => {
|
|
333
|
+
if (!value) return null;
|
|
334
|
+
const d1 = new Date(value);
|
|
335
|
+
const d2 = new Date(limit);
|
|
336
|
+
if (isNaN(d1) || d1 <= d2) {
|
|
337
|
+
return {
|
|
338
|
+
field,
|
|
339
|
+
message: message || `Date must be after ${limit}`,
|
|
340
|
+
type: 'after'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
startsWith: (prefix, message) => {
|
|
348
|
+
return (value, field) => {
|
|
349
|
+
if (typeof value === 'string' && !value.startsWith(prefix)) {
|
|
350
|
+
return {
|
|
351
|
+
field,
|
|
352
|
+
message: message || `Field must start with "${prefix}"`,
|
|
353
|
+
type: 'startsWith'
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
endsWith: (suffix, message) => {
|
|
361
|
+
return (value, field) => {
|
|
362
|
+
if (typeof value === 'string' && !value.endsWith(suffix)) {
|
|
363
|
+
return {
|
|
364
|
+
field,
|
|
365
|
+
message: message || `Field must end with "${suffix}"`,
|
|
366
|
+
type: 'endsWith'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
function validate(schema) {
|
|
375
|
+
return (req, res, next) => {
|
|
376
|
+
try {
|
|
377
|
+
schema.validate(req.body);
|
|
378
|
+
next();
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error instanceof ValidationError) {
|
|
381
|
+
return res.status(400).json({
|
|
382
|
+
success: false,
|
|
383
|
+
message: 'Validation failed',
|
|
384
|
+
errors: error.errors
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function validatePartial(schema) {
|
|
393
|
+
const partial = {};
|
|
394
|
+
|
|
395
|
+
for (const field in schema.fields) {
|
|
396
|
+
const rules = schema.fields[field];
|
|
397
|
+
|
|
398
|
+
const filtered = rules.filter(v =>
|
|
399
|
+
v.name !== 'required' &&
|
|
400
|
+
v.name !== 'requiredTrue' &&
|
|
401
|
+
v.name !== 'mustBeTrue'
|
|
402
|
+
);
|
|
403
|
+
partial[field] = [validators.optional(), ...filtered];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return new Schema(partial);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
class LiekoExpress {
|
|
410
|
+
constructor() {
|
|
411
|
+
this.groupStack = [];
|
|
412
|
+
this.routes = [];
|
|
413
|
+
this.middlewares = [];
|
|
414
|
+
this.errorHandlers = [];
|
|
415
|
+
this.notFoundHandler = null;
|
|
416
|
+
this.server = null;
|
|
417
|
+
|
|
418
|
+
this.settings = {
|
|
419
|
+
debug: false,
|
|
420
|
+
'x-powered-by': 'lieko-express',
|
|
421
|
+
'trust proxy': false,
|
|
422
|
+
strictTrailingSlash: true,
|
|
423
|
+
allowTrailingSlash: false,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
set(name, value) {
|
|
428
|
+
this.settings[name] = value;
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
get(name) {
|
|
433
|
+
return this.settings[name];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
enable(name) {
|
|
437
|
+
this.settings[name] = true;
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
disable(name) {
|
|
442
|
+
this.settings[name] = false;
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
enabled(name) {
|
|
447
|
+
return !!this.settings[name];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
disabled(name) {
|
|
451
|
+
return !this.settings[name];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
get(path, ...handlers) {
|
|
455
|
+
this._addRoute('GET', path, ...handlers);
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
post(path, ...handlers) {
|
|
460
|
+
this._addRoute('POST', path, ...handlers);
|
|
461
|
+
return this;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
put(path, ...handlers) {
|
|
465
|
+
this._addRoute('PUT', path, ...handlers);
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
delete(path, ...handlers) {
|
|
470
|
+
this._addRoute('DELETE', path, ...handlers);
|
|
471
|
+
return this;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
patch(path, ...handlers) {
|
|
475
|
+
this._addRoute('PATCH', path, ...handlers);
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
all(path, ...handlers) {
|
|
480
|
+
this._addRoute('ALL', path, ...handlers);
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
group(basePath, ...args) {
|
|
485
|
+
const parent = this;
|
|
486
|
+
|
|
487
|
+
const callback = args.pop();
|
|
488
|
+
if (typeof callback !== "function") {
|
|
489
|
+
throw new Error("group() requires a callback as last argument");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const middlewares = args.filter(fn => typeof fn === "function");
|
|
493
|
+
|
|
494
|
+
const normalize = (p) => p.replace(/\/+$/, '');
|
|
495
|
+
const fullBase = normalize(basePath);
|
|
496
|
+
|
|
497
|
+
const subApp = {
|
|
498
|
+
_call(method, path, handlers) {
|
|
499
|
+
const finalPath = normalize(fullBase + path);
|
|
500
|
+
parent[method](finalPath, ...middlewares, ...handlers);
|
|
501
|
+
return subApp;
|
|
502
|
+
},
|
|
503
|
+
get(path, ...handlers) { return this._call('get', path, handlers); },
|
|
504
|
+
post(path, ...handlers) { return this._call('post', path, handlers); },
|
|
505
|
+
put(path, ...handlers) { return this._call('put', path, handlers); },
|
|
506
|
+
patch(path, ...handlers) { return this._call('patch', path, handlers); },
|
|
507
|
+
delete(path, ...handlers) { return this._call('delete', path, handlers); },
|
|
508
|
+
all(path, ...handlers) { return this._call('all', path, handlers); },
|
|
509
|
+
|
|
510
|
+
use(pathOrMw, ...rest) {
|
|
511
|
+
if (typeof pathOrMw === "function") {
|
|
512
|
+
parent.use(fullBase, ...middlewares, pathOrMw);
|
|
513
|
+
return subApp;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (typeof pathOrMw === "string") {
|
|
517
|
+
const finalPath = normalize(fullBase + pathOrMw);
|
|
518
|
+
parent.use(finalPath, ...middlewares, ...rest);
|
|
519
|
+
return subApp;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
throw new Error("Invalid group.use() arguments");
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
group(subPath, ...subArgs) {
|
|
526
|
+
const subCb = subArgs.pop();
|
|
527
|
+
const subMw = subArgs.filter(fn => typeof fn === "function");
|
|
528
|
+
|
|
529
|
+
const finalPath = normalize(fullBase + subPath);
|
|
530
|
+
parent.group(finalPath, ...middlewares, ...subMw, subCb);
|
|
531
|
+
return subApp;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
this.groupStack.push({ basePath: fullBase, middlewares });
|
|
536
|
+
callback(subApp);
|
|
537
|
+
this.groupStack.pop();
|
|
538
|
+
|
|
539
|
+
return this;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
_checkMiddleware(handler) {
|
|
543
|
+
const isAsync = handler.constructor.name === 'AsyncFunction';
|
|
544
|
+
|
|
545
|
+
if (isAsync) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const paramCount = handler.length;
|
|
550
|
+
|
|
551
|
+
if (paramCount < 3) {
|
|
552
|
+
console.warn(`
|
|
553
|
+
⚠️ WARNING: Synchronous middleware detected without 'next' parameter!
|
|
554
|
+
This may cause the request to hang indefinitely.
|
|
555
|
+
|
|
556
|
+
Your middleware: ${handler.toString().split('\n')[0].substring(0, 80)}...
|
|
557
|
+
|
|
558
|
+
Fix: Add 'next' as third parameter and call it:
|
|
559
|
+
✅ (req, res, next) => { /* your code */ next(); }
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
notFound(handler) {
|
|
565
|
+
this.notFoundHandler = handler;
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
errorHandler(handler) {
|
|
570
|
+
if (handler.length !== 4) {
|
|
571
|
+
throw new Error('errorHandler() requires (err, req, res, next)');
|
|
572
|
+
}
|
|
573
|
+
this.errorHandlers.push(handler);
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
use(...args) {
|
|
578
|
+
// auto-mount router on "/"
|
|
579
|
+
if (args.length === 1 && args[0] instanceof LiekoExpress) {
|
|
580
|
+
this._mountRouter('/', args[0]);
|
|
581
|
+
return this;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// app.use(middleware)
|
|
585
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
586
|
+
this._checkMiddleware(args[0]);
|
|
587
|
+
this.middlewares.push({ path: null, handler: args[0] });
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// app.use(path, middleware)
|
|
592
|
+
if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'function') {
|
|
593
|
+
this._checkMiddleware(args[1]);
|
|
594
|
+
this.middlewares.push({ path: args[0], handler: args[1] });
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// app.use(path, router)
|
|
599
|
+
if (args.length === 2 && typeof args[0] === 'string' && args[1] instanceof LiekoExpress) {
|
|
600
|
+
this._mountRouter(args[0], args[1]);
|
|
601
|
+
return this;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// app.use(path, middleware, router)
|
|
605
|
+
if (args.length === 3 && typeof args[0] === 'string' && typeof args[1] === 'function' && args[2] instanceof LiekoExpress) {
|
|
606
|
+
const [path, middleware, router] = args;
|
|
607
|
+
this._checkMiddleware(middleware);
|
|
608
|
+
this.middlewares.push({ path, handler: middleware });
|
|
609
|
+
this._mountRouter(path, router);
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// app.use(path, ...middlewares, router)
|
|
614
|
+
if (args.length >= 3 && typeof args[0] === 'string') {
|
|
615
|
+
const path = args[0];
|
|
616
|
+
const lastArg = args[args.length - 1];
|
|
617
|
+
|
|
618
|
+
if (lastArg instanceof LiekoExpress) {
|
|
619
|
+
const middlewares = args.slice(1, -1);
|
|
620
|
+
middlewares.forEach(mw => {
|
|
621
|
+
if (typeof mw === 'function') {
|
|
622
|
+
this._checkMiddleware(mw);
|
|
623
|
+
this.middlewares.push({ path, handler: mw });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
this._mountRouter(path, lastArg);
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const middlewares = args.slice(1);
|
|
631
|
+
const allFunctions = middlewares.every(mw => typeof mw === 'function');
|
|
632
|
+
if (allFunctions) {
|
|
633
|
+
middlewares.forEach(mw => {
|
|
634
|
+
this._checkMiddleware(mw);
|
|
635
|
+
this.middlewares.push({ path, handler: mw });
|
|
636
|
+
});
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
throw new Error('Invalid use() arguments');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
_mountRouter(basePath, router) {
|
|
645
|
+
basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
646
|
+
router.groupStack = [...this.groupStack];
|
|
647
|
+
|
|
648
|
+
router.routes.forEach(route => {
|
|
649
|
+
const fullPath = route.path === '/' ? basePath : basePath + route.path;
|
|
650
|
+
|
|
651
|
+
this.routes.push({
|
|
652
|
+
...route,
|
|
653
|
+
path: fullPath,
|
|
654
|
+
pattern: this._pathToRegex(fullPath),
|
|
655
|
+
groupChain: [
|
|
656
|
+
...this.groupStack,
|
|
657
|
+
...(route.groupChain || [])
|
|
658
|
+
]
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
_addRoute(method, path, ...handlers) {
|
|
664
|
+
if (handlers.length === 0) {
|
|
665
|
+
throw new Error('Route handler is required');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const finalHandler = handlers[handlers.length - 1];
|
|
669
|
+
const routeMiddlewares = handlers.slice(0, -1);
|
|
670
|
+
|
|
671
|
+
routeMiddlewares.forEach(mw => {
|
|
672
|
+
if (typeof mw === 'function') {
|
|
673
|
+
this._checkMiddleware(mw);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
this.routes.push({
|
|
678
|
+
method,
|
|
679
|
+
path,
|
|
680
|
+
handler: finalHandler,
|
|
681
|
+
middlewares: routeMiddlewares,
|
|
682
|
+
pattern: this._pathToRegex(path),
|
|
683
|
+
groupChain: [...this.groupStack]
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
_pathToRegex(path) {
|
|
688
|
+
let pattern = path
|
|
689
|
+
.replace(/:(\w+)/g, '(?<$1>[^/]+)') // params :id
|
|
690
|
+
.replace(/\*/g, '.*'); // wildcards
|
|
691
|
+
|
|
692
|
+
const allowTrailing = this.settings.allowTrailingSlash === true ||
|
|
693
|
+
this.settings.strictTrailingSlash === false;
|
|
694
|
+
|
|
695
|
+
const isStaticRoute = !pattern.includes('(') &&
|
|
696
|
+
!pattern.includes('*') &&
|
|
697
|
+
!path.endsWith('/');
|
|
698
|
+
|
|
699
|
+
if (allowTrailing && isStaticRoute) {
|
|
700
|
+
pattern += '/?';
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return new RegExp(`^${pattern}$`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
_findRoute(method, pathname) {
|
|
707
|
+
for (const route of this.routes) {
|
|
708
|
+
if (route.method !== method && route.method !== 'ALL') continue;
|
|
709
|
+
const match = pathname.match(route.pattern);
|
|
710
|
+
if (match) {
|
|
711
|
+
return { ...route, params: match.groups || {} };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async _runErrorHandlers(err, req, res) {
|
|
718
|
+
if (this.errorHandlers.length === 0) {
|
|
719
|
+
console.error("\n🔥 INTERNAL ERROR");
|
|
720
|
+
console.error(err.stack || err);
|
|
721
|
+
return res.status(500).json({
|
|
722
|
+
success: false,
|
|
723
|
+
error: "Internal Server Error",
|
|
724
|
+
message: err.message
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let index = 0;
|
|
729
|
+
|
|
730
|
+
const runNext = async () => {
|
|
731
|
+
const handler = this.errorHandlers[index++];
|
|
732
|
+
if (!handler) return;
|
|
733
|
+
|
|
734
|
+
return new Promise((resolve, reject) => {
|
|
735
|
+
try {
|
|
736
|
+
handler(err, req, res, (nextErr) => {
|
|
737
|
+
if (nextErr) reject(nextErr);
|
|
738
|
+
else resolve(runNext());
|
|
739
|
+
});
|
|
740
|
+
} catch (e) {
|
|
741
|
+
reject(e);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
await runNext();
|
|
748
|
+
} catch (e) {
|
|
749
|
+
console.error("\n🔥 ERROR INSIDE ERROR HANDLER");
|
|
750
|
+
console.error(e.stack || e);
|
|
751
|
+
res.status(500).json({
|
|
752
|
+
success: false,
|
|
753
|
+
error: "Internal Server Error",
|
|
754
|
+
message: e.message
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
error(res, errorObj) {
|
|
760
|
+
if (typeof errorObj === "string") {
|
|
761
|
+
errorObj = { message: errorObj };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (!errorObj || typeof errorObj !== "object") {
|
|
765
|
+
return res.status(500).json({
|
|
766
|
+
success: false,
|
|
767
|
+
error: {
|
|
768
|
+
code: "SERVER_ERROR",
|
|
769
|
+
message: "Invalid error format passed to res.error()"
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const HTTP_STATUS = {
|
|
775
|
+
// 4xx — CLIENT ERRORS
|
|
776
|
+
INVALID_REQUEST: 400,
|
|
777
|
+
VALIDATION_FAILED: 400,
|
|
778
|
+
NO_TOKEN_PROVIDED: 401,
|
|
779
|
+
INVALID_TOKEN: 401,
|
|
780
|
+
FORBIDDEN: 403,
|
|
781
|
+
NOT_FOUND: 404,
|
|
782
|
+
METHOD_NOT_ALLOWED: 405,
|
|
783
|
+
CONFLICT: 409,
|
|
784
|
+
RECORD_EXISTS: 409,
|
|
785
|
+
TOO_MANY_REQUESTS: 429,
|
|
786
|
+
|
|
787
|
+
// 5xx — SERVER ERRORS
|
|
788
|
+
SERVER_ERROR: 500,
|
|
789
|
+
SERVICE_UNAVAILABLE: 503
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const status = errorObj.status || HTTP_STATUS[errorObj.code] || 500;
|
|
793
|
+
|
|
794
|
+
return res.status(status).json({
|
|
795
|
+
success: false,
|
|
796
|
+
error: {
|
|
797
|
+
code: errorObj.code || 'SERVER_ERROR',
|
|
798
|
+
message: errorObj.message || 'An error occurred',
|
|
799
|
+
...errorObj
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
_parseIp(rawIp) {
|
|
805
|
+
if (!rawIp) return { raw: null, ipv4: null, ipv6: null };
|
|
806
|
+
|
|
807
|
+
let ip = rawIp;
|
|
808
|
+
|
|
809
|
+
// Remove IPv6 IPv4-mapped prefix "::ffff:"
|
|
810
|
+
if (ip.startsWith("::ffff:")) {
|
|
811
|
+
ip = ip.replace("::ffff:", "");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const family = net.isIP(ip); // 0=invalid, 4=IPv4, 6=IPv6
|
|
815
|
+
|
|
816
|
+
return {
|
|
817
|
+
raw: rawIp,
|
|
818
|
+
ipv4: family === 4 ? ip : null,
|
|
819
|
+
ipv6: family === 6 ? ip : null,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
_isTrustedProxy(ip) {
|
|
824
|
+
const trust = this.settings['trust proxy'];
|
|
825
|
+
|
|
826
|
+
if (!trust) return false;
|
|
827
|
+
|
|
828
|
+
if (trust === true) return true;
|
|
829
|
+
|
|
830
|
+
if (trust === 'loopback') {
|
|
831
|
+
return ip === '127.0.0.1' || ip === '::1';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (typeof trust === 'string') {
|
|
835
|
+
return ip === trust;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (Array.isArray(trust)) {
|
|
839
|
+
return trust.includes(ip);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (typeof trust === 'function') {
|
|
843
|
+
return trust(ip);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async _handleRequest(req, res) {
|
|
850
|
+
this._enhanceRequest(req);
|
|
851
|
+
const url = req.url;
|
|
852
|
+
const questionMarkIndex = url.indexOf('?');
|
|
853
|
+
const pathname = questionMarkIndex === -1 ? url : url.substring(0, questionMarkIndex);
|
|
854
|
+
|
|
855
|
+
const query = {};
|
|
856
|
+
if (questionMarkIndex !== -1) {
|
|
857
|
+
const searchParams = new URLSearchParams(url.substring(questionMarkIndex + 1));
|
|
858
|
+
for (const [key, value] of searchParams) query[key] = value;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
req.query = query;
|
|
862
|
+
req.params = {};
|
|
863
|
+
|
|
864
|
+
for (const key in req.query) {
|
|
865
|
+
const value = req.query[key];
|
|
866
|
+
if (value === 'true') req.query[key] = true;
|
|
867
|
+
if (value === 'false') req.query[key] = false;
|
|
868
|
+
if (/^\d+$/.test(value)) req.query[key] = parseInt(value);
|
|
869
|
+
if (/^\d+\.\d+$/.test(value)) req.query[key] = parseFloat(value);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
await this._parseBody(req);
|
|
873
|
+
req._startTime = process.hrtime.bigint();
|
|
874
|
+
this._enhanceResponse(req, res);
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
for (const mw of this.middlewares) {
|
|
878
|
+
if (res.headersSent) return;
|
|
879
|
+
|
|
880
|
+
if (mw.path && !pathname.startsWith(mw.path)) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
await new Promise((resolve, reject) => {
|
|
885
|
+
const next = async (error) => {
|
|
886
|
+
if (error) {
|
|
887
|
+
await this._runErrorHandlers(error, req, res);
|
|
888
|
+
return resolve();
|
|
889
|
+
}
|
|
890
|
+
resolve();
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const result = mw.handler(req, res, next);
|
|
894
|
+
if (result && typeof result.then === 'function') {
|
|
895
|
+
result.then(resolve).catch(reject);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (res.headersSent) return;
|
|
901
|
+
|
|
902
|
+
const route = this._findRoute(req.method, pathname);
|
|
903
|
+
|
|
904
|
+
if (!route) {
|
|
905
|
+
if (this.notFoundHandler) {
|
|
906
|
+
return this.notFoundHandler(req, res);
|
|
907
|
+
} else {
|
|
908
|
+
return res.status(404).json({ error: 'Route not found' });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
req.params = route.params;
|
|
913
|
+
|
|
914
|
+
for (const middleware of route.middlewares) {
|
|
915
|
+
if (res.headersSent) return;
|
|
916
|
+
|
|
917
|
+
await new Promise((resolve, reject) => {
|
|
918
|
+
const next = async (error) => {
|
|
919
|
+
if (error) {
|
|
920
|
+
await this._runErrorHandlers(error, req, res);
|
|
921
|
+
return resolve();
|
|
922
|
+
}
|
|
923
|
+
resolve();
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const result = middleware(req, res, next);
|
|
927
|
+
if (result && typeof result.then === 'function') {
|
|
928
|
+
result.then(resolve).catch(reject);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (res.headersSent) return;
|
|
934
|
+
|
|
935
|
+
await route.handler(req, res);
|
|
936
|
+
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (!res.headersSent) {
|
|
939
|
+
await this._runErrorHandlers(error, req, res);
|
|
940
|
+
} else {
|
|
941
|
+
console.error("UNCAUGHT ERROR AFTER RESPONSE SENT:", error);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
_enhanceRequest(req) {
|
|
947
|
+
const remoteIp = req.connection.remoteAddress || '';
|
|
948
|
+
|
|
949
|
+
req.ips = [remoteIp];
|
|
950
|
+
let clientIp = remoteIp;
|
|
951
|
+
|
|
952
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
953
|
+
|
|
954
|
+
if (forwardedFor) {
|
|
955
|
+
const proxyChain = forwardedFor.split(',').map(ip => ip.trim());
|
|
956
|
+
|
|
957
|
+
if (this._isTrustedProxy(remoteIp)) {
|
|
958
|
+
clientIp = proxyChain[0];
|
|
959
|
+
req.ips = proxyChain;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
req.ip = this._parseIp(clientIp);
|
|
964
|
+
|
|
965
|
+
req.protocol = req.headers['x-forwarded-proto'] || 'http';
|
|
966
|
+
req.secure = req.protocol === 'https';
|
|
967
|
+
|
|
968
|
+
const host = req.headers['host'];
|
|
969
|
+
if (host) {
|
|
970
|
+
const [hostname] = host.split(':');
|
|
971
|
+
req.hostname = hostname;
|
|
972
|
+
req.subdomains = hostname.split('.').slice(0, -2).reverse();
|
|
973
|
+
} else {
|
|
974
|
+
req.hostname = '';
|
|
975
|
+
req.subdomains = [];
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
req.originalUrl = req.url;
|
|
979
|
+
req.xhr = (req.headers['x-requested-with'] || '').toLowerCase() === 'xmlhttprequest';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
_enhanceResponse(req, res) {
|
|
983
|
+
res.locals = {};
|
|
984
|
+
let responseSent = false;
|
|
985
|
+
let statusCode = 200;
|
|
986
|
+
|
|
987
|
+
const getDateHeader = (() => {
|
|
988
|
+
let cachedDate = '';
|
|
989
|
+
let lastTimestamp = 0;
|
|
990
|
+
|
|
991
|
+
return () => {
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
if (now !== lastTimestamp) {
|
|
994
|
+
lastTimestamp = now;
|
|
995
|
+
cachedDate = new Date(now).toUTCString();
|
|
996
|
+
}
|
|
997
|
+
return cachedDate;
|
|
998
|
+
};
|
|
999
|
+
})();
|
|
1000
|
+
|
|
1001
|
+
const buildHeaders = (contentType, length) => {
|
|
1002
|
+
const poweredBy = this.settings['x-powered-by'];
|
|
1003
|
+
const poweredByHeader = poweredBy
|
|
1004
|
+
? { 'X-Powered-By': poweredBy === true ? 'lieko-express' : poweredBy }
|
|
1005
|
+
: {};
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
'Content-Type': contentType,
|
|
1009
|
+
'Content-Length': length,
|
|
1010
|
+
'Date': getDateHeader(),
|
|
1011
|
+
'Connection': 'keep-alive',
|
|
1012
|
+
'Cache-Control': 'no-store',
|
|
1013
|
+
...poweredByHeader
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
res.status = (code) => {
|
|
1018
|
+
statusCode = code;
|
|
1019
|
+
res.statusCode = code;
|
|
1020
|
+
return res;
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
const originalSetHeader = res.setHeader.bind(res);
|
|
1024
|
+
res.setHeader = (name, value) => {
|
|
1025
|
+
originalSetHeader(name, value);
|
|
1026
|
+
return res;
|
|
1027
|
+
};
|
|
1028
|
+
res.set = res.setHeader;
|
|
1029
|
+
res.header = res.setHeader;
|
|
1030
|
+
|
|
1031
|
+
res.removeHeader = function (name) {
|
|
1032
|
+
res.removeHeader(name);
|
|
1033
|
+
return res;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
res.type = function (mime) {
|
|
1037
|
+
res.setHeader("Content-Type", mime);
|
|
1038
|
+
return res;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
res.json = (data) => {
|
|
1042
|
+
if (responseSent) return res;
|
|
1043
|
+
|
|
1044
|
+
const json = JSON.stringify(data);
|
|
1045
|
+
const length = Buffer.byteLength(json);
|
|
1046
|
+
|
|
1047
|
+
res.writeHead(statusCode, buildHeaders('application/json; charset=utf-8', length));
|
|
1048
|
+
|
|
1049
|
+
responseSent = true;
|
|
1050
|
+
statusCode = 200;
|
|
1051
|
+
return res.end(json);
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
res.send = (data) => {
|
|
1055
|
+
if (responseSent) return res;
|
|
1056
|
+
|
|
1057
|
+
let body, contentType;
|
|
1058
|
+
|
|
1059
|
+
if (data === null) {
|
|
1060
|
+
body = 'null';
|
|
1061
|
+
contentType = 'application/json; charset=utf-8';
|
|
1062
|
+
} else if (typeof data === 'object') {
|
|
1063
|
+
body = JSON.stringify(data);
|
|
1064
|
+
contentType = 'application/json; charset=utf-8';
|
|
1065
|
+
} else if (typeof data === 'string') {
|
|
1066
|
+
body = data;
|
|
1067
|
+
contentType = 'text/plain; charset=utf-8';
|
|
1068
|
+
} else {
|
|
1069
|
+
body = String(data);
|
|
1070
|
+
contentType = 'text/plain; charset=utf-8';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const length = Buffer.byteLength(body);
|
|
1074
|
+
|
|
1075
|
+
res.writeHead(statusCode, buildHeaders(contentType, length));
|
|
1076
|
+
|
|
1077
|
+
responseSent = true;
|
|
1078
|
+
statusCode = 200;
|
|
1079
|
+
return res.end(body);
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
res.html = function (html, status = 200) {
|
|
1083
|
+
res.statusCode = status;
|
|
1084
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1085
|
+
res.end(html);
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
res.ok = (data, message) => {
|
|
1089
|
+
const payload = { success: true, data };
|
|
1090
|
+
if (message !== undefined) payload.message = message;
|
|
1091
|
+
return res.status(200).json(payload);
|
|
1092
|
+
};
|
|
1093
|
+
res.success = res.ok;
|
|
1094
|
+
|
|
1095
|
+
res.created = (data, message = 'Resource created successfully') => {
|
|
1096
|
+
const payload = { success: true, data, message };
|
|
1097
|
+
return res.status(201).json(payload);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
res.noContent = () => {
|
|
1101
|
+
return res.status(204).end();
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
res.accepted = (data = null, message = 'Request accepted') => {
|
|
1105
|
+
return res.status(202).json({ success: true, message, data });
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
res.paginated = (items, total, message = 'Data retrieved successfully') => {
|
|
1109
|
+
const page = Math.max(1, Number(req.query.page) || 1);
|
|
1110
|
+
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 10));
|
|
1111
|
+
const totalPages = Math.ceil(total / limit);
|
|
1112
|
+
|
|
1113
|
+
return res.status(200).json({
|
|
1114
|
+
success: true,
|
|
1115
|
+
data: items,
|
|
1116
|
+
message,
|
|
1117
|
+
pagination: {
|
|
1118
|
+
page,
|
|
1119
|
+
limit,
|
|
1120
|
+
total,
|
|
1121
|
+
totalPages,
|
|
1122
|
+
hasNext: page < totalPages,
|
|
1123
|
+
hasPrev: page > 1
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
res.redirect = (url, status = 302) => {
|
|
1129
|
+
responseSent = true;
|
|
1130
|
+
res.writeHead(status, { Location: url });
|
|
1131
|
+
res.end();
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
res.error = (obj) => this.error(res, obj);
|
|
1135
|
+
res.fail = res.error;
|
|
1136
|
+
|
|
1137
|
+
res.badRequest = function (msg = "BAD_REQUEST") {
|
|
1138
|
+
return res.status(400).error(msg);
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
res.unauthorized = function (msg = "UNAUTHORIZED") {
|
|
1142
|
+
return res.status(401).error(msg);
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
res.forbidden = function (msg = "FORBIDDEN") {
|
|
1146
|
+
return res.status(403).error(msg);
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
res.notFound = function (msg = "NOT_FOUND") {
|
|
1150
|
+
return res.status(404).error(msg);
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
res.serverError = function (msg = "SERVER_ERROR") {
|
|
1154
|
+
return res.status(500).error(msg);
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const originalEnd = res.end.bind(res);
|
|
1158
|
+
|
|
1159
|
+
res.end = (...args) => {
|
|
1160
|
+
const result = originalEnd(...args);
|
|
1161
|
+
|
|
1162
|
+
if (this.settings.debug && req._startTime) {
|
|
1163
|
+
const end = process.hrtime.bigint();
|
|
1164
|
+
const durationMs = Number(end - req._startTime) / 1_000_000;
|
|
1165
|
+
|
|
1166
|
+
this._debugLog(req, res, {
|
|
1167
|
+
time: durationMs
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return result;
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async _parseBody(req) {
|
|
1176
|
+
return new Promise((resolve) => {
|
|
1177
|
+
if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
|
|
1178
|
+
req.body = {};
|
|
1179
|
+
req.files = {};
|
|
1180
|
+
return resolve();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
|
1184
|
+
req.body = {};
|
|
1185
|
+
req.files = {};
|
|
1186
|
+
|
|
1187
|
+
let raw = '';
|
|
1188
|
+
req.on('data', chunk => raw += chunk);
|
|
1189
|
+
req.on('end', () => {
|
|
1190
|
+
|
|
1191
|
+
if (contentType.includes('application/json')) {
|
|
1192
|
+
try {
|
|
1193
|
+
req.body = JSON.parse(raw);
|
|
1194
|
+
} catch {
|
|
1195
|
+
req.body = {};
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
1199
|
+
req.body = Object.fromEntries(new URLSearchParams(raw));
|
|
1200
|
+
}
|
|
1201
|
+
else if (contentType.includes('multipart/form-data')) {
|
|
1202
|
+
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
1203
|
+
if (boundaryMatch) {
|
|
1204
|
+
const boundary = '--' + boundaryMatch[1];
|
|
1205
|
+
const parts = raw.split(boundary).filter(p => p && !p.includes('--'));
|
|
1206
|
+
|
|
1207
|
+
for (let part of parts) {
|
|
1208
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
1209
|
+
if (headerEnd === -1) continue;
|
|
1210
|
+
|
|
1211
|
+
const headers = part.slice(0, headerEnd);
|
|
1212
|
+
const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
|
|
1213
|
+
|
|
1214
|
+
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
1215
|
+
const filenameMatch = headers.match(/filename="([^"]*)"/);
|
|
1216
|
+
const field = nameMatch?.[1];
|
|
1217
|
+
if (!field) continue;
|
|
1218
|
+
|
|
1219
|
+
if (filenameMatch?.[1]) {
|
|
1220
|
+
req.files[field] = {
|
|
1221
|
+
filename: filenameMatch[1],
|
|
1222
|
+
data: Buffer.from(body, 'binary'),
|
|
1223
|
+
contentType: headers.match(/Content-Type: (.*)/i)?.[1] || 'application/octet-stream'
|
|
1224
|
+
};
|
|
1225
|
+
} else {
|
|
1226
|
+
req.body[field] = body;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
req.body = raw ? { text: raw } : {};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
for (const key in req.body) {
|
|
1236
|
+
const value = req.body[key];
|
|
1237
|
+
|
|
1238
|
+
if (typeof value === 'string' && value.trim() !== '' && !isNaN(value) && !isNaN(parseFloat(value))) {
|
|
1239
|
+
req.body[key] = parseFloat(value);
|
|
1240
|
+
}
|
|
1241
|
+
else if (value === 'true') {
|
|
1242
|
+
req.body[key] = true;
|
|
1243
|
+
}
|
|
1244
|
+
else if (value === 'false') {
|
|
1245
|
+
req.body[key] = false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
resolve();
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async _runMiddleware(handler, req, res) {
|
|
1255
|
+
return new Promise((resolve, reject) => {
|
|
1256
|
+
const next = (err) => err ? reject(err) : resolve();
|
|
1257
|
+
const result = handler(req, res, next);
|
|
1258
|
+
if (result && typeof result.then === 'function') {
|
|
1259
|
+
result.then(resolve).catch(reject);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
_debugLog(req, res, meta) {
|
|
1265
|
+
if (!this.settings.debug) return;
|
|
1266
|
+
|
|
1267
|
+
let timeFormatted;
|
|
1268
|
+
const timeMs = meta.time;
|
|
1269
|
+
|
|
1270
|
+
if (timeMs < 1) {
|
|
1271
|
+
const us = (timeMs * 1000).toFixed(1);
|
|
1272
|
+
timeFormatted = `${us}µs`;
|
|
1273
|
+
} else if (timeMs >= 1000) {
|
|
1274
|
+
const s = (timeMs / 1000).toFixed(3);
|
|
1275
|
+
timeFormatted = `${s}s`;
|
|
1276
|
+
} else {
|
|
1277
|
+
timeFormatted = `${timeMs.toFixed(3)}ms`;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const color = (code) =>
|
|
1281
|
+
code >= 500 ? '\x1b[31m' :
|
|
1282
|
+
code >= 400 ? '\x1b[33m' :
|
|
1283
|
+
code >= 300 ? '\x1b[36m' :
|
|
1284
|
+
'\x1b[32m';
|
|
1285
|
+
|
|
1286
|
+
console.log(
|
|
1287
|
+
`\n🟦 DEBUG REQUEST` +
|
|
1288
|
+
`\n→ ${req.method} ${req.originalUrl}` +
|
|
1289
|
+
`\n→ IP: ${req.ip.ipv4}` +
|
|
1290
|
+
`\n→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m` +
|
|
1291
|
+
`\n→ Duration: ${timeFormatted}` +
|
|
1292
|
+
`\n→ Params: ${JSON.stringify(req.params || {})}` +
|
|
1293
|
+
`\n→ Query: ${JSON.stringify(req.query || {})}` +
|
|
1294
|
+
`\n→ Body: ${JSON.stringify(req.body || {})}` +
|
|
1295
|
+
`\n→ Files: ${Object.keys(req.files || {}).join(', ')}` +
|
|
1296
|
+
`\n---------------------------------------------\n`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
_buildRouteTree() {
|
|
1301
|
+
const tree = {};
|
|
1302
|
+
|
|
1303
|
+
for (const route of this.routes) {
|
|
1304
|
+
let node = tree;
|
|
1305
|
+
|
|
1306
|
+
for (const group of route.groupChain) {
|
|
1307
|
+
const key = group.basePath;
|
|
1308
|
+
|
|
1309
|
+
if (!node[key]) {
|
|
1310
|
+
node[key] = {
|
|
1311
|
+
__meta: group,
|
|
1312
|
+
__children: {},
|
|
1313
|
+
__routes: []
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
node = node[key].__children;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (!node.__routes) node.__routes = [];
|
|
1321
|
+
node.__routes.push(route);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return tree;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
listRoutes() {
|
|
1328
|
+
return this.routes.map(route => ({
|
|
1329
|
+
method: route.method,
|
|
1330
|
+
path: route.path,
|
|
1331
|
+
middlewares: route.middlewares.length
|
|
1332
|
+
}));
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
printRoutes() {
|
|
1336
|
+
console.log('\n📌 Registered Routes:\n');
|
|
1337
|
+
|
|
1338
|
+
this.routes.forEach(r => {
|
|
1339
|
+
console.log(
|
|
1340
|
+
`${r.method.padEnd(6)} ${r.path} ` +
|
|
1341
|
+
`(mw: ${r.middlewares.length})`
|
|
1342
|
+
);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
console.log('');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
printRoutesNested(tree = null, indent = '') {
|
|
1349
|
+
if (!tree) {
|
|
1350
|
+
console.log('\n📌 Nested Routes:\n');
|
|
1351
|
+
tree = this._buildRouteTree();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const prefix = indent + ' ';
|
|
1355
|
+
|
|
1356
|
+
for (const [path, data] of Object.entries(tree)) {
|
|
1357
|
+
if (path.startsWith("__")) continue;
|
|
1358
|
+
|
|
1359
|
+
const mwCount = data.__meta.middlewares.length;
|
|
1360
|
+
console.log(`${indent}${path} [${mwCount} mw]`);
|
|
1361
|
+
|
|
1362
|
+
if (data.__routes && data.__routes.length) {
|
|
1363
|
+
for (const route of data.__routes) {
|
|
1364
|
+
const shortPath = route.path.replace(path, '') || '/';
|
|
1365
|
+
console.log(`${prefix}${route.method.padEnd(6)} ${shortPath}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
this.printRoutesNested(data.__children, prefix);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (tree.__routes) {
|
|
1372
|
+
for (const route of tree.__routes) {
|
|
1373
|
+
console.log(`${indent}${route.method.padEnd(6)} ${route.path}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
listen() {
|
|
1379
|
+
const args = Array.from(arguments);
|
|
1380
|
+
const server = createServer(this._handleRequest.bind(this));
|
|
1381
|
+
|
|
1382
|
+
server.listen.apply(server, args);
|
|
1383
|
+
this.server = server;
|
|
1384
|
+
return server;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function Lieko() {
|
|
1389
|
+
return new LiekoExpress();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function Router() {
|
|
1393
|
+
return new Lieko();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
module.exports = Lieko;
|
|
1397
|
+
module.exports.Router = Router;
|
|
1398
|
+
module.exports.Schema = Schema;
|
|
1399
|
+
module.exports.schema = (...args) => new Schema(...args);
|
|
1400
|
+
module.exports.validators = validators;
|
|
1401
|
+
module.exports.validate = validate;
|
|
1402
|
+
module.exports.validatePartial = validatePartial;
|
|
1403
|
+
module.exports.ValidationError = ValidationError;
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lieko-express",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Lieko-express — A Modern, Minimal, REST API Framework for Node.js",
|
|
5
|
+
"main": "lieko-express.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "EiwSrvt eiwsrvt@gmail.com",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"files": [
|
|
14
|
+
"lieko-express.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
]
|
|
17
|
+
}
|