@technomoron/api-server-base 1.1.6 → 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.
@@ -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({
@@ -705,7 +758,7 @@ class ApiServer {
705
758
  this.authModule(module);
706
759
  }
707
760
  module.checkConfig();
708
- const base = this.config.apiBasePath ?? '/api';
761
+ const base = this.apiBasePath;
709
762
  const ns = module.namespace;
710
763
  const mountPath = `${base}${ns}`;
711
764
  module.mountpath = mountPath;
@@ -717,6 +770,7 @@ class ApiServer {
717
770
  }
718
771
  });
719
772
  this.app.use(mountPath, router);
773
+ this.ensureApiNotFoundOrdering();
720
774
  return this;
721
775
  }
722
776
  dumpRequest(apiReq) {
@@ -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;
@@ -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;
@@ -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({
@@ -697,7 +750,7 @@ export class ApiServer {
697
750
  this.authModule(module);
698
751
  }
699
752
  module.checkConfig();
700
- const base = this.config.apiBasePath ?? '/api';
753
+ const base = this.apiBasePath;
701
754
  const ns = module.namespace;
702
755
  const mountPath = `${base}${ns}`;
703
756
  module.mountpath = mountPath;
@@ -709,6 +762,7 @@ export class ApiServer {
709
762
  }
710
763
  });
711
764
  this.app.use(mountPath, router);
765
+ this.ensureApiNotFoundOrdering();
712
766
  return this;
713
767
  }
714
768
  dumpRequest(apiReq) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",