@lucaapp/service-utils 1.28.0 → 1.29.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);
@@ -47,11 +74,16 @@ const handleMiddleware = async (middleware, context, request, response) => new P
47
74
  const handlerRequestQuery = ((await middleware.options.schemas?.query?.parseAsync(request.query)) ||
48
75
  undefined);
49
76
  const handlerRequestHeaders = ((await middleware.options.schemas?.headers?.parseAsync(request.headers)) || undefined);
77
+ const { ip, baseUrl, route, method } = request;
50
78
  const handlerRequest = {
51
79
  body: handlerRequestBody,
52
80
  params: handlerRequestParams,
53
81
  query: handlerRequestQuery,
54
82
  headers: handlerRequestHeaders,
83
+ ip,
84
+ baseUrl,
85
+ method,
86
+ path: route.path,
55
87
  };
56
88
  await middleware.handler(handlerRequest, async (middlewareResponse) => {
57
89
  try {
@@ -83,10 +115,21 @@ const handleMiddleware = async (middleware, context, request, response) => new P
83
115
  });
84
116
  }
85
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
+ }
86
129
  reject(error);
87
130
  }
88
131
  });
89
- const mountEndpoint = (router, method, path, options, handler) => {
132
+ const mountEndpoint = (router, method, path, options, handler, debug) => {
90
133
  (0, assert_1.default)(options.responses.length > 0, 'You need to specify at least one response');
91
134
  router[method](path, async (request, response, next) => {
92
135
  try {
@@ -106,7 +149,7 @@ const mountEndpoint = (router, method, path, options, handler) => {
106
149
  const ctx = {};
107
150
  if (options.middlewares instanceof Array) {
108
151
  for (const middleware of options.middlewares) {
109
- const shouldContinue = await handleMiddleware(middleware, ctx, request, response);
152
+ const shouldContinue = await handleMiddleware(middleware, ctx, request, response, debug);
110
153
  if (!shouldContinue)
111
154
  return;
112
155
  }
@@ -129,6 +172,17 @@ const mountEndpoint = (router, method, path, options, handler) => {
129
172
  if (error instanceof zod_1.z.ZodError && !(0, boom_1.isBoom)(error)) {
130
173
  return response.status(400).send({ issues: error.issues });
131
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
+ }
132
186
  next(error);
133
187
  }
134
188
  });
@@ -142,6 +196,22 @@ options) => {
142
196
  let querySchema = options.schemas?.query || undefined;
143
197
  let headersSchema = options.schemas?.headers || undefined;
144
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
+ }
145
215
  for (const middleware of options.middlewares || []) {
146
216
  if (middleware.options.schemas?.body) {
147
217
  bodySchema = bodySchema
@@ -166,6 +236,22 @@ options) => {
166
236
  if (middleware.options.responses) {
167
237
  responseSchemas = responseSchemas.concat(middleware.options.responses);
168
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
+ }
169
255
  }
170
256
  const responseMap = buildResponseConfig(responseSchemas);
171
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
  };
@@ -10,6 +10,10 @@ export type MiddlewareHandler<TResponseSchema, TRequestBody = undefined, TReques
10
10
  params: TRequestParams;
11
11
  query: TRequestQuery;
12
12
  headers: TRequestHeaders;
13
+ ip: string;
14
+ baseUrl: string;
15
+ path: string;
16
+ method: string;
13
17
  }, send: (response: ExtractZodOutput<ArrayToUnion<TResponseSchema>>) => void, next: (context: TContext) => void) => Promise<void>;
14
18
  export type MiddlewareOptions<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TContext = undefined> = {
15
19
  schemas?: {
@@ -20,6 +24,7 @@ export type MiddlewareOptions<TResponseSchemas extends ReadonlyArray<EndpointRes
20
24
  context?: z.ZodSchema<TContext>;
21
25
  };
22
26
  responses: TResponseSchemas;
27
+ errors?: Record<string, number>;
23
28
  };
24
29
  export type Middleware<TResponseSchemas extends ReadonlyArray<EndpointResponseSchema>, TRequestBody = undefined, TRequestParams = undefined, TRequestQuery = undefined, TRequestHeaders = undefined, TContext = undefined> = {
25
30
  options: MiddlewareOptions<TResponseSchemas, TRequestBody, TRequestParams, TRequestQuery, TRequestHeaders, TContext>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [