snap-on-openapi 1.0.11 → 1.0.14

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.
package/dist/OpenApi.d.ts CHANGED
@@ -51,10 +51,10 @@ export declare class OpenApi<TRouteTypes extends string, TErrorCodes extends str
51
51
  body: unknown;
52
52
  headers: Record<string, string>;
53
53
  }>;
54
- protected handleError(e: unknown, req: Request): {
54
+ protected handleError(e: unknown, req: Request): Promise<{
55
55
  status: number;
56
56
  body: z.TypeOf<import("./index.js").OpenApiErrorConfigMap<TErrorCodes>[TErrorCodes]["responseValidator"]>;
57
57
  headers: {};
58
- };
58
+ }>;
59
59
  protected static getBuilder(): ConfigBuilder<SampleRouteType, ErrorCode, DefaultErrorMap, DefaultRouteParamsMap, DefaultRouteContextMap, DefaultRouteMap, DefaultConfig>;
60
60
  }
package/dist/OpenApi.js CHANGED
@@ -129,6 +129,9 @@ export class OpenApi {
129
129
  }
130
130
  async processRootRoute(originalReq) {
131
131
  try {
132
+ if (this.config.onRequest) {
133
+ await this.config.onRequest({ request: originalReq, logger: this.logger });
134
+ }
132
135
  const url = new URL(originalReq.url);
133
136
  const basePath = this.getBasePath() === '/' ? '' : this.getBasePath();
134
137
  const urlPath = url.pathname.replace(basePath, '');
@@ -176,12 +179,19 @@ export class OpenApi {
176
179
  query: reqQuery,
177
180
  body: body,
178
181
  };
179
- this.logger.info(`Calling route ${route.path}`);
180
- this.logger.info(`${req.method}: ${req.path}`, {
181
- path: req.params,
182
- query: req.query,
183
- body: req.body,
184
- });
182
+ const onRoute = {
183
+ request: originalReq,
184
+ logger: this.logger,
185
+ path: urlPath,
186
+ method: originalReq.method,
187
+ params: pathParams,
188
+ query: reqQuery,
189
+ body: body,
190
+ route: route,
191
+ };
192
+ if (this.config.onRoute) {
193
+ await this.config.onRoute(onRoute);
194
+ }
185
195
  const queryValidator = route.validators.query?.strict() ?? z.object({});
186
196
  const query = queryValidator.safeParse(req.query);
187
197
  if (!query.success) {
@@ -192,80 +202,82 @@ export class OpenApi {
192
202
  if (!path.success) {
193
203
  throw new ValidationError(path.error, ValidationLocation.Path, req.params);
194
204
  }
195
- let response;
196
205
  const containsBody = route.method !== Method.GET;
206
+ let bodyData = {};
197
207
  if (containsBody && route.validators.body) {
198
- const body = route.validators.body.safeParse(req.body);
199
- if (!body.success) {
200
- throw new ValidationError(body.error, ValidationLocation.Body, req.body);
208
+ const bodyResult = route.validators.body.safeParse(req.body);
209
+ if (!bodyResult.success) {
210
+ throw new ValidationError(bodyResult.error, ValidationLocation.Body, req.body);
201
211
  }
202
- const context = await this.config.routes[route.type].contextFactory({
203
- route: route,
204
- request: originalReq,
205
- params: {
206
- query: query.data,
207
- path: path.data,
208
- body: body.data,
209
- },
210
- });
211
- response = await route.handler({
212
- ...context,
213
- params: {
214
- query: query.data,
215
- path: path.data,
216
- body: body.data,
217
- },
218
- });
219
- }
220
- else {
221
- const context = await this.config.routes[route.type].contextFactory({
222
- route: route,
223
- request: originalReq,
224
- params: {
225
- query: query.data,
226
- path: path.data,
227
- body: {},
228
- },
229
- });
230
- response = await route.handler({
231
- ...context,
232
- params: {
233
- query: query.data,
234
- path: path.data,
235
- body: {},
236
- }
237
- });
212
+ bodyData = bodyResult.data;
238
213
  }
214
+ const context = await this.config.routes[route.type].contextFactory({
215
+ route: route,
216
+ request: originalReq,
217
+ logger: this.logger,
218
+ params: {
219
+ query: query.data,
220
+ path: path.data,
221
+ body: bodyData,
222
+ },
223
+ });
224
+ const onHandler = {
225
+ ...onRoute,
226
+ validated: {
227
+ query: query.data,
228
+ path: path.data,
229
+ body: bodyData,
230
+ },
231
+ };
232
+ const response = await route.handler({
233
+ ...context,
234
+ params: {
235
+ query: query.data,
236
+ path: path.data,
237
+ body: bodyData,
238
+ },
239
+ });
239
240
  const finalResponse = route.validators.responseHeaders ? response : { body: response, headers: {} };
240
241
  const finalResponseValidator = z.object({
241
242
  body: route.validators.response ?? z.undefined(),
242
243
  headers: route.validators.responseHeaders?.strict() ?? z.object({}),
243
244
  });
245
+ const onResponse = {
246
+ ...onHandler,
247
+ response: { status: 200, body: finalResponse.body, headers: finalResponse.headers },
248
+ };
244
249
  if (this.config.disableResponseValidation) {
245
- this.logger.info('Response: 200', finalResponse);
250
+ if (this.config.onResponse) {
251
+ await this.config.onResponse(onResponse);
252
+ }
246
253
  return { status: 200, body: finalResponse.body, headers: finalResponse.headers };
247
254
  }
248
255
  const validated = finalResponseValidator.safeParse(finalResponse);
249
256
  if (!validated.success) {
250
257
  throw new ValidationError(validated.error, ValidationLocation.Response, finalResponse);
251
258
  }
252
- this.logger.info('Response: 200', validated.data);
259
+ if (this.config.onResponse) {
260
+ await this.config.onResponse(onResponse);
261
+ }
253
262
  return { status: 200, body: validated.data.body, headers: validated.data.headers };
254
263
  }
255
264
  catch (e) {
256
- return this.handleError(e, originalReq);
265
+ return await this.handleError(e, originalReq);
257
266
  }
258
267
  }
259
- handleError(e, req) {
260
- this.logger.error('Error during request openAPI route handling', e);
268
+ async handleError(e, req) {
261
269
  try {
262
- const response = this.config.handleError ? this.config.handleError(e, { request: req, logger: this.logger }) : this.config.defaultError;
270
+ const event = {
271
+ request: req,
272
+ logger: this.logger,
273
+ error: e,
274
+ };
275
+ const response = this.config.onError ? await this.config.onError(event) : this.config.defaultError;
263
276
  const status = this.config.errors[response.code].status;
264
277
  const valid = this.config.errors[response.code].responseValidator.safeParse(response.body);
265
278
  if (!valid.success) {
266
279
  throw new Error("Error response haven't passed validation");
267
280
  }
268
- this.logger.info(`Response: '${status}'`, response.body);
269
281
  return { status: Number(status), body: response.body, headers: {} };
270
282
  }
271
283
  catch (e) {
@@ -2,6 +2,9 @@ import { ErrorCode } from '../../../enums/ErrorCode.js';
2
2
  import { SampleRouteType } from '../../../enums/SampleRouteType.js';
3
3
  import { Config } from '../../../types/config/Config.js';
4
4
  import { ErrorResponse } from '../../../types/config/ErrorResponse.js';
5
+ import { OnErrorEvent } from '../../../types/events/OnErrorEvent.js';
6
+ import { OnResponseEvent } from '../../../types/events/OnResponseEvent.js';
7
+ import { OnRouteEvent } from '../../../types/events/OnRouteEvent.js';
5
8
  import { RoutePath } from '../../../types/RoutePath.js';
6
9
  import { DefaultErrorMap } from './DefaultErrorMap.js';
7
10
  import { DefaultRouteContextMap } from './DefaultRouteContextMap.js';
@@ -17,6 +20,10 @@ export declare class DefaultConfig implements Config<SampleRouteType, ErrorCode,
17
20
  readonly error: ErrorCode.UnknownError;
18
21
  };
19
22
  };
20
- handleError?: (e: unknown) => ErrorResponse<ErrorCode, DefaultErrorMap>;
23
+ onRequest?: () => Promise<void>;
24
+ onRoute?: (e: OnRouteEvent) => Promise<void>;
25
+ onHandler?: () => Promise<void>;
26
+ onResponse?: (e: OnResponseEvent) => Promise<void>;
27
+ onError?: (e: OnErrorEvent) => Promise<ErrorResponse<ErrorCode, DefaultErrorMap>>;
21
28
  skipDescriptionsCheck?: boolean;
22
29
  }
@@ -14,9 +14,23 @@ export class DefaultConfig {
14
14
  error: ErrorCode.UnknownError,
15
15
  },
16
16
  };
17
- handleError = (e) => {
18
- if (e instanceof ValidationError) {
19
- const zodError = e.getZodError();
17
+ onRequest = () => Promise.resolve();
18
+ onRoute = async (e) => {
19
+ e.logger.info(`Calling route ${e.route.path}`);
20
+ e.logger.info(`${e.method}: ${e.request.url}`, {
21
+ path: e.path,
22
+ query: e.query,
23
+ body: e.body,
24
+ });
25
+ };
26
+ onHandler = () => Promise.resolve();
27
+ onResponse = async (e) => {
28
+ e.logger.info(`Response: ${e.response.status}`, { body: e.response.body, headers: e.response.headers });
29
+ };
30
+ onError = async (e) => {
31
+ e.logger.error('Error during request openAPI route handling', { url: e.request.url, error: e.error });
32
+ if (e.error instanceof ValidationError) {
33
+ const zodError = e.error.getZodError();
20
34
  const map = [];
21
35
  for (const issue of zodError.issues) {
22
36
  map.push({
@@ -24,18 +38,18 @@ export class DefaultConfig {
24
38
  message: issue.message,
25
39
  });
26
40
  }
27
- if (e.getLocation() !== ValidationLocation.Response) {
41
+ if (e.error.getLocation() !== ValidationLocation.Response) {
28
42
  const response = {
29
43
  error: {
30
44
  code: ErrorCode.ValidationFailed,
31
- location: e.getLocation(),
45
+ location: e.error.getLocation(),
32
46
  fieldErrors: map,
33
47
  },
34
48
  };
35
49
  return { code: ErrorCode.ValidationFailed, body: response };
36
50
  }
37
51
  }
38
- if (e instanceof BuiltInError && e.getCode() === ErrorCode.NotFound) {
52
+ if (e.error instanceof BuiltInError && e.error.getCode() === ErrorCode.NotFound) {
39
53
  return { code: ErrorCode.NotFound, body: { error: ErrorCode.NotFound } };
40
54
  }
41
55
  const unknownError = {
@@ -41,7 +41,12 @@ export class ExpressWrapper {
41
41
  for (const header of Object.entries(result.headers)) {
42
42
  res.header(header[0], header[1]);
43
43
  }
44
- res.json(result.body);
44
+ if (result.body instanceof Buffer) {
45
+ res.send(result.body);
46
+ }
47
+ else {
48
+ res.json(result.body);
49
+ }
45
50
  };
46
51
  const regex = new RegExp(`${route}.*`);
47
52
  expressApp.get(regex, handler);
@@ -2,5 +2,5 @@ export interface ExpressResponse {
2
2
  header: (name: string, value: string) => ExpressResponse;
3
3
  status: (code: number) => ExpressResponse;
4
4
  json: (data: unknown) => ExpressResponse;
5
- send: (body: string) => ExpressResponse;
5
+ send: (body: string | Buffer) => ExpressResponse;
6
6
  }
@@ -51,7 +51,8 @@ export class TanstackStartWrapper {
51
51
  getOpenApiRootMethods() {
52
52
  const processor = async (ctx) => {
53
53
  const response = await this.service.processRootRoute(ctx.request);
54
- const res = new Response(JSON.stringify(response.body), {
54
+ const body = response.body instanceof Buffer ? response.body : JSON.stringify(response.body);
55
+ const res = new Response(body, {
55
56
  status: response.status,
56
57
  headers: response.headers,
57
58
  });
@@ -2,6 +2,7 @@ import { OpenApi } from '../../OpenApi.js';
2
2
  import { Method } from '../../enums/Methods.js';
3
3
  import { SampleRouteType } from '../../enums/SampleRouteType.js';
4
4
  import { RoutePath } from '../../types/RoutePath.js';
5
+ import { TestLogger } from './utils/TestLogger.js';
5
6
  export declare class TestUtils {
6
7
  static createRequest(route: RoutePath, method?: Method, body?: object): Request;
7
8
  static sendRequest(api: OpenApi<any, any, any>, route: RoutePath, method?: Method, body?: object): Promise<{
@@ -10,4 +11,5 @@ export declare class TestUtils {
10
11
  }>;
11
12
  static createOpenApi(): OpenApi<SampleRouteType, import("../../index.js").OpenApiErrorCode, import("../../index.js").OpenApiDefaultConfig>;
12
13
  static awaitGeneric<T>(timeoutMs: number, intervalMs: number, callback: () => Promise<T | null>): Promise<T | null>;
14
+ static getTestLogger(): TestLogger;
13
15
  }
@@ -2,6 +2,7 @@ import z from 'zod';
2
2
  import { OpenApi } from '../../OpenApi.js';
3
3
  import { Method } from '../../enums/Methods.js';
4
4
  import { SampleRouteType } from '../../enums/SampleRouteType.js';
5
+ import { TestLogger } from './utils/TestLogger.js';
5
6
  export class TestUtils {
6
7
  static createRequest(route, method = Method.GET, body) {
7
8
  const request = new Request(`http://localhost${route}`, {
@@ -45,4 +46,7 @@ export class TestUtils {
45
46
  } while (now < deadline);
46
47
  return null;
47
48
  }
49
+ static getTestLogger() {
50
+ return new TestLogger('TestUtils');
51
+ }
48
52
  }
@@ -0,0 +1,15 @@
1
+ import { Logger } from '../../Logger/Logger.js';
2
+ export declare class TestLogger extends Logger {
3
+ private messages;
4
+ popMessage(): {
5
+ message: string;
6
+ level: string;
7
+ data?: object;
8
+ } | undefined;
9
+ shiftMessage(): {
10
+ message: string;
11
+ level: string;
12
+ data?: object;
13
+ } | undefined;
14
+ protected log(message: string, level: string, data?: object): void;
15
+ }
@@ -0,0 +1,13 @@
1
+ import { Logger } from '../../Logger/Logger.js';
2
+ export class TestLogger extends Logger {
3
+ messages = [];
4
+ popMessage() {
5
+ return this.messages.pop();
6
+ }
7
+ shiftMessage() {
8
+ return this.messages.shift();
9
+ }
10
+ log(message, level, data) {
11
+ this.messages.push({ message, level, data });
12
+ }
13
+ }
@@ -16,4 +16,4 @@ export const stringDateTransformer = z.union([
16
16
  .transform((x) => {
17
17
  return new Date(Date.parse(x));
18
18
  }),
19
- ]).openapi({ type: 'string', format: 'date-time' });
19
+ ]).openapi({ type: 'string', format: 'date' });
@@ -9,6 +9,11 @@ import { RouteConfigMap } from './RouteConfigMap.js';
9
9
  import { RouteContextMap } from './RouteContextMap.js';
10
10
  import { RouteExtraPropsMap } from './RouteExtraPropsMap.js';
11
11
  import { Server } from './Server.js';
12
+ import { OnErrorEvent } from '../events/OnErrorEvent.js';
13
+ import { OnHandlerEvent } from '../events/OnHandlerEvent.js';
14
+ import { OnRequestEvent } from '../events/OnRequestEvent.js';
15
+ import { OnResponseEvent } from '../events/OnResponseEvent.js';
16
+ import { OnRouteEvent } from '../events/OnRouteEvent.js';
12
17
  export type Config<TRouteTypes extends string, TErrorCodes extends string, TErrorConfigMap extends ErrorConfigMap<TErrorCodes>, TRouteParamMap extends RouteExtraPropsMap<TRouteTypes>, TRouteContextMap extends RouteContextMap<TRouteTypes, TRouteParamMap>, TRouteConfigMap extends RouteConfigMap<TRouteTypes, TErrorCodes, TRouteParamMap, TRouteContextMap>> = {
13
18
  disableResponseValidation?: boolean;
14
19
  logger?: Logger;
@@ -28,10 +33,11 @@ export type Config<TRouteTypes extends string, TErrorCodes extends string, TErro
28
33
  apiVersion?: string;
29
34
  servers?: Server[];
30
35
  logLevel?: LogLevel;
31
- handleError?: (e: unknown, context: {
32
- request: Request;
33
- logger: Logger;
34
- }) => ErrorResponse<TErrorCodes, TErrorConfigMap>;
36
+ onRequest?: (e: OnRequestEvent) => Promise<void>;
37
+ onRoute?: (e: OnRouteEvent) => Promise<void>;
38
+ onHandler?: (e: OnHandlerEvent) => Promise<void>;
39
+ onResponse?: (e: OnResponseEvent) => Promise<void>;
40
+ onError?: (e: OnErrorEvent) => Promise<ErrorResponse<TErrorCodes, TErrorConfigMap>>;
35
41
  middleware?: <T extends TRouteTypes>(route: AnyRoute<T>, ctx: Awaited<ReturnType<TRouteContextMap[T]>>) => Promise<{
36
42
  body?: unknown;
37
43
  status?: number;
@@ -1,9 +1,11 @@
1
1
  import { ZodObject, ZodRawShape } from 'zod';
2
2
  import { AnyRoute } from '../AnyRoute.js';
3
3
  import { RouteExtraProps } from './RouteExtraProps.js';
4
+ import { Logger } from '../../services/Logger/Logger.js';
4
5
  export type ContextParams<TRouteType extends string, TExtraProps extends ZodObject<ZodRawShape> | undefined> = {
5
6
  route: AnyRoute<TRouteType> & RouteExtraProps<TExtraProps>;
6
7
  request: Request;
8
+ logger: Logger;
7
9
  params: {
8
10
  body: unknown;
9
11
  query: unknown;
@@ -0,0 +1,7 @@
1
+ import { Logger } from '../../services/Logger/Logger.js';
2
+ import { OnResponseEvent } from './OnResponseEvent.js';
3
+ export interface OnErrorEvent extends Partial<OnResponseEvent> {
4
+ request: Request;
5
+ logger: Logger;
6
+ error: unknown;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { OnRouteEvent } from './OnRouteEvent.js';
2
+ export interface OnHandlerEvent extends OnRouteEvent {
3
+ validated: {
4
+ query: Record<string, unknown>;
5
+ path: Record<string, unknown>;
6
+ body: unknown;
7
+ };
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Logger } from '../../services/Logger/Logger.js';
2
+ export interface OnRequestEvent {
3
+ request: Request;
4
+ logger: Logger;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { OnHandlerEvent } from './OnHandlerEvent.js';
2
+ export interface OnResponseEvent extends OnHandlerEvent {
3
+ response: {
4
+ status: number;
5
+ body: unknown;
6
+ headers: Record<string, string>;
7
+ };
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { AnyRoute } from '../AnyRoute.js';
2
+ import { OnRequestEvent } from './OnRequestEvent.js';
3
+ export interface OnRouteEvent extends OnRequestEvent {
4
+ path: string;
5
+ method: string;
6
+ params: Record<string, string>;
7
+ query: Record<string, string | string[]>;
8
+ body: unknown;
9
+ route: AnyRoute<string>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "snap-on-openapi",
3
3
  "author": "Alex Sarychev",
4
- "version": "1.0.11",
4
+ "version": "1.0.14",
5
5
  "description": "Swiftly build type-checked OpenAPI applications with Zod and TypeScript",
6
6
  "type": "module",
7
7
  "license": "ISC",