@shadow-library/fastify 0.0.2 → 0.0.4

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.
@@ -3,6 +3,7 @@ import { type FastifyInstance } from 'fastify';
3
3
  import { Chain as MockRequestChain, InjectOptions as MockRequestOptions, Response as MockResponse } from 'light-my-request';
4
4
  import { JsonObject } from 'type-fest';
5
5
  import { HttpRequest, HttpResponse, ServerMetadata } from '../interfaces/index.js';
6
+ import { Context } from '../services/index.js';
6
7
  import { type FastifyConfig } from './fastify-module.interface.js';
7
8
  declare module 'fastify' {
8
9
  interface FastifyRequest {
@@ -19,18 +20,36 @@ export interface RequestContext {
19
20
  query: Record<string, string>;
20
21
  body: JsonObject;
21
22
  }
23
+ export interface RequestMetadata {
24
+ rid?: string;
25
+ srid?: string;
26
+ method?: string;
27
+ url?: string;
28
+ status?: number;
29
+ reqLen?: string;
30
+ reqIp?: string;
31
+ resLen?: string;
32
+ timeTaken?: string;
33
+ body?: any;
34
+ query?: object;
35
+ service?: string;
36
+ [key: string]: any;
37
+ }
22
38
  export declare class FastifyRouter extends Router {
23
39
  private readonly config;
24
40
  private readonly instance;
41
+ private readonly context;
25
42
  private readonly logger;
26
- constructor(config: FastifyConfig, instance: FastifyInstance);
43
+ constructor(config: FastifyConfig, instance: FastifyInstance, context: Context);
27
44
  getInstance(): FastifyInstance;
28
45
  private registerRawBody;
46
+ private getRequestLogger;
29
47
  private parseControllers;
30
48
  private getStatusCode;
31
49
  private generateRouteHandler;
32
50
  register(controllers: ControllerRouteMetadata[]): Promise<void>;
33
51
  start(): Promise<void>;
34
52
  stop(): Promise<void>;
35
- mockRequest(options?: MockRequestOptions): MockRequestChain | Promise<MockResponse>;
53
+ mockRequest(): MockRequestChain;
54
+ mockRequest(options: MockRequestOptions): Promise<MockResponse>;
36
55
  }
@@ -23,15 +23,18 @@ const common_1 = require("@shadow-library/common");
23
23
  const deepmerge_1 = __importDefault(require("deepmerge"));
24
24
  const constants_1 = require("../constants.js");
25
25
  const decorators_1 = require("../decorators/index.js");
26
+ const services_1 = require("../services/index.js");
26
27
  const httpMethods = Object.values(decorators_1.HttpMethod).filter(m => m !== decorators_1.HttpMethod.ALL);
27
28
  let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends app_1.Router {
28
29
  config;
29
30
  instance;
31
+ context;
30
32
  logger = common_1.Logger.getLogger(FastifyRouter_1.name);
31
- constructor(config, instance) {
33
+ constructor(config, instance, context) {
32
34
  super();
33
35
  this.config = config;
34
36
  this.instance = instance;
37
+ this.context = context;
35
38
  }
36
39
  getInstance() {
37
40
  return this.instance;
@@ -46,6 +49,33 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends app_1.Router {
46
49
  return parser(req, body.toString(), done);
47
50
  });
48
51
  }
52
+ getRequestLogger() {
53
+ return (req, res, done) => {
54
+ const startTime = process.hrtime();
55
+ res.raw.on('finish', () => {
56
+ const isLoggingDisabled = this.context.get('DISABLE_REQUEST_LOGGING') ?? false;
57
+ if (isLoggingDisabled)
58
+ return done();
59
+ const metadata = {};
60
+ metadata.rid = this.context.getRID();
61
+ metadata.url = req.url;
62
+ metadata.method = req.method;
63
+ metadata.status = res.statusCode;
64
+ metadata.service = req.headers['x-service'];
65
+ metadata.reqLen = req.headers['content-length'];
66
+ metadata.reqIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
67
+ metadata.resLen = res.getHeader('content-length');
68
+ const resTime = process.hrtime(startTime);
69
+ metadata.timeTaken = (resTime[0] * 1e3 + resTime[1] * 1e-6).toFixed(3);
70
+ if (req.query)
71
+ metadata.query = req.query;
72
+ if (req.body)
73
+ metadata.body = req.body;
74
+ this.logger.http('http', metadata);
75
+ });
76
+ return done();
77
+ };
78
+ }
49
79
  parseControllers(controllers) {
50
80
  const parsedControllers = { middlewares: [], routes: [] };
51
81
  for (const controller of controllers) {
@@ -123,6 +153,10 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends app_1.Router {
123
153
  const hasRawBody = routes.some(r => r.metadata.rawBody);
124
154
  if (hasRawBody)
125
155
  this.registerRawBody();
156
+ this.logger.debug('Registering the global middlewares');
157
+ this.instance.addHook('onRequest', this.context.init());
158
+ this.instance.addHook('onRequest', this.getRequestLogger());
159
+ this.logger.info('Registered global middlewares');
126
160
  for (const route of routes) {
127
161
  const metadata = route.metadata;
128
162
  (0, assert_1.default)(metadata.path, 'Route path is required');
@@ -166,8 +200,9 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends app_1.Router {
166
200
  this.logger.info(`server started at ${address}`);
167
201
  }
168
202
  async stop() {
169
- this.logger.info('stopping server');
203
+ this.logger.debug('stopping server');
170
204
  await this.instance.close();
205
+ this.logger.info('server stopped');
171
206
  }
172
207
  mockRequest(options) {
173
208
  return options ? this.instance.inject(options) : this.instance.inject();
@@ -178,5 +213,5 @@ exports.FastifyRouter = FastifyRouter = FastifyRouter_1 = __decorate([
178
213
  (0, app_1.Injectable)(),
179
214
  __param(0, (0, app_1.Inject)(constants_1.FASTIFY_CONFIG)),
180
215
  __param(1, (0, app_1.Inject)(constants_1.FASTIFY_INSTANCE)),
181
- __metadata("design:paramtypes", [Object, Object])
216
+ __metadata("design:paramtypes", [Object, Object, services_1.Context])
182
217
  ], FastifyRouter);
@@ -22,7 +22,6 @@ class FastifyModule {
22
22
  ignoreDuplicateSlashes: true,
23
23
  requestIdLogLabel: 'rid',
24
24
  genReqId: () => (0, uuid_1.v4)(),
25
- ajv: { customOptions: { removeAdditional: true, useDefaults: true, allErrors: true } },
26
25
  };
27
26
  }
28
27
  static forRoot(options) {
@@ -1,7 +1,9 @@
1
1
  import { ValidationError } from '@shadow-library/common';
2
+ import { SchemaObject } from 'ajv';
2
3
  import { FastifyInstance } from 'fastify';
3
- import { FastifySchemaValidationError, SchemaErrorDataVar } from 'fastify/types/schema';
4
+ import { FastifyRouteSchemaDef, FastifySchemaValidationError, FastifyValidationResult, SchemaErrorDataVar } from 'fastify/types/schema';
4
5
  import { FastifyConfig, FastifyModuleOptions } from './fastify-module.interface.js';
5
6
  export declare const notFoundHandler: () => never;
7
+ export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>): FastifyValidationResult;
6
8
  export declare function formatSchemaErrors(errors: FastifySchemaValidationError[], dataVar: SchemaErrorDataVar): ValidationError;
7
9
  export declare function createFastifyInstance(config: FastifyConfig, fastifyFactory?: FastifyModuleOptions['fastifyFactory']): Promise<FastifyInstance>;
@@ -1,14 +1,41 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.notFoundHandler = void 0;
7
+ exports.compileValidator = compileValidator;
4
8
  exports.formatSchemaErrors = formatSchemaErrors;
5
9
  exports.createFastifyInstance = createFastifyInstance;
10
+ const assert_1 = __importDefault(require("assert"));
6
11
  const common_1 = require("@shadow-library/common");
12
+ const ajv_1 = __importDefault(require("ajv"));
7
13
  const fastify_1 = require("fastify");
8
14
  const server_error_1 = require("../server.error.js");
15
+ const allowedHttpParts = ['body', 'params', 'querystring'];
16
+ const strictValidator = new ajv_1.default({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true });
17
+ const lenientValidator = new ajv_1.default({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true });
9
18
  const notFoundError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S002);
10
19
  const notFoundHandler = () => (0, common_1.throwError)(notFoundError);
11
20
  exports.notFoundHandler = notFoundHandler;
21
+ function compileValidator(routeSchema) {
22
+ (0, assert_1.default)(allowedHttpParts.includes(routeSchema.httpPart), `Invalid httpPart: ${routeSchema.httpPart}`);
23
+ if (routeSchema.httpPart !== 'querystring')
24
+ return strictValidator.compile(routeSchema.schema);
25
+ const validate = lenientValidator.compile(routeSchema.schema);
26
+ return (data) => {
27
+ validate(data);
28
+ for (const error of validate.errors ?? []) {
29
+ const path = error.instancePath.substring(1);
30
+ const defaultValue = routeSchema.schema.properties?.[path]?.default;
31
+ if (defaultValue !== undefined)
32
+ data[path] = defaultValue;
33
+ else
34
+ delete data[path];
35
+ }
36
+ return { value: data };
37
+ };
38
+ }
12
39
  function formatSchemaErrors(errors, dataVar) {
13
40
  const validationError = new common_1.ValidationError();
14
41
  for (const error of errors) {
@@ -29,5 +56,6 @@ async function createFastifyInstance(config, fastifyFactory) {
29
56
  instance.setSchemaErrorFormatter(formatSchemaErrors);
30
57
  instance.setNotFoundHandler(exports.notFoundHandler);
31
58
  instance.setErrorHandler(errorHandler.handle.bind(errorHandler));
59
+ instance.setValidatorCompiler(compileValidator);
32
60
  return fastifyFactory ? await fastifyFactory(instance) : instance;
33
61
  }
@@ -3,9 +3,9 @@ type Key = string | symbol;
3
3
  export declare class Context {
4
4
  private readonly storage;
5
5
  init(): MiddlewareHandler;
6
- protected get<T>(key: Key, throwOnMissing: true): T;
7
- protected get<T>(key: Key, throwOnMissing?: false): T | null;
8
- protected set<T>(key: Key, value: T): this;
6
+ get<T>(key: Key, throwOnMissing: true): T;
7
+ get<T>(key: Key, throwOnMissing?: false): T | null;
8
+ set<T>(key: Key, value: T): this;
9
9
  getRequest(): Request;
10
10
  getResponse(): Response;
11
11
  getRID(): string;
@@ -3,6 +3,7 @@ import { type FastifyInstance } from 'fastify';
3
3
  import { Chain as MockRequestChain, InjectOptions as MockRequestOptions, Response as MockResponse } from 'light-my-request';
4
4
  import { JsonObject } from 'type-fest';
5
5
  import { HttpRequest, HttpResponse, ServerMetadata } from '../interfaces/index.js';
6
+ import { Context } from '../services/index.js';
6
7
  import { type FastifyConfig } from './fastify-module.interface.js';
7
8
  declare module 'fastify' {
8
9
  interface FastifyRequest {
@@ -19,18 +20,36 @@ export interface RequestContext {
19
20
  query: Record<string, string>;
20
21
  body: JsonObject;
21
22
  }
23
+ export interface RequestMetadata {
24
+ rid?: string;
25
+ srid?: string;
26
+ method?: string;
27
+ url?: string;
28
+ status?: number;
29
+ reqLen?: string;
30
+ reqIp?: string;
31
+ resLen?: string;
32
+ timeTaken?: string;
33
+ body?: any;
34
+ query?: object;
35
+ service?: string;
36
+ [key: string]: any;
37
+ }
22
38
  export declare class FastifyRouter extends Router {
23
39
  private readonly config;
24
40
  private readonly instance;
41
+ private readonly context;
25
42
  private readonly logger;
26
- constructor(config: FastifyConfig, instance: FastifyInstance);
43
+ constructor(config: FastifyConfig, instance: FastifyInstance, context: Context);
27
44
  getInstance(): FastifyInstance;
28
45
  private registerRawBody;
46
+ private getRequestLogger;
29
47
  private parseControllers;
30
48
  private getStatusCode;
31
49
  private generateRouteHandler;
32
50
  register(controllers: ControllerRouteMetadata[]): Promise<void>;
33
51
  start(): Promise<void>;
34
52
  stop(): Promise<void>;
35
- mockRequest(options?: MockRequestOptions): MockRequestChain | Promise<MockResponse>;
53
+ mockRequest(): MockRequestChain;
54
+ mockRequest(options: MockRequestOptions): Promise<MockResponse>;
36
55
  }
@@ -17,15 +17,18 @@ import { InternalError, Logger, utils } from '@shadow-library/common';
17
17
  import merge from 'deepmerge';
18
18
  import { FASTIFY_CONFIG, FASTIFY_INSTANCE, HTTP_CONTROLLER_INPUTS, HTTP_CONTROLLER_TYPE } from '../constants.js';
19
19
  import { HttpMethod } from '../decorators/index.js';
20
+ import { Context } from '../services/index.js';
20
21
  const httpMethods = Object.values(HttpMethod).filter(m => m !== HttpMethod.ALL);
21
22
  let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends Router {
22
23
  config;
23
24
  instance;
25
+ context;
24
26
  logger = Logger.getLogger(FastifyRouter_1.name);
25
- constructor(config, instance) {
27
+ constructor(config, instance, context) {
26
28
  super();
27
29
  this.config = config;
28
30
  this.instance = instance;
31
+ this.context = context;
29
32
  }
30
33
  getInstance() {
31
34
  return this.instance;
@@ -40,6 +43,33 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends Router {
40
43
  return parser(req, body.toString(), done);
41
44
  });
42
45
  }
46
+ getRequestLogger() {
47
+ return (req, res, done) => {
48
+ const startTime = process.hrtime();
49
+ res.raw.on('finish', () => {
50
+ const isLoggingDisabled = this.context.get('DISABLE_REQUEST_LOGGING') ?? false;
51
+ if (isLoggingDisabled)
52
+ return done();
53
+ const metadata = {};
54
+ metadata.rid = this.context.getRID();
55
+ metadata.url = req.url;
56
+ metadata.method = req.method;
57
+ metadata.status = res.statusCode;
58
+ metadata.service = req.headers['x-service'];
59
+ metadata.reqLen = req.headers['content-length'];
60
+ metadata.reqIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
61
+ metadata.resLen = res.getHeader('content-length');
62
+ const resTime = process.hrtime(startTime);
63
+ metadata.timeTaken = (resTime[0] * 1e3 + resTime[1] * 1e-6).toFixed(3);
64
+ if (req.query)
65
+ metadata.query = req.query;
66
+ if (req.body)
67
+ metadata.body = req.body;
68
+ this.logger.http('http', metadata);
69
+ });
70
+ return done();
71
+ };
72
+ }
43
73
  parseControllers(controllers) {
44
74
  const parsedControllers = { middlewares: [], routes: [] };
45
75
  for (const controller of controllers) {
@@ -117,6 +147,10 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends Router {
117
147
  const hasRawBody = routes.some(r => r.metadata.rawBody);
118
148
  if (hasRawBody)
119
149
  this.registerRawBody();
150
+ this.logger.debug('Registering the global middlewares');
151
+ this.instance.addHook('onRequest', this.context.init());
152
+ this.instance.addHook('onRequest', this.getRequestLogger());
153
+ this.logger.info('Registered global middlewares');
120
154
  for (const route of routes) {
121
155
  const metadata = route.metadata;
122
156
  assert(metadata.path, 'Route path is required');
@@ -160,8 +194,9 @@ let FastifyRouter = FastifyRouter_1 = class FastifyRouter extends Router {
160
194
  this.logger.info(`server started at ${address}`);
161
195
  }
162
196
  async stop() {
163
- this.logger.info('stopping server');
197
+ this.logger.debug('stopping server');
164
198
  await this.instance.close();
199
+ this.logger.info('server stopped');
165
200
  }
166
201
  mockRequest(options) {
167
202
  return options ? this.instance.inject(options) : this.instance.inject();
@@ -171,6 +206,6 @@ FastifyRouter = FastifyRouter_1 = __decorate([
171
206
  Injectable(),
172
207
  __param(0, Inject(FASTIFY_CONFIG)),
173
208
  __param(1, Inject(FASTIFY_INSTANCE)),
174
- __metadata("design:paramtypes", [Object, Object])
209
+ __metadata("design:paramtypes", [Object, Object, Context])
175
210
  ], FastifyRouter);
176
211
  export { FastifyRouter };
@@ -19,7 +19,6 @@ export class FastifyModule {
19
19
  ignoreDuplicateSlashes: true,
20
20
  requestIdLogLabel: 'rid',
21
21
  genReqId: () => uuid(),
22
- ajv: { customOptions: { removeAdditional: true, useDefaults: true, allErrors: true } },
23
22
  };
24
23
  }
25
24
  static forRoot(options) {
@@ -1,7 +1,9 @@
1
1
  import { ValidationError } from '@shadow-library/common';
2
+ import { SchemaObject } from 'ajv';
2
3
  import { FastifyInstance } from 'fastify';
3
- import { FastifySchemaValidationError, SchemaErrorDataVar } from 'fastify/types/schema';
4
+ import { FastifyRouteSchemaDef, FastifySchemaValidationError, FastifyValidationResult, SchemaErrorDataVar } from 'fastify/types/schema';
4
5
  import { FastifyConfig, FastifyModuleOptions } from './fastify-module.interface.js';
5
6
  export declare const notFoundHandler: () => never;
7
+ export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>): FastifyValidationResult;
6
8
  export declare function formatSchemaErrors(errors: FastifySchemaValidationError[], dataVar: SchemaErrorDataVar): ValidationError;
7
9
  export declare function createFastifyInstance(config: FastifyConfig, fastifyFactory?: FastifyModuleOptions['fastifyFactory']): Promise<FastifyInstance>;
@@ -1,8 +1,31 @@
1
+ import assert from 'assert';
1
2
  import { ValidationError, throwError, utils } from '@shadow-library/common';
3
+ import Ajv from 'ajv';
2
4
  import { fastify } from 'fastify';
3
5
  import { ServerError, ServerErrorCode } from '../server.error.js';
6
+ const allowedHttpParts = ['body', 'params', 'querystring'];
7
+ const strictValidator = new Ajv({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true });
8
+ const lenientValidator = new Ajv({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true });
4
9
  const notFoundError = new ServerError(ServerErrorCode.S002);
5
10
  export const notFoundHandler = () => throwError(notFoundError);
11
+ export function compileValidator(routeSchema) {
12
+ assert(allowedHttpParts.includes(routeSchema.httpPart), `Invalid httpPart: ${routeSchema.httpPart}`);
13
+ if (routeSchema.httpPart !== 'querystring')
14
+ return strictValidator.compile(routeSchema.schema);
15
+ const validate = lenientValidator.compile(routeSchema.schema);
16
+ return (data) => {
17
+ validate(data);
18
+ for (const error of validate.errors ?? []) {
19
+ const path = error.instancePath.substring(1);
20
+ const defaultValue = routeSchema.schema.properties?.[path]?.default;
21
+ if (defaultValue !== undefined)
22
+ data[path] = defaultValue;
23
+ else
24
+ delete data[path];
25
+ }
26
+ return { value: data };
27
+ };
28
+ }
6
29
  export function formatSchemaErrors(errors, dataVar) {
7
30
  const validationError = new ValidationError();
8
31
  for (const error of errors) {
@@ -23,5 +46,6 @@ export async function createFastifyInstance(config, fastifyFactory) {
23
46
  instance.setSchemaErrorFormatter(formatSchemaErrors);
24
47
  instance.setNotFoundHandler(notFoundHandler);
25
48
  instance.setErrorHandler(errorHandler.handle.bind(errorHandler));
49
+ instance.setValidatorCompiler(compileValidator);
26
50
  return fastifyFactory ? await fastifyFactory(instance) : instance;
27
51
  }
@@ -3,9 +3,9 @@ type Key = string | symbol;
3
3
  export declare class Context {
4
4
  private readonly storage;
5
5
  init(): MiddlewareHandler;
6
- protected get<T>(key: Key, throwOnMissing: true): T;
7
- protected get<T>(key: Key, throwOnMissing?: false): T | null;
8
- protected set<T>(key: Key, value: T): this;
6
+ get<T>(key: Key, throwOnMissing: true): T;
7
+ get<T>(key: Key, throwOnMissing?: false): T | null;
8
+ set<T>(key: Key, value: T): this;
9
9
  getRequest(): Request;
10
10
  getResponse(): Response;
11
11
  getRID(): string;
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@shadow-library/fastify",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
+ "sideEffects": false,
5
6
  "description": "A Fastify wrapper featuring decorator-based routing, middleware and error handling",
6
7
  "repository": {
7
8
  "type": "git",
@@ -14,7 +15,9 @@
14
15
  },
15
16
  "homepage": "https://github.com/shadow-library/fastify#readme",
16
17
  "dependencies": {
18
+ "@fastify/middie": "^9.0.3",
17
19
  "@shadow-library/class-schema": "^0.0.5",
20
+ "ajv": "^8.17.1",
18
21
  "deepmerge": "^4.3.1",
19
22
  "fastify": "^5.2.1",
20
23
  "jest": "^29.7.0",