@technomoron/api-server-base 1.1.5 → 1.1.7
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.
|
@@ -325,7 +325,7 @@ function fillConfig(config) {
|
|
|
325
325
|
accessCookie: config.accessCookie ?? 'dat',
|
|
326
326
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
327
327
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
328
|
-
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60
|
|
328
|
+
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
|
|
329
329
|
authApi: config.authApi ?? false,
|
|
330
330
|
devMode: config.devMode ?? false,
|
|
331
331
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
@@ -335,7 +335,9 @@ function fillConfig(config) {
|
|
|
335
335
|
class ApiServer {
|
|
336
336
|
constructor(config = {}) {
|
|
337
337
|
this.currReq = null;
|
|
338
|
+
this.apiNotFoundHandler = null;
|
|
338
339
|
this.config = fillConfig(config);
|
|
340
|
+
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
339
341
|
this.storageAdapter = auth_storage_js_1.nullAuthStorage;
|
|
340
342
|
this.moduleAdapter = auth_module_js_1.nullAuthModule;
|
|
341
343
|
this.app = (0, express_1.default)();
|
|
@@ -345,6 +347,7 @@ class ApiServer {
|
|
|
345
347
|
}
|
|
346
348
|
this.middlewares();
|
|
347
349
|
// addSwaggerUi(this.app);
|
|
350
|
+
this.installApiNotFoundHandler();
|
|
348
351
|
}
|
|
349
352
|
authStorage(storage) {
|
|
350
353
|
this.storageAdapter = storage;
|
|
@@ -493,6 +496,56 @@ class ApiServer {
|
|
|
493
496
|
};
|
|
494
497
|
this.app.use((0, cors_1.default)(corsOptions));
|
|
495
498
|
}
|
|
499
|
+
normalizeApiBasePath(path) {
|
|
500
|
+
if (!path || typeof path !== 'string') {
|
|
501
|
+
return '/api';
|
|
502
|
+
}
|
|
503
|
+
const trimmed = path.trim();
|
|
504
|
+
if (!trimmed) {
|
|
505
|
+
return '/api';
|
|
506
|
+
}
|
|
507
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
508
|
+
if (withLeadingSlash.length === 1) {
|
|
509
|
+
return withLeadingSlash;
|
|
510
|
+
}
|
|
511
|
+
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
512
|
+
}
|
|
513
|
+
installApiNotFoundHandler() {
|
|
514
|
+
if (this.apiNotFoundHandler) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.apiNotFoundHandler = (req, res) => {
|
|
518
|
+
const payload = {
|
|
519
|
+
code: 404,
|
|
520
|
+
message: this.describeMissingEndpoint(req),
|
|
521
|
+
data: null,
|
|
522
|
+
errors: {}
|
|
523
|
+
};
|
|
524
|
+
res.status(404).json(payload);
|
|
525
|
+
};
|
|
526
|
+
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
527
|
+
}
|
|
528
|
+
ensureApiNotFoundOrdering() {
|
|
529
|
+
this.installApiNotFoundHandler();
|
|
530
|
+
if (!this.apiNotFoundHandler) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const stack = this.app?._router?.stack;
|
|
534
|
+
if (!stack) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
|
|
538
|
+
if (index === -1 || index === stack.length - 1) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const [layer] = stack.splice(index, 1);
|
|
542
|
+
stack.push(layer);
|
|
543
|
+
}
|
|
544
|
+
describeMissingEndpoint(req) {
|
|
545
|
+
const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
|
|
546
|
+
const target = req.originalUrl || req.url || this.apiBasePath;
|
|
547
|
+
return `No such endpoint: ${method} ${target}`;
|
|
548
|
+
}
|
|
496
549
|
start() {
|
|
497
550
|
this.app
|
|
498
551
|
.listen({
|
|
@@ -628,18 +681,18 @@ class ApiServer {
|
|
|
628
681
|
handle_request(handler, auth) {
|
|
629
682
|
return async (req, res, next) => {
|
|
630
683
|
void next;
|
|
684
|
+
const apiReq = {
|
|
685
|
+
server: this,
|
|
686
|
+
req,
|
|
687
|
+
res,
|
|
688
|
+
token: '',
|
|
689
|
+
tokenData: null,
|
|
690
|
+
getClientInfo: () => ensureClientInfo(apiReq),
|
|
691
|
+
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
692
|
+
getClientIpChain: () => ensureClientInfo(apiReq).ipchain
|
|
693
|
+
};
|
|
694
|
+
this.currReq = apiReq;
|
|
631
695
|
try {
|
|
632
|
-
const apiReq = {
|
|
633
|
-
server: this,
|
|
634
|
-
req,
|
|
635
|
-
res,
|
|
636
|
-
token: '',
|
|
637
|
-
tokenData: null,
|
|
638
|
-
getClientInfo: () => ensureClientInfo(apiReq),
|
|
639
|
-
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
640
|
-
getClientIpChain: () => ensureClientInfo(apiReq).ipchain
|
|
641
|
-
};
|
|
642
|
-
this.currReq = apiReq;
|
|
643
696
|
if (this.config.hydrateGetBody) {
|
|
644
697
|
hydrateGetBody(apiReq.req);
|
|
645
698
|
}
|
|
@@ -660,7 +713,11 @@ class ApiServer {
|
|
|
660
713
|
throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
|
|
661
714
|
}
|
|
662
715
|
const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
|
|
663
|
-
|
|
716
|
+
const responsePayload = { code, message, data };
|
|
717
|
+
if (this.config.debug) {
|
|
718
|
+
this.dumpResponse(apiReq, responsePayload, code);
|
|
719
|
+
}
|
|
720
|
+
res.status(code).json(responsePayload);
|
|
664
721
|
}
|
|
665
722
|
catch (error) {
|
|
666
723
|
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
@@ -668,20 +725,28 @@ class ApiServer {
|
|
|
668
725
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
669
726
|
? apiError.errors
|
|
670
727
|
: {};
|
|
671
|
-
|
|
728
|
+
const errorPayload = {
|
|
672
729
|
code: apiError.code,
|
|
673
730
|
message: apiError.message,
|
|
674
731
|
data: apiError.data ?? null,
|
|
675
732
|
errors: normalizedErrors
|
|
676
|
-
}
|
|
733
|
+
};
|
|
734
|
+
if (this.config.debug) {
|
|
735
|
+
this.dumpResponse(apiReq, errorPayload, apiError.code);
|
|
736
|
+
}
|
|
737
|
+
res.status(apiError.code).json(errorPayload);
|
|
677
738
|
}
|
|
678
739
|
else {
|
|
679
|
-
|
|
740
|
+
const errorPayload = {
|
|
680
741
|
code: 500,
|
|
681
742
|
message: this.guessExceptionText(error),
|
|
682
743
|
data: null,
|
|
683
744
|
errors: {}
|
|
684
|
-
}
|
|
745
|
+
};
|
|
746
|
+
if (this.config.debug) {
|
|
747
|
+
this.dumpResponse(apiReq, errorPayload, 500);
|
|
748
|
+
}
|
|
749
|
+
res.status(500).json(errorPayload);
|
|
685
750
|
}
|
|
686
751
|
}
|
|
687
752
|
};
|
|
@@ -693,7 +758,7 @@ class ApiServer {
|
|
|
693
758
|
this.authModule(module);
|
|
694
759
|
}
|
|
695
760
|
module.checkConfig();
|
|
696
|
-
const base = this.
|
|
761
|
+
const base = this.apiBasePath;
|
|
697
762
|
const ns = module.namespace;
|
|
698
763
|
const mountPath = `${base}${ns}`;
|
|
699
764
|
module.mountpath = mountPath;
|
|
@@ -705,6 +770,7 @@ class ApiServer {
|
|
|
705
770
|
}
|
|
706
771
|
});
|
|
707
772
|
this.app.use(mountPath, router);
|
|
773
|
+
this.ensureApiNotFoundOrdering();
|
|
708
774
|
return this;
|
|
709
775
|
}
|
|
710
776
|
dumpRequest(apiReq) {
|
|
@@ -719,6 +785,59 @@ class ApiServer {
|
|
|
719
785
|
console.log('Headers:', req.headers);
|
|
720
786
|
console.log('------------------------');
|
|
721
787
|
}
|
|
788
|
+
formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {
|
|
789
|
+
if (value === null || value === undefined) {
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
if (typeof value === 'string') {
|
|
793
|
+
return value.length <= maxLength
|
|
794
|
+
? value
|
|
795
|
+
: `${value.slice(0, maxLength)}… [truncated ${value.length - maxLength} chars]`;
|
|
796
|
+
}
|
|
797
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
if (typeof value === 'symbol') {
|
|
801
|
+
return value.toString();
|
|
802
|
+
}
|
|
803
|
+
if (value instanceof Date) {
|
|
804
|
+
return value.toISOString();
|
|
805
|
+
}
|
|
806
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
|
|
807
|
+
return `<Buffer length=${value.length}>`;
|
|
808
|
+
}
|
|
809
|
+
if (typeof value === 'function') {
|
|
810
|
+
return `[Function ${value.name || 'anonymous'}]`;
|
|
811
|
+
}
|
|
812
|
+
if (typeof value === 'object') {
|
|
813
|
+
const obj = value;
|
|
814
|
+
if (seen.has(obj)) {
|
|
815
|
+
return '[Circular]';
|
|
816
|
+
}
|
|
817
|
+
seen.add(obj);
|
|
818
|
+
if (Array.isArray(value)) {
|
|
819
|
+
const arr = value.map((item) => this.formatDebugValue(item, maxLength, seen));
|
|
820
|
+
seen.delete(obj);
|
|
821
|
+
return arr;
|
|
822
|
+
}
|
|
823
|
+
const recordValue = value;
|
|
824
|
+
const entries = Object.entries(recordValue).reduce((acc, [key, val]) => {
|
|
825
|
+
acc[key] = this.formatDebugValue(val, maxLength, seen);
|
|
826
|
+
return acc;
|
|
827
|
+
}, {});
|
|
828
|
+
seen.delete(obj);
|
|
829
|
+
return entries;
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
dumpResponse(apiReq, payload, status) {
|
|
834
|
+
const url = apiReq.req.originalUrl || apiReq.req.url;
|
|
835
|
+
console.log('--- Outgoing Response! ---');
|
|
836
|
+
console.log('URL:', url);
|
|
837
|
+
console.log('Status:', status);
|
|
838
|
+
console.log('Payload:', this.formatDebugValue(payload));
|
|
839
|
+
console.log('--------------------------');
|
|
840
|
+
}
|
|
722
841
|
}
|
|
723
842
|
exports.ApiServer = ApiServer;
|
|
724
843
|
exports.default = ApiServer;
|
|
@@ -9,7 +9,7 @@ import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
|
9
9
|
import { ApiModule } from './api-module.js';
|
|
10
10
|
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
11
|
import type { AuthProviderModule } from './auth-module.js';
|
|
12
|
-
import type { AuthStorage, AuthTokenData } from './auth-storage.js';
|
|
12
|
+
import type { AuthStorage, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
|
|
13
13
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
14
14
|
export type { Multer } from 'multer';
|
|
15
15
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -35,7 +35,7 @@ interface JwtDecodeResult<T> {
|
|
|
35
35
|
data?: T;
|
|
36
36
|
error?: string;
|
|
37
37
|
}
|
|
38
|
-
export interface ApiTokenData extends JwtPayload {
|
|
38
|
+
export interface ApiTokenData extends JwtPayload, AuthTokenMetadata {
|
|
39
39
|
uid: unknown;
|
|
40
40
|
iat?: number;
|
|
41
41
|
exp?: number;
|
|
@@ -101,8 +101,10 @@ export declare class ApiServer {
|
|
|
101
101
|
app: Application;
|
|
102
102
|
currReq: ApiRequest | null;
|
|
103
103
|
readonly config: ApiServerConf;
|
|
104
|
+
private readonly apiBasePath;
|
|
104
105
|
private storageAdapter;
|
|
105
106
|
private moduleAdapter;
|
|
107
|
+
private apiNotFoundHandler;
|
|
106
108
|
constructor(config?: Partial<ApiServerConf>);
|
|
107
109
|
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
108
110
|
/**
|
|
@@ -134,6 +136,10 @@ export declare class ApiServer {
|
|
|
134
136
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
135
137
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
136
138
|
private middlewares;
|
|
139
|
+
private normalizeApiBasePath;
|
|
140
|
+
private installApiNotFoundHandler;
|
|
141
|
+
private ensureApiNotFoundOrdering;
|
|
142
|
+
private describeMissingEndpoint;
|
|
137
143
|
start(): this;
|
|
138
144
|
private verifyJWT;
|
|
139
145
|
private authenticate;
|
|
@@ -144,5 +150,7 @@ export declare class ApiServer {
|
|
|
144
150
|
private handle_request;
|
|
145
151
|
api<T extends ApiModule<any>>(module: T): this;
|
|
146
152
|
dumpRequest(apiReq: ApiRequest): void;
|
|
153
|
+
private formatDebugValue;
|
|
154
|
+
dumpResponse(apiReq: ApiRequest, payload: unknown, status: number): void;
|
|
147
155
|
}
|
|
148
156
|
export default ApiServer;
|
|
@@ -9,7 +9,7 @@ import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
|
9
9
|
import { ApiModule } from './api-module.js';
|
|
10
10
|
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
11
|
import type { AuthProviderModule } from './auth-module.js';
|
|
12
|
-
import type { AuthStorage, AuthTokenData } from './auth-storage.js';
|
|
12
|
+
import type { AuthStorage, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
|
|
13
13
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
14
14
|
export type { Multer } from 'multer';
|
|
15
15
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -35,7 +35,7 @@ interface JwtDecodeResult<T> {
|
|
|
35
35
|
data?: T;
|
|
36
36
|
error?: string;
|
|
37
37
|
}
|
|
38
|
-
export interface ApiTokenData extends JwtPayload {
|
|
38
|
+
export interface ApiTokenData extends JwtPayload, AuthTokenMetadata {
|
|
39
39
|
uid: unknown;
|
|
40
40
|
iat?: number;
|
|
41
41
|
exp?: number;
|
|
@@ -101,8 +101,10 @@ export declare class ApiServer {
|
|
|
101
101
|
app: Application;
|
|
102
102
|
currReq: ApiRequest | null;
|
|
103
103
|
readonly config: ApiServerConf;
|
|
104
|
+
private readonly apiBasePath;
|
|
104
105
|
private storageAdapter;
|
|
105
106
|
private moduleAdapter;
|
|
107
|
+
private apiNotFoundHandler;
|
|
106
108
|
constructor(config?: Partial<ApiServerConf>);
|
|
107
109
|
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
108
110
|
/**
|
|
@@ -134,6 +136,10 @@ export declare class ApiServer {
|
|
|
134
136
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
135
137
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
136
138
|
private middlewares;
|
|
139
|
+
private normalizeApiBasePath;
|
|
140
|
+
private installApiNotFoundHandler;
|
|
141
|
+
private ensureApiNotFoundOrdering;
|
|
142
|
+
private describeMissingEndpoint;
|
|
137
143
|
start(): this;
|
|
138
144
|
private verifyJWT;
|
|
139
145
|
private authenticate;
|
|
@@ -144,5 +150,7 @@ export declare class ApiServer {
|
|
|
144
150
|
private handle_request;
|
|
145
151
|
api<T extends ApiModule<any>>(module: T): this;
|
|
146
152
|
dumpRequest(apiReq: ApiRequest): void;
|
|
153
|
+
private formatDebugValue;
|
|
154
|
+
dumpResponse(apiReq: ApiRequest, payload: unknown, status: number): void;
|
|
147
155
|
}
|
|
148
156
|
export default ApiServer;
|
|
@@ -317,7 +317,7 @@ function fillConfig(config) {
|
|
|
317
317
|
accessCookie: config.accessCookie ?? 'dat',
|
|
318
318
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
319
319
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
320
|
-
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60
|
|
320
|
+
refreshExpiry: config.refreshExpiry ?? 30 * 24 * 60 * 60,
|
|
321
321
|
authApi: config.authApi ?? false,
|
|
322
322
|
devMode: config.devMode ?? false,
|
|
323
323
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
@@ -327,7 +327,9 @@ function fillConfig(config) {
|
|
|
327
327
|
export class ApiServer {
|
|
328
328
|
constructor(config = {}) {
|
|
329
329
|
this.currReq = null;
|
|
330
|
+
this.apiNotFoundHandler = null;
|
|
330
331
|
this.config = fillConfig(config);
|
|
332
|
+
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
331
333
|
this.storageAdapter = nullAuthStorage;
|
|
332
334
|
this.moduleAdapter = nullAuthModule;
|
|
333
335
|
this.app = express();
|
|
@@ -337,6 +339,7 @@ export class ApiServer {
|
|
|
337
339
|
}
|
|
338
340
|
this.middlewares();
|
|
339
341
|
// addSwaggerUi(this.app);
|
|
342
|
+
this.installApiNotFoundHandler();
|
|
340
343
|
}
|
|
341
344
|
authStorage(storage) {
|
|
342
345
|
this.storageAdapter = storage;
|
|
@@ -485,6 +488,56 @@ export class ApiServer {
|
|
|
485
488
|
};
|
|
486
489
|
this.app.use(cors(corsOptions));
|
|
487
490
|
}
|
|
491
|
+
normalizeApiBasePath(path) {
|
|
492
|
+
if (!path || typeof path !== 'string') {
|
|
493
|
+
return '/api';
|
|
494
|
+
}
|
|
495
|
+
const trimmed = path.trim();
|
|
496
|
+
if (!trimmed) {
|
|
497
|
+
return '/api';
|
|
498
|
+
}
|
|
499
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
500
|
+
if (withLeadingSlash.length === 1) {
|
|
501
|
+
return withLeadingSlash;
|
|
502
|
+
}
|
|
503
|
+
return withLeadingSlash.replace(/\/+$/, '') || '/api';
|
|
504
|
+
}
|
|
505
|
+
installApiNotFoundHandler() {
|
|
506
|
+
if (this.apiNotFoundHandler) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.apiNotFoundHandler = (req, res) => {
|
|
510
|
+
const payload = {
|
|
511
|
+
code: 404,
|
|
512
|
+
message: this.describeMissingEndpoint(req),
|
|
513
|
+
data: null,
|
|
514
|
+
errors: {}
|
|
515
|
+
};
|
|
516
|
+
res.status(404).json(payload);
|
|
517
|
+
};
|
|
518
|
+
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
519
|
+
}
|
|
520
|
+
ensureApiNotFoundOrdering() {
|
|
521
|
+
this.installApiNotFoundHandler();
|
|
522
|
+
if (!this.apiNotFoundHandler) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const stack = this.app?._router?.stack;
|
|
526
|
+
if (!stack) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const index = stack.findIndex((layer) => layer.handle === this.apiNotFoundHandler);
|
|
530
|
+
if (index === -1 || index === stack.length - 1) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const [layer] = stack.splice(index, 1);
|
|
534
|
+
stack.push(layer);
|
|
535
|
+
}
|
|
536
|
+
describeMissingEndpoint(req) {
|
|
537
|
+
const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
|
|
538
|
+
const target = req.originalUrl || req.url || this.apiBasePath;
|
|
539
|
+
return `No such endpoint: ${method} ${target}`;
|
|
540
|
+
}
|
|
488
541
|
start() {
|
|
489
542
|
this.app
|
|
490
543
|
.listen({
|
|
@@ -620,18 +673,18 @@ export class ApiServer {
|
|
|
620
673
|
handle_request(handler, auth) {
|
|
621
674
|
return async (req, res, next) => {
|
|
622
675
|
void next;
|
|
676
|
+
const apiReq = {
|
|
677
|
+
server: this,
|
|
678
|
+
req,
|
|
679
|
+
res,
|
|
680
|
+
token: '',
|
|
681
|
+
tokenData: null,
|
|
682
|
+
getClientInfo: () => ensureClientInfo(apiReq),
|
|
683
|
+
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
684
|
+
getClientIpChain: () => ensureClientInfo(apiReq).ipchain
|
|
685
|
+
};
|
|
686
|
+
this.currReq = apiReq;
|
|
623
687
|
try {
|
|
624
|
-
const apiReq = {
|
|
625
|
-
server: this,
|
|
626
|
-
req,
|
|
627
|
-
res,
|
|
628
|
-
token: '',
|
|
629
|
-
tokenData: null,
|
|
630
|
-
getClientInfo: () => ensureClientInfo(apiReq),
|
|
631
|
-
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
632
|
-
getClientIpChain: () => ensureClientInfo(apiReq).ipchain
|
|
633
|
-
};
|
|
634
|
-
this.currReq = apiReq;
|
|
635
688
|
if (this.config.hydrateGetBody) {
|
|
636
689
|
hydrateGetBody(apiReq.req);
|
|
637
690
|
}
|
|
@@ -652,7 +705,11 @@ export class ApiServer {
|
|
|
652
705
|
throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
|
|
653
706
|
}
|
|
654
707
|
const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
|
|
655
|
-
|
|
708
|
+
const responsePayload = { code, message, data };
|
|
709
|
+
if (this.config.debug) {
|
|
710
|
+
this.dumpResponse(apiReq, responsePayload, code);
|
|
711
|
+
}
|
|
712
|
+
res.status(code).json(responsePayload);
|
|
656
713
|
}
|
|
657
714
|
catch (error) {
|
|
658
715
|
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
@@ -660,20 +717,28 @@ export class ApiServer {
|
|
|
660
717
|
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
661
718
|
? apiError.errors
|
|
662
719
|
: {};
|
|
663
|
-
|
|
720
|
+
const errorPayload = {
|
|
664
721
|
code: apiError.code,
|
|
665
722
|
message: apiError.message,
|
|
666
723
|
data: apiError.data ?? null,
|
|
667
724
|
errors: normalizedErrors
|
|
668
|
-
}
|
|
725
|
+
};
|
|
726
|
+
if (this.config.debug) {
|
|
727
|
+
this.dumpResponse(apiReq, errorPayload, apiError.code);
|
|
728
|
+
}
|
|
729
|
+
res.status(apiError.code).json(errorPayload);
|
|
669
730
|
}
|
|
670
731
|
else {
|
|
671
|
-
|
|
732
|
+
const errorPayload = {
|
|
672
733
|
code: 500,
|
|
673
734
|
message: this.guessExceptionText(error),
|
|
674
735
|
data: null,
|
|
675
736
|
errors: {}
|
|
676
|
-
}
|
|
737
|
+
};
|
|
738
|
+
if (this.config.debug) {
|
|
739
|
+
this.dumpResponse(apiReq, errorPayload, 500);
|
|
740
|
+
}
|
|
741
|
+
res.status(500).json(errorPayload);
|
|
677
742
|
}
|
|
678
743
|
}
|
|
679
744
|
};
|
|
@@ -685,7 +750,7 @@ export class ApiServer {
|
|
|
685
750
|
this.authModule(module);
|
|
686
751
|
}
|
|
687
752
|
module.checkConfig();
|
|
688
|
-
const base = this.
|
|
753
|
+
const base = this.apiBasePath;
|
|
689
754
|
const ns = module.namespace;
|
|
690
755
|
const mountPath = `${base}${ns}`;
|
|
691
756
|
module.mountpath = mountPath;
|
|
@@ -697,6 +762,7 @@ export class ApiServer {
|
|
|
697
762
|
}
|
|
698
763
|
});
|
|
699
764
|
this.app.use(mountPath, router);
|
|
765
|
+
this.ensureApiNotFoundOrdering();
|
|
700
766
|
return this;
|
|
701
767
|
}
|
|
702
768
|
dumpRequest(apiReq) {
|
|
@@ -711,5 +777,58 @@ export class ApiServer {
|
|
|
711
777
|
console.log('Headers:', req.headers);
|
|
712
778
|
console.log('------------------------');
|
|
713
779
|
}
|
|
780
|
+
formatDebugValue(value, maxLength = 50, seen = new WeakSet()) {
|
|
781
|
+
if (value === null || value === undefined) {
|
|
782
|
+
return value;
|
|
783
|
+
}
|
|
784
|
+
if (typeof value === 'string') {
|
|
785
|
+
return value.length <= maxLength
|
|
786
|
+
? value
|
|
787
|
+
: `${value.slice(0, maxLength)}… [truncated ${value.length - maxLength} chars]`;
|
|
788
|
+
}
|
|
789
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
if (typeof value === 'symbol') {
|
|
793
|
+
return value.toString();
|
|
794
|
+
}
|
|
795
|
+
if (value instanceof Date) {
|
|
796
|
+
return value.toISOString();
|
|
797
|
+
}
|
|
798
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
|
|
799
|
+
return `<Buffer length=${value.length}>`;
|
|
800
|
+
}
|
|
801
|
+
if (typeof value === 'function') {
|
|
802
|
+
return `[Function ${value.name || 'anonymous'}]`;
|
|
803
|
+
}
|
|
804
|
+
if (typeof value === 'object') {
|
|
805
|
+
const obj = value;
|
|
806
|
+
if (seen.has(obj)) {
|
|
807
|
+
return '[Circular]';
|
|
808
|
+
}
|
|
809
|
+
seen.add(obj);
|
|
810
|
+
if (Array.isArray(value)) {
|
|
811
|
+
const arr = value.map((item) => this.formatDebugValue(item, maxLength, seen));
|
|
812
|
+
seen.delete(obj);
|
|
813
|
+
return arr;
|
|
814
|
+
}
|
|
815
|
+
const recordValue = value;
|
|
816
|
+
const entries = Object.entries(recordValue).reduce((acc, [key, val]) => {
|
|
817
|
+
acc[key] = this.formatDebugValue(val, maxLength, seen);
|
|
818
|
+
return acc;
|
|
819
|
+
}, {});
|
|
820
|
+
seen.delete(obj);
|
|
821
|
+
return entries;
|
|
822
|
+
}
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
dumpResponse(apiReq, payload, status) {
|
|
826
|
+
const url = apiReq.req.originalUrl || apiReq.req.url;
|
|
827
|
+
console.log('--- Outgoing Response! ---');
|
|
828
|
+
console.log('URL:', url);
|
|
829
|
+
console.log('Status:', status);
|
|
830
|
+
console.log('Payload:', this.formatDebugValue(payload));
|
|
831
|
+
console.log('--------------------------');
|
|
832
|
+
}
|
|
714
833
|
}
|
|
715
834
|
export default ApiServer;
|