@technomoron/api-server-base 1.1.1 → 1.1.3

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.
@@ -133,6 +133,13 @@ class ApiError extends Error {
133
133
  }
134
134
  }
135
135
  exports.ApiError = ApiError;
136
+ function isApiErrorLike(candidate) {
137
+ if (!candidate || typeof candidate !== 'object') {
138
+ return false;
139
+ }
140
+ const maybeError = candidate;
141
+ return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
142
+ }
136
143
  function fillConfig(config) {
137
144
  return {
138
145
  apiPort: config.apiPort ?? 3101,
@@ -428,16 +435,31 @@ class ApiServer {
428
435
  }
429
436
  apiReq.tokenData = await this.authenticate(apiReq, auth.type);
430
437
  await this.authorize(apiReq, auth.req);
431
- const [code, data = null, message = 'Success'] = await handler(apiReq);
438
+ const handlerResult = await handler(apiReq);
439
+ if (!Array.isArray(handlerResult)) {
440
+ throw new ApiError({
441
+ code: 500,
442
+ message: 'Handler result must be an array: [status, data?, message?]'
443
+ });
444
+ }
445
+ const [code, data = null, rawMessage = 'Success'] = handlerResult;
446
+ if (typeof code !== 'number' || Number.isNaN(code)) {
447
+ throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
448
+ }
449
+ const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
432
450
  res.status(code).json({ code, message, data });
433
451
  }
434
452
  catch (error) {
435
- if (error instanceof ApiError) {
436
- res.status(error.code).json({
437
- code: error.code,
438
- message: error.message,
439
- data: error.data || null,
440
- errors: error.errors || []
453
+ if (error instanceof ApiError || isApiErrorLike(error)) {
454
+ const apiError = error;
455
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
456
+ ? apiError.errors
457
+ : {};
458
+ res.status(apiError.code).json({
459
+ code: apiError.code,
460
+ message: apiError.message,
461
+ data: apiError.data ?? null,
462
+ errors: normalizedErrors
441
463
  });
442
464
  }
443
465
  else {
@@ -445,7 +467,7 @@ class ApiServer {
445
467
  code: 500,
446
468
  message: this.guessExceptionText(error),
447
469
  data: null,
448
- errors: []
470
+ errors: {}
449
471
  });
450
472
  }
451
473
  }
@@ -5,6 +5,7 @@ export interface AuthTokenMetadata {
5
5
  fingerprint?: string;
6
6
  label?: string;
7
7
  scope?: string | string[];
8
+ revokeSessions?: 'device' | 'domain' | 'client' | 'user';
8
9
  }
9
10
  export interface AuthTokenData extends AuthTokenMetadata {
10
11
  access: string;
@@ -125,6 +125,13 @@ export class ApiError extends Error {
125
125
  this.errors = errors !== undefined ? errors : {};
126
126
  }
127
127
  }
128
+ function isApiErrorLike(candidate) {
129
+ if (!candidate || typeof candidate !== 'object') {
130
+ return false;
131
+ }
132
+ const maybeError = candidate;
133
+ return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
134
+ }
128
135
  function fillConfig(config) {
129
136
  return {
130
137
  apiPort: config.apiPort ?? 3101,
@@ -420,16 +427,31 @@ export class ApiServer {
420
427
  }
421
428
  apiReq.tokenData = await this.authenticate(apiReq, auth.type);
422
429
  await this.authorize(apiReq, auth.req);
423
- const [code, data = null, message = 'Success'] = await handler(apiReq);
430
+ const handlerResult = await handler(apiReq);
431
+ if (!Array.isArray(handlerResult)) {
432
+ throw new ApiError({
433
+ code: 500,
434
+ message: 'Handler result must be an array: [status, data?, message?]'
435
+ });
436
+ }
437
+ const [code, data = null, rawMessage = 'Success'] = handlerResult;
438
+ if (typeof code !== 'number' || Number.isNaN(code)) {
439
+ throw new ApiError({ code: 500, message: 'Handler result must start with a numeric status code' });
440
+ }
441
+ const message = typeof rawMessage === 'string' ? rawMessage : 'Success';
424
442
  res.status(code).json({ code, message, data });
425
443
  }
426
444
  catch (error) {
427
- if (error instanceof ApiError) {
428
- res.status(error.code).json({
429
- code: error.code,
430
- message: error.message,
431
- data: error.data || null,
432
- errors: error.errors || []
445
+ if (error instanceof ApiError || isApiErrorLike(error)) {
446
+ const apiError = error;
447
+ const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
448
+ ? apiError.errors
449
+ : {};
450
+ res.status(apiError.code).json({
451
+ code: apiError.code,
452
+ message: apiError.message,
453
+ data: apiError.data ?? null,
454
+ errors: normalizedErrors
433
455
  });
434
456
  }
435
457
  else {
@@ -437,7 +459,7 @@ export class ApiServer {
437
459
  code: 500,
438
460
  message: this.guessExceptionText(error),
439
461
  data: null,
440
- errors: []
462
+ errors: {}
441
463
  });
442
464
  }
443
465
  }
@@ -5,6 +5,7 @@ export interface AuthTokenMetadata {
5
5
  fingerprint?: string;
6
6
  label?: string;
7
7
  scope?: string | string[];
8
+ revokeSessions?: 'device' | 'domain' | 'client' | 'user';
8
9
  }
9
10
  export interface AuthTokenData extends AuthTokenMetadata {
10
11
  access: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",