@lucaapp/service-utils 1.28.1 → 1.30.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.
@@ -10,6 +10,7 @@ type ApiConstructorOptions = {
10
10
  apiVersion?: string;
11
11
  apiTitle?: string;
12
12
  apiDescription?: string;
13
+ debug?: boolean;
13
14
  };
14
15
  export declare class Api {
15
16
  router: Router;
@@ -19,6 +20,7 @@ export declare class Api {
19
20
  apiTitle: string;
20
21
  apiDescription: string;
21
22
  private prefixPath;
23
+ debug: boolean;
22
24
  constructor(options?: ApiConstructorOptions);
23
25
  get<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TMiddlewares extends ReadonlyArray<Middleware<any, any, any, any, any, any>> | undefined = undefined>(path: string, summary: string, options: EndpointOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>, handler: EndpointHandler<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>): void;
24
26
  post<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TMiddlewares extends ReadonlyArray<Middleware<any, any, any, any, any, any>> | undefined = undefined>(path: string, summary: string, options: EndpointOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>, handler: EndpointHandler<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>): void;
@@ -18,26 +18,27 @@ class Api {
18
18
  this.apiVersion = options.apiVersion || '1.0.0';
19
19
  this.apiTitle = options.apiTitle || 'API';
20
20
  this.apiDescription = options.apiDescription || '';
21
+ this.debug = options.debug || false;
21
22
  this.mountSwaggerMiddlewares();
22
23
  }
23
24
  get(path, summary, options, handler) {
24
- (0, endpoint_1.mountEndpoint)(this.router, 'get', path, options, handler);
25
+ (0, endpoint_1.mountEndpoint)(this.router, 'get', path, options, handler, this.debug);
25
26
  (0, endpoint_1.registerEndpoint)(this.registry, 'get', this.prefixPath, path, summary, options);
26
27
  }
27
28
  post(path, summary, options, handler) {
28
- (0, endpoint_1.mountEndpoint)(this.router, 'post', path, options, handler);
29
+ (0, endpoint_1.mountEndpoint)(this.router, 'post', path, options, handler, this.debug);
29
30
  (0, endpoint_1.registerEndpoint)(this.registry, 'post', this.prefixPath, path, summary, options);
30
31
  }
31
32
  patch(path, summary, options, handler) {
32
- (0, endpoint_1.mountEndpoint)(this.router, 'patch', path, options, handler);
33
+ (0, endpoint_1.mountEndpoint)(this.router, 'patch', path, options, handler, this.debug);
33
34
  (0, endpoint_1.registerEndpoint)(this.registry, 'patch', this.prefixPath, path, summary, options);
34
35
  }
35
36
  put(path, summary, options, handler) {
36
- (0, endpoint_1.mountEndpoint)(this.router, 'put', path, options, handler);
37
+ (0, endpoint_1.mountEndpoint)(this.router, 'put', path, options, handler, this.debug);
37
38
  (0, endpoint_1.registerEndpoint)(this.registry, 'put', this.prefixPath, path, summary, options);
38
39
  }
39
40
  delete(path, summary, options, handler) {
40
- (0, endpoint_1.mountEndpoint)(this.router, 'delete', path, options, handler);
41
+ (0, endpoint_1.mountEndpoint)(this.router, 'delete', path, options, handler, this.debug);
41
42
  (0, endpoint_1.registerEndpoint)(this.registry, 'delete', this.prefixPath, path, summary, options);
42
43
  }
43
44
  generateOpenAPISpec() {
@@ -3,6 +3,6 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
3
3
  import { Middleware, EndpointResponseSchema } from './types/middleware';
4
4
  import { EndpointOptions, EndpointHandler } from './types/endpoint';
5
5
  type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
6
- export declare const mountEndpoint: <TResponseSchemas extends readonly EndpointResponseSchema[], TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TMiddlewares extends readonly Middleware<any, any, any, any, any, any>[] | undefined = undefined>(router: Router, method: HttpMethod, path: string, options: EndpointOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>, handler: EndpointHandler<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>) => void;
6
+ export declare const mountEndpoint: <TResponseSchemas extends readonly EndpointResponseSchema[], TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TMiddlewares extends readonly Middleware<any, any, any, any, any, any>[] | undefined = undefined>(router: Router, method: HttpMethod, path: string, options: EndpointOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>, handler: EndpointHandler<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TMiddlewares>, debug: boolean) => void;
7
7
  export declare const registerEndpoint: (registry: OpenAPIRegistry, method: HttpMethod, prefixPath: string, path: string, summary: string, options: EndpointOptions<any, any, any, any, any, any>) => void;
8
8
  export {};
@@ -10,19 +10,46 @@ const boom_1 = require("@hapi/boom");
10
10
  const buildResponseConfig = (responseOptions) => {
11
11
  const responses = {};
12
12
  responseOptions.forEach(response => {
13
- responses[String(response.status)] = {
14
- description: response.description,
15
- ...(!(response.schema instanceof zod_1.z.ZodVoid) && {
16
- content: {
17
- 'application/json': {
18
- schema: response.schema,
13
+ const isAlreadyPresent = !!responses[String(response.status)];
14
+ if (!isAlreadyPresent) {
15
+ responses[String(response.status)] = {
16
+ description: response.description,
17
+ ...(!(response.schema instanceof zod_1.z.ZodVoid) && {
18
+ content: {
19
+ 'application/json': {
20
+ schema: response.schema,
21
+ },
19
22
  },
20
- },
21
- }),
22
- };
23
+ }),
24
+ };
25
+ return;
26
+ }
27
+ const existingResponse = responses[response.status];
28
+ existingResponse.description =
29
+ existingResponse.description + ' | ' + response.description;
30
+ // No Content merge
31
+ if (!response.schema) {
32
+ return;
33
+ }
34
+ // No Content
35
+ if (!existingResponse.content ||
36
+ !existingResponse.content['application/json']) {
37
+ return;
38
+ }
39
+ existingResponse.content['application/json'].schema =
40
+ existingResponse.content['application/json'].schema.or(response.schema);
23
41
  });
24
42
  return responses;
25
43
  };
44
+ const isTypedError = (error) => {
45
+ if (!(error instanceof Error))
46
+ return false;
47
+ // can't access possibly existing type property without casting to any
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ if (typeof error.type !== 'string')
50
+ return false;
51
+ return true;
52
+ };
26
53
  const validateAndSendResponse = async (expressResponse, response, validResponses) => {
27
54
  (0, assert_1.default)(typeof response.status === 'number');
28
55
  // get schema respective to status code
@@ -39,7 +66,7 @@ const validateAndSendResponse = async (expressResponse, response, validResponses
39
66
  }
40
67
  expressResponse.status(response.status).send(validatedResponseBody);
41
68
  };
42
- const handleMiddleware = async (middleware, context, request, response) => new Promise(async (resolve, reject) => {
69
+ const handleMiddleware = async (middleware, context, request, response, debug) => new Promise(async (resolve, reject) => {
43
70
  try {
44
71
  const handlerRequestBody = ((await middleware.options.schemas?.body?.parseAsync(request.body)) ||
45
72
  undefined);
@@ -58,9 +85,7 @@ const handleMiddleware = async (middleware, context, request, response) => new P
58
85
  method,
59
86
  path: route.path,
60
87
  };
61
- await middleware.handler(handlerRequest,
62
- // { ip, baseUrl, route, method } = request,
63
- async (middlewareResponse) => {
88
+ await middleware.handler(handlerRequest, async (middlewareResponse) => {
64
89
  try {
65
90
  await validateAndSendResponse(response, middlewareResponse, middleware.options.responses);
66
91
  resolve(false);
@@ -90,10 +115,21 @@ const handleMiddleware = async (middleware, context, request, response) => new P
90
115
  });
91
116
  }
92
117
  catch (error) {
118
+ if (isTypedError(error) &&
119
+ middleware.options.errors &&
120
+ middleware.options.errors[error.type]) {
121
+ const statusCode = middleware.options.errors[error.type];
122
+ return response.status(statusCode).send({
123
+ statusCode,
124
+ error: error.type,
125
+ message: error.message,
126
+ ...(debug && { stack: error.stack }),
127
+ });
128
+ }
93
129
  reject(error);
94
130
  }
95
131
  });
96
- const mountEndpoint = (router, method, path, options, handler) => {
132
+ const mountEndpoint = (router, method, path, options, handler, debug) => {
97
133
  (0, assert_1.default)(options.responses.length > 0, 'You need to specify at least one response');
98
134
  router[method](path, async (request, response, next) => {
99
135
  try {
@@ -113,7 +149,7 @@ const mountEndpoint = (router, method, path, options, handler) => {
113
149
  const ctx = {};
114
150
  if (options.middlewares instanceof Array) {
115
151
  for (const middleware of options.middlewares) {
116
- const shouldContinue = await handleMiddleware(middleware, ctx, request, response);
152
+ const shouldContinue = await handleMiddleware(middleware, ctx, request, response, debug);
117
153
  if (!shouldContinue)
118
154
  return;
119
155
  }
@@ -136,6 +172,17 @@ const mountEndpoint = (router, method, path, options, handler) => {
136
172
  if (error instanceof zod_1.z.ZodError && !(0, boom_1.isBoom)(error)) {
137
173
  return response.status(400).send({ issues: error.issues });
138
174
  }
175
+ if (isTypedError(error) &&
176
+ options.errors &&
177
+ options.errors[error.type]) {
178
+ const statusCode = options.errors[error.type];
179
+ return response.status(statusCode).send({
180
+ statusCode,
181
+ error: error.type,
182
+ message: error.message,
183
+ ...(debug && { stack: error.stack }),
184
+ });
185
+ }
139
186
  next(error);
140
187
  }
141
188
  });
@@ -149,6 +196,22 @@ options) => {
149
196
  let querySchema = options.schemas?.query || undefined;
150
197
  let headersSchema = options.schemas?.headers || undefined;
151
198
  let responseSchemas = options.responses;
199
+ if (options.errors) {
200
+ for (const errorType of Object.keys(options.errors)) {
201
+ const statusCode = options.errors[errorType];
202
+ responseSchemas = responseSchemas.concat([
203
+ {
204
+ status: statusCode,
205
+ description: errorType,
206
+ schema: zod_1.z.object({
207
+ statusCode: zod_1.z.literal(statusCode),
208
+ error: zod_1.z.literal(errorType),
209
+ message: zod_1.z.string(),
210
+ }),
211
+ },
212
+ ]);
213
+ }
214
+ }
152
215
  for (const middleware of options.middlewares || []) {
153
216
  if (middleware.options.schemas?.body) {
154
217
  bodySchema = bodySchema
@@ -173,6 +236,22 @@ options) => {
173
236
  if (middleware.options.responses) {
174
237
  responseSchemas = responseSchemas.concat(middleware.options.responses);
175
238
  }
239
+ if (middleware.options.errors) {
240
+ for (const errorType of Object.keys(middleware.options.errors)) {
241
+ const statusCode = middleware.options.errors[errorType];
242
+ responseSchemas = responseSchemas.concat([
243
+ {
244
+ status: statusCode,
245
+ description: errorType,
246
+ schema: zod_1.z.object({
247
+ statusCode: zod_1.z.literal(statusCode),
248
+ error: zod_1.z.literal(errorType),
249
+ message: zod_1.z.string(),
250
+ }),
251
+ },
252
+ ]);
253
+ }
254
+ }
176
255
  }
177
256
  const responseMap = buildResponseConfig(responseSchemas);
178
257
  registry.registerPath({
@@ -16,4 +16,5 @@ export type EndpointOptions<TResponseSchemas extends ReadonlyArray<EndpointRespo
16
16
  };
17
17
  middlewares?: TMiddlewares;
18
18
  responses: TResponseSchemas;
19
+ errors?: Record<string, number>;
19
20
  };
@@ -24,6 +24,7 @@ export type MiddlewareOptions<TResponseSchemas extends ReadonlyArray<EndpointRes
24
24
  context?: z.ZodSchema<TContext>;
25
25
  };
26
26
  responses: TResponseSchemas;
27
+ errors?: Record<string, number>;
27
28
  };
28
29
  export type Middleware<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TContext = undefined> = {
29
30
  options: MiddlewareOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TContext>;
@@ -0,0 +1 @@
1
+ export * from './money';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./money"), exports);
@@ -0,0 +1,29 @@
1
+ interface Money {
2
+ getAmount(): number;
3
+ add(addend?: Money): Money;
4
+ subtract(subtrahend?: Money): Money;
5
+ multiply(multiplier: number): Money;
6
+ divide(divisor: number): Money;
7
+ equalsTo(comparator: Money): boolean;
8
+ lessThan(comparator: Money): boolean;
9
+ lessThanOrEqual(comparator: Money): boolean;
10
+ greaterThan(comparator: Money): boolean;
11
+ greaterThanOrEqual(comparator: Money): boolean;
12
+ isZero(): boolean;
13
+ isPositive(): boolean;
14
+ isNegative(): boolean;
15
+ }
16
+ declare class Money implements Money {
17
+ private readonly value;
18
+ static fromAmount(value: number): Money;
19
+ static fromFloat(value: number): Money;
20
+ static zero(): Money;
21
+ private constructor();
22
+ clone(): Money;
23
+ getEuroAmount(): number;
24
+ getEuroString(): string;
25
+ percentage(percentage: number): Money;
26
+ min(comparator: Money): Money;
27
+ max(comparator: Money): Money;
28
+ }
29
+ export { Money };
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Money = void 0;
4
+ const CENTS_PER_EURO = 100;
5
+ const EPSILON = 0.00000000001;
6
+ const roundHalfUp = (value) => {
7
+ const rounded = Math.sign(value) * Math.round(Math.abs(value) + EPSILON);
8
+ return rounded === 0 ? 0 : rounded;
9
+ };
10
+ const euroToAmount = (amountInEuros) => roundHalfUp(amountInEuros * CENTS_PER_EURO);
11
+ const amountToEuro = (value) => value / CENTS_PER_EURO;
12
+ class Money {
13
+ static fromAmount(value) {
14
+ if (!Number.isInteger(value)) {
15
+ throw new TypeError('Integer required.');
16
+ }
17
+ return new Money(value);
18
+ }
19
+ static fromFloat(value) {
20
+ return new Money(euroToAmount(value));
21
+ }
22
+ static zero() {
23
+ return Money.fromAmount(0);
24
+ }
25
+ constructor(amount) {
26
+ this.value = amount;
27
+ }
28
+ clone() {
29
+ return Money.fromAmount(this.value);
30
+ }
31
+ getAmount() {
32
+ return this.value;
33
+ }
34
+ getEuroAmount() {
35
+ return amountToEuro(this.value);
36
+ }
37
+ getEuroString() {
38
+ return this.getEuroAmount().toFixed(2);
39
+ }
40
+ add(addend) {
41
+ return Money.fromAmount(this.value + (addend?.value || 0));
42
+ }
43
+ subtract(subtrahend) {
44
+ return Money.fromAmount(this.value - (subtrahend?.value || 0));
45
+ }
46
+ multiply(multiplier) {
47
+ return Money.fromAmount(roundHalfUp(this.value * multiplier));
48
+ }
49
+ divide(divisor) {
50
+ return Money.fromAmount(roundHalfUp(this.value / divisor));
51
+ }
52
+ percentage(percentage) {
53
+ if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
54
+ throw new TypeError('Expected number between 0 and 100');
55
+ }
56
+ return Money.fromAmount(roundHalfUp((this.value / 100) * percentage));
57
+ }
58
+ equalsTo(comparator) {
59
+ return this.value === comparator.value;
60
+ }
61
+ lessThan(comparator) {
62
+ return this.value < comparator.value;
63
+ }
64
+ lessThanOrEqual(comparator) {
65
+ return this.value <= comparator.value;
66
+ }
67
+ greaterThan(comparator) {
68
+ return this.value > comparator.value;
69
+ }
70
+ greaterThanOrEqual(comparator) {
71
+ return this.value >= comparator.value;
72
+ }
73
+ isZero() {
74
+ return this.value === 0;
75
+ }
76
+ isPositive() {
77
+ return this.value > 0;
78
+ }
79
+ isNegative() {
80
+ return this.value < 0;
81
+ }
82
+ min(comparator) {
83
+ return this.value < comparator.value ? this.clone() : comparator.clone();
84
+ }
85
+ max(comparator) {
86
+ return this.value > comparator.value ? this.clone() : comparator.clone();
87
+ }
88
+ }
89
+ exports.Money = Money;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "1.28.1",
3
+ "version": "1.30.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [