@shadow-library/fastify 0.0.5 → 0.0.7

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.
Files changed (46) hide show
  1. package/README.md +451 -14
  2. package/cjs/classes/default-error-handler.js +5 -4
  3. package/cjs/decorators/http-input.decorator.d.ts +2 -1
  4. package/cjs/decorators/http-input.decorator.js +4 -5
  5. package/cjs/decorators/index.d.ts +1 -0
  6. package/cjs/decorators/index.js +1 -0
  7. package/cjs/decorators/middleware.decorator.js +2 -2
  8. package/cjs/decorators/sensitive.decorator.d.ts +4 -0
  9. package/cjs/decorators/sensitive.decorator.js +15 -0
  10. package/cjs/interfaces/middleware.interface.d.ts +9 -6
  11. package/cjs/interfaces/route-handler.interface.d.ts +6 -2
  12. package/cjs/interfaces/server-metadata.interface.d.ts +5 -4
  13. package/cjs/module/error-response.dto.d.ts +1 -1
  14. package/cjs/module/error-response.dto.js +1 -1
  15. package/cjs/module/fastify-module.interface.d.ts +6 -1
  16. package/cjs/module/fastify-router.d.ts +27 -4
  17. package/cjs/module/fastify-router.js +90 -21
  18. package/cjs/module/fastify.module.js +25 -7
  19. package/cjs/module/fastify.utils.js +20 -7
  20. package/cjs/server.error.d.ts +2 -0
  21. package/cjs/server.error.js +9 -7
  22. package/cjs/services/context.service.d.ts +14 -6
  23. package/cjs/services/context.service.js +50 -11
  24. package/esm/classes/default-error-handler.js +6 -5
  25. package/esm/decorators/http-input.decorator.d.ts +2 -1
  26. package/esm/decorators/http-input.decorator.js +2 -3
  27. package/esm/decorators/index.d.ts +1 -0
  28. package/esm/decorators/index.js +1 -0
  29. package/esm/decorators/middleware.decorator.js +1 -1
  30. package/esm/decorators/sensitive.decorator.d.ts +4 -0
  31. package/esm/decorators/sensitive.decorator.js +12 -0
  32. package/esm/interfaces/middleware.interface.d.ts +9 -6
  33. package/esm/interfaces/route-handler.interface.d.ts +6 -2
  34. package/esm/interfaces/server-metadata.interface.d.ts +5 -4
  35. package/esm/module/error-response.dto.d.ts +1 -1
  36. package/esm/module/error-response.dto.js +1 -1
  37. package/esm/module/fastify-module.interface.d.ts +6 -1
  38. package/esm/module/fastify-router.d.ts +27 -4
  39. package/esm/module/fastify-router.js +89 -20
  40. package/esm/module/fastify.module.js +26 -8
  41. package/esm/module/fastify.utils.js +19 -6
  42. package/esm/server.error.d.ts +2 -0
  43. package/esm/server.error.js +9 -7
  44. package/esm/services/context.service.d.ts +14 -6
  45. package/esm/services/context.service.js +48 -9
  46. package/package.json +6 -4
@@ -1,4 +1,4 @@
1
- import assert from 'assert';
1
+ import assert from 'node:assert';
2
2
  import { Controller } from '@shadow-library/app';
3
3
  import { HTTP_CONTROLLER_TYPE } from '../constants.js';
4
4
  const propertyKeys = ['generate', 'use'];
@@ -0,0 +1,4 @@
1
+ import { MaskOptions } from '@shadow-library/common';
2
+ export type SensitiveDataType = 'secret' | 'email' | 'number' | 'words';
3
+ export declare function Sensitive(type?: SensitiveDataType): PropertyDecorator;
4
+ export declare function Sensitive(maskOptions: MaskOptions): PropertyDecorator;
@@ -0,0 +1,12 @@
1
+ import { FieldMetadata } from '@shadow-library/class-schema';
2
+ export function Sensitive(typeOrMaskOptions = 'secret') {
3
+ return (target, propertyKey) => {
4
+ const options = { sensitive: true };
5
+ if (typeof typeOrMaskOptions === 'string')
6
+ options.type = typeOrMaskOptions;
7
+ else
8
+ options.maskOptions = typeOrMaskOptions;
9
+ const decorator = FieldMetadata({ 'x-fastify': options });
10
+ decorator(target, propertyKey);
11
+ };
12
+ }
@@ -1,9 +1,12 @@
1
- import { RouteMetdata } from '@shadow-library/app';
2
- import { HttpRequest, HttpResponse } from './route-handler.interface.js';
3
- export type MiddlewareHandler = (request: HttpRequest, response: HttpResponse) => Promise<any>;
1
+ import { RouteMetadata } from '@shadow-library/app';
2
+ import { HttpCallback, HttpRequest, HttpResponse, RouteHandler } from './route-handler.interface.js';
4
3
  export interface MiddlewareGenerator {
5
- generate(metadata: RouteMetdata): MiddlewareHandler | undefined | Promise<MiddlewareHandler | undefined>;
4
+ cacheKey?: (metadata: RouteMetadata) => string;
5
+ generate(metadata: RouteMetadata): RouteHandler | undefined | Promise<RouteHandler | undefined>;
6
6
  }
7
- export interface HttpMiddleware {
8
- use(request: HttpRequest, response: HttpResponse): Promise<any>;
7
+ export interface AsyncHttpMiddleware {
8
+ use(request: HttpRequest, response: HttpResponse): Promise<unknown>;
9
+ }
10
+ export interface CallbackHttpMiddleware {
11
+ use(request: HttpRequest, response: HttpResponse, done: HttpCallback): void;
9
12
  }
@@ -1,4 +1,8 @@
1
- import { FastifyReply, FastifyRequest } from 'fastify';
1
+ import { SyncValue } from '@shadow-library/common';
2
+ import { DoneFuncWithErrOrRes, FastifyReply, FastifyRequest } from 'fastify';
2
3
  export type HttpRequest = FastifyRequest;
3
4
  export type HttpResponse = FastifyReply;
4
- export type RouteHandler = (req: HttpRequest, res: HttpResponse) => unknown | Promise<unknown>;
5
+ export type HttpCallback = DoneFuncWithErrOrRes;
6
+ export type CallbackRouteHandler = (request: HttpRequest, response: HttpResponse, done: HttpCallback) => SyncValue<unknown>;
7
+ export type AsyncRouteHandler = (request: HttpRequest, response: HttpResponse) => Promise<unknown>;
8
+ export type RouteHandler<T extends (...args: any[]) => any = any> = ReturnType<T> extends Promise<unknown> ? AsyncRouteHandler : CallbackRouteHandler;
@@ -1,24 +1,25 @@
1
- import { RouteMetdata } from '@shadow-library/app';
1
+ import { RouteMetadata } from '@shadow-library/app';
2
2
  import { JSONSchema } from '@shadow-library/class-schema';
3
3
  import { RouteShorthandOptions } from 'fastify';
4
4
  import { HTTP_CONTROLLER_TYPE } from '../constants.js';
5
5
  import { HttpMethod, RouteInputSchemas } from '../decorators/index.js';
6
6
  declare module '@shadow-library/app' {
7
- interface RouteMetdata extends Omit<RouteShorthandOptions, 'config'> {
7
+ interface RouteMetadata extends Omit<RouteShorthandOptions, 'config'> {
8
8
  method?: HttpMethod;
9
9
  path?: string;
10
10
  schemas?: RouteInputSchemas & {
11
11
  response?: Record<number | string, JSONSchema>;
12
12
  };
13
13
  rawBody?: boolean;
14
+ silentValidation?: boolean;
14
15
  status?: number;
15
16
  headers?: Record<string, string | (() => string)>;
16
17
  redirect?: string;
17
18
  render?: string | true;
18
19
  }
19
- interface ControllerMetdata {
20
+ interface ControllerMetadata {
20
21
  [HTTP_CONTROLLER_TYPE]?: 'router' | 'middleware';
21
22
  path?: string;
22
23
  }
23
24
  }
24
- export type ServerMetadata = RouteMetdata;
25
+ export type ServerMetadata = RouteMetadata;
@@ -6,5 +6,5 @@ export declare class ErrorResponseDto {
6
6
  code: string;
7
7
  type: string;
8
8
  message: string;
9
- fields: ErrorFieldDto[];
9
+ fields?: ErrorFieldDto[];
10
10
  }
@@ -43,7 +43,7 @@ __decorate([
43
43
  __metadata("design:type", String)
44
44
  ], ErrorResponseDto.prototype, "message", void 0);
45
45
  __decorate([
46
- Field(() => [ErrorFieldDto], { required: false }),
46
+ Field(() => [ErrorFieldDto], { optional: true }),
47
47
  __metadata("design:type", Array)
48
48
  ], ErrorResponseDto.prototype, "fields", void 0);
49
49
  ErrorResponseDto = __decorate([
@@ -8,12 +8,17 @@ export interface FastifyConfig extends FastifyServerOptions {
8
8
  port: number;
9
9
  errorHandler: ErrorHandler;
10
10
  responseSchema?: Record<string | number, JSONSchema>;
11
+ enableChildRoutes?: boolean;
12
+ maskSensitiveData?: boolean;
11
13
  }
12
14
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
13
15
  imports?: ModuleMetadata['imports'];
14
16
  fastifyFactory?: (instance: FastifyInstance) => Promisable<FastifyInstance>;
17
+ controllers?: ModuleMetadata['controllers'];
18
+ providers?: ModuleMetadata['providers'];
19
+ exports?: ModuleMetadata['exports'];
15
20
  }
16
- export interface FastifyModuleAsyncOptions extends Pick<FastifyModuleOptions, 'imports' | 'fastifyFactory'> {
21
+ export interface FastifyModuleAsyncOptions extends Pick<FastifyModuleOptions, 'imports' | 'controllers' | 'providers' | 'exports' | 'fastifyFactory'> {
17
22
  useFactory: (...args: any[]) => Promisable<FastifyConfig>;
18
23
  inject?: FactoryProvider['inject'];
19
24
  }
@@ -1,16 +1,18 @@
1
1
  import { ControllerRouteMetadata, Router } from '@shadow-library/app';
2
2
  import { type FastifyInstance } from 'fastify';
3
3
  import { Chain as MockRequestChain, InjectOptions as MockRequestOptions, Response as MockResponse } from 'light-my-request';
4
- import { JsonObject } from 'type-fest';
4
+ import { JsonObject, JsonValue } from 'type-fest';
5
+ import { HttpMethod } from '../decorators/index.js';
5
6
  import { HttpRequest, HttpResponse, ServerMetadata } from '../interfaces/index.js';
6
- import { Context } from '../services/index.js';
7
+ import { ContextService } from '../services/index.js';
7
8
  import { type FastifyConfig } from './fastify-module.interface.js';
8
9
  declare module 'fastify' {
9
10
  interface FastifyRequest {
10
11
  rawBody?: Buffer;
11
12
  }
12
13
  interface FastifyContextConfig {
13
- metadata: ServerMetadata;
14
+ metadata?: ServerMetadata;
15
+ artifacts?: RouteArtifacts;
14
16
  }
15
17
  }
16
18
  export interface RequestContext {
@@ -35,21 +37,42 @@ export interface RequestMetadata {
35
37
  service?: string;
36
38
  [key: string]: any;
37
39
  }
40
+ export interface ChildRouteRequest {
41
+ method: HttpMethod.GET;
42
+ url: string;
43
+ params: Record<string, string>;
44
+ query: Record<string, string>;
45
+ }
46
+ interface RouteArtifacts {
47
+ transforms: {
48
+ maskBody?(body: object): object;
49
+ maskQuery?(query: object): object;
50
+ maskParams?(params: object): object;
51
+ };
52
+ }
38
53
  export declare class FastifyRouter extends Router {
39
54
  private readonly config;
40
55
  private readonly instance;
41
56
  private readonly context;
57
+ static readonly name = "FastifyRouter";
42
58
  private readonly logger;
43
- constructor(config: FastifyConfig, instance: FastifyInstance, context: Context);
59
+ private readonly cachedDynamicMiddlewares;
60
+ private readonly childRouter;
61
+ private readonly sensitiveTransformer;
62
+ constructor(config: FastifyConfig, instance: FastifyInstance, context: ContextService);
44
63
  getInstance(): FastifyInstance;
45
64
  private registerRawBody;
65
+ private maskField;
46
66
  private getRequestLogger;
47
67
  private parseControllers;
48
68
  private getStatusCode;
49
69
  private generateRouteHandler;
70
+ private getMiddlewareHandler;
50
71
  register(controllers: ControllerRouteMetadata[]): Promise<void>;
51
72
  start(): Promise<void>;
52
73
  stop(): Promise<void>;
74
+ resolveChildRoute<T extends JsonValue = JsonObject>(url: string): Promise<T>;
53
75
  mockRequest(): MockRequestChain;
54
76
  mockRequest(options: MockRequestOptions): Promise<MockResponse>;
55
77
  }
78
+ export {};
@@ -10,24 +10,36 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
- import assert from 'assert';
13
+ import assert from 'node:assert';
14
14
  import { Inject, Injectable, Router } from '@shadow-library/app';
15
+ import { ClassSchema, TransformerFactory } from '@shadow-library/class-schema';
15
16
  import { InternalError, Logger, utils } from '@shadow-library/common';
16
17
  import merge from 'deepmerge';
18
+ import findMyWay from 'find-my-way';
19
+ import stringify from 'json-stable-stringify';
17
20
  import { FASTIFY_CONFIG, FASTIFY_INSTANCE, HTTP_CONTROLLER_INPUTS, HTTP_CONTROLLER_TYPE, NAMESPACE } from '../constants.js';
18
21
  import { HttpMethod } from '../decorators/index.js';
19
- import { Context } from '../services/index.js';
22
+ import { ContextService } from '../services/index.js';
20
23
  const httpMethods = Object.values(HttpMethod).filter(m => m !== HttpMethod.ALL);
24
+ const DEFAULT_ARTIFACTS = { transforms: {} };
21
25
  let FastifyRouter = class FastifyRouter extends Router {
22
26
  config;
23
27
  instance;
24
28
  context;
29
+ static name = 'FastifyRouter';
25
30
  logger = Logger.getLogger(NAMESPACE, 'FastifyRouter');
31
+ cachedDynamicMiddlewares = new Map();
32
+ childRouter = null;
33
+ sensitiveTransformer = new TransformerFactory(s => s['x-fastify']?.sensitive === true);
26
34
  constructor(config, instance, context) {
27
35
  super();
28
36
  this.config = config;
29
37
  this.instance = instance;
30
38
  this.context = context;
39
+ if (config.enableChildRoutes) {
40
+ const options = utils.object.pickKeys(config, ['ignoreTrailingSlash', 'ignoreDuplicateSlashes', 'allowUnsafeRegex', 'caseSensitive', 'maxParamLength', 'querystringParser']);
41
+ this.childRouter = findMyWay(options);
42
+ }
31
43
  }
32
44
  getInstance() {
33
45
  return this.instance;
@@ -37,21 +49,34 @@ let FastifyRouter = class FastifyRouter extends Router {
37
49
  const parser = this.instance.getDefaultJsonParser('error', 'error');
38
50
  this.instance.addContentTypeParser('application/json', opts, (req, body, done) => {
39
51
  const { metadata } = req.routeOptions.config;
40
- if (metadata.rawBody)
52
+ if (metadata?.rawBody)
41
53
  req.rawBody = body;
42
54
  return parser(req, body.toString(), done);
43
55
  });
44
56
  }
57
+ maskField(value, schema) {
58
+ const type = schema['x-fastify']?.type;
59
+ const stringified = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value);
60
+ if (type === 'email')
61
+ return utils.string.maskEmail(stringified);
62
+ if (type === 'number')
63
+ return utils.string.maskNumber(stringified);
64
+ if (type === 'words')
65
+ return utils.string.maskWords(stringified);
66
+ return '****';
67
+ }
45
68
  getRequestLogger() {
46
69
  return (req, res, done) => {
47
70
  const startTime = process.hrtime();
48
71
  res.raw.on('finish', () => {
49
72
  const isLoggingDisabled = this.context.get('DISABLE_REQUEST_LOGGING') ?? false;
50
73
  if (isLoggingDisabled)
51
- return done();
74
+ return;
75
+ const { url, config } = req.routeOptions;
76
+ const { transforms } = config.artifacts ?? DEFAULT_ARTIFACTS;
52
77
  const metadata = {};
53
78
  metadata.rid = this.context.getRID();
54
- metadata.url = req.url;
79
+ metadata.url = url ?? req.raw.url;
55
80
  metadata.method = req.method;
56
81
  metadata.status = res.statusCode;
57
82
  metadata.service = req.headers['x-service'];
@@ -60,13 +85,15 @@ let FastifyRouter = class FastifyRouter extends Router {
60
85
  metadata.resLen = res.getHeader('content-length');
61
86
  const resTime = process.hrtime(startTime);
62
87
  metadata.timeTaken = (resTime[0] * 1e3 + resTime[1] * 1e-6).toFixed(3);
63
- if (req.query)
64
- metadata.query = req.query;
65
88
  if (req.body)
66
- metadata.body = req.body;
67
- this.logger.http('http', metadata);
89
+ metadata.body = transforms.maskBody ? transforms.maskBody(structuredClone(req.body)) : req.body;
90
+ if (req.query)
91
+ metadata.query = transforms.maskQuery ? transforms.maskQuery(structuredClone(req.query)) : req.query;
92
+ if (req.params)
93
+ metadata.params = transforms.maskParams ? transforms.maskParams(structuredClone(req.params)) : req.params;
94
+ this.logger.http(`${req.method} ${metadata.url} -> ${res.statusCode} (${metadata.timeTaken}ms)`, metadata);
68
95
  });
69
- return done();
96
+ done();
70
97
  };
71
98
  }
72
99
  parseControllers(controllers) {
@@ -140,6 +167,18 @@ let FastifyRouter = class FastifyRouter extends Router {
140
167
  return response.send(data);
141
168
  };
142
169
  }
170
+ async getMiddlewareHandler(middleware, metadata) {
171
+ if (!middleware.metadata.generates)
172
+ return middleware.handler.bind(middleware.instance);
173
+ const genCacheKey = 'cacheKey' in middleware.instance && typeof middleware.instance.cacheKey === 'function' ? middleware.instance.cacheKey : stringify;
174
+ const cacheKey = genCacheKey(metadata);
175
+ const cachedMiddleware = this.cachedDynamicMiddlewares.get(cacheKey);
176
+ if (cachedMiddleware)
177
+ return cachedMiddleware;
178
+ const handler = await middleware.handler.apply(middleware.instance, [metadata]);
179
+ this.cachedDynamicMiddlewares.set(cacheKey, handler);
180
+ return handler;
181
+ }
143
182
  async register(controllers) {
144
183
  const { middlewares, routes } = this.parseControllers(controllers);
145
184
  const defaultResponseSchemas = this.config.responseSchema ?? {};
@@ -156,14 +195,15 @@ let FastifyRouter = class FastifyRouter extends Router {
156
195
  assert(metadata.method, 'Route method is required');
157
196
  this.logger.debug(`registering route ${metadata.method} ${metadata.path}`);
158
197
  const fastifyRouteOptions = utils.object.omitKeys(metadata, ['path', 'method', 'schemas', 'rawBody', 'status', 'headers', 'redirect', 'render']);
159
- const routeOptions = { ...fastifyRouteOptions, config: { metadata } };
198
+ const artifacts = { transforms: {} };
199
+ const routeOptions = { ...fastifyRouteOptions, config: { metadata, artifacts } };
160
200
  routeOptions.url = metadata.path;
161
201
  routeOptions.method = metadata.method === HttpMethod.ALL ? httpMethods : [metadata.method];
162
202
  routeOptions.handler = this.generateRouteHandler(route);
163
203
  for (const middleware of middlewares) {
164
204
  const name = middleware.metatype.name;
165
- const { generates, type } = middleware.metadata;
166
- const handler = generates ? await middleware.handler(metadata) : middleware.handler.bind(middleware);
205
+ const { type } = middleware.metadata;
206
+ const handler = await this.getMiddlewareHandler(middleware, metadata);
167
207
  if (typeof handler === 'function') {
168
208
  this.logger.debug(`applying '${type}' middleware '${name}'`);
169
209
  const middlewareHandler = routeOptions[type];
@@ -176,12 +216,35 @@ let FastifyRouter = class FastifyRouter extends Router {
176
216
  routeOptions.schema = {};
177
217
  routeOptions.attachValidation = metadata.silentValidation ?? false;
178
218
  routeOptions.schema.response = merge(metadata.schemas?.response ?? {}, defaultResponseSchemas);
179
- if (metadata.schemas?.body)
180
- routeOptions.schema.body = metadata.schemas.body;
181
- if (metadata.schemas?.params)
182
- routeOptions.schema.params = metadata.schemas.params;
183
- if (metadata.schemas?.query)
184
- routeOptions.schema.querystring = metadata.schemas.query;
219
+ const { body: bodySchema, params: paramsSchema, query: querySchema } = metadata.schemas ?? {};
220
+ const isMaskEnabled = this.config.maskSensitiveData ?? true;
221
+ if (bodySchema) {
222
+ const schema = typeof bodySchema === 'function' ? ClassSchema.generate(bodySchema) : bodySchema;
223
+ routeOptions.schema.body = schema;
224
+ if (ClassSchema.isBranded(schema) && isMaskEnabled) {
225
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
226
+ if (transformer)
227
+ artifacts.transforms.maskBody = obj => transformer(obj, this.maskField);
228
+ }
229
+ }
230
+ if (paramsSchema) {
231
+ const schema = typeof paramsSchema === 'function' ? ClassSchema.generate(paramsSchema) : paramsSchema;
232
+ routeOptions.schema.params = schema;
233
+ if (ClassSchema.isBranded(schema) && isMaskEnabled) {
234
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
235
+ if (transformer)
236
+ artifacts.transforms.maskParams = obj => transformer(obj, this.maskField);
237
+ }
238
+ }
239
+ if (querySchema) {
240
+ const schema = typeof querySchema === 'function' ? ClassSchema.generate(querySchema) : querySchema;
241
+ routeOptions.schema.querystring = schema;
242
+ if (ClassSchema.isBranded(schema) && isMaskEnabled) {
243
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
244
+ if (transformer)
245
+ artifacts.transforms.maskQuery = obj => transformer(obj, this.maskField);
246
+ }
247
+ }
185
248
  this.logger.debug('route options', { options: routeOptions });
186
249
  this.instance.route(routeOptions);
187
250
  this.logger.info(`registered route ${metadata.method} ${routeOptions.url}`);
@@ -197,6 +260,12 @@ let FastifyRouter = class FastifyRouter extends Router {
197
260
  await this.instance.close();
198
261
  this.logger.info('server stopped');
199
262
  }
263
+ async resolveChildRoute(url) {
264
+ if (!this.childRouter)
265
+ throw new InternalError('Child routes are not enabled');
266
+ const response = await this.instance.inject({ method: 'GET', url, headers: { 'x-service': 'internal-child-route' } });
267
+ return response.json();
268
+ }
200
269
  mockRequest(options) {
201
270
  return options ? this.instance.inject(options) : this.instance.inject();
202
271
  }
@@ -205,6 +274,6 @@ FastifyRouter = __decorate([
205
274
  Injectable(),
206
275
  __param(0, Inject(FASTIFY_CONFIG)),
207
276
  __param(1, Inject(FASTIFY_INSTANCE)),
208
- __metadata("design:paramtypes", [Object, Object, Context])
277
+ __metadata("design:paramtypes", [Object, Object, ContextService])
209
278
  ], FastifyRouter);
210
279
  export { FastifyRouter };
@@ -1,9 +1,10 @@
1
1
  import { Module, Router } from '@shadow-library/app';
2
2
  import { ClassSchema } from '@shadow-library/class-schema';
3
- import { utils } from '@shadow-library/common';
3
+ import { Config, utils } from '@shadow-library/common';
4
4
  import { v4 as uuid } from 'uuid';
5
5
  import { DefaultErrorHandler } from '../classes/index.js';
6
6
  import { FASTIFY_CONFIG, FASTIFY_INSTANCE } from '../constants.js';
7
+ import { ContextService } from '../services/index.js';
7
8
  import { ErrorResponseDto } from './error-response.dto.js';
8
9
  import { FastifyRouter } from './fastify-router.js';
9
10
  import { createFastifyInstance } from './fastify.utils.js';
@@ -15,23 +16,40 @@ export class FastifyModule {
15
16
  port: 8080,
16
17
  responseSchema: { '4xx': errorResponseSchema, '5xx': errorResponseSchema },
17
18
  errorHandler: new DefaultErrorHandler(),
18
- ignoreTrailingSlash: true,
19
- ignoreDuplicateSlashes: true,
19
+ maskSensitiveData: Config.isProd(),
20
20
  requestIdLogLabel: 'rid',
21
21
  genReqId: () => uuid(),
22
+ routerOptions: {
23
+ ignoreTrailingSlash: true,
24
+ ignoreDuplicateSlashes: true,
25
+ },
22
26
  };
23
27
  }
24
28
  static forRoot(options) {
25
- const config = Object.assign({}, this.getDefaultConfig(), utils.object.omitKeys(options, ['imports', 'fastifyFactory']));
26
- return this.forRootAsync({ imports: options.imports, useFactory: () => config, fastifyFactory: options.fastifyFactory });
29
+ const config = Object.assign({}, this.getDefaultConfig(), utils.object.omitKeys(options, ['imports', 'controllers', 'providers', 'exports', 'fastifyFactory']));
30
+ return this.forRootAsync({
31
+ imports: options.imports,
32
+ controllers: options.controllers,
33
+ providers: options.providers,
34
+ exports: options.exports,
35
+ useFactory: () => config,
36
+ fastifyFactory: options.fastifyFactory,
37
+ });
27
38
  }
28
39
  static forRootAsync(options) {
29
40
  const imports = options.imports ?? [];
30
- const providers = [{ token: Router, useClass: FastifyRouter }];
41
+ const controllers = options.controllers ?? [];
42
+ const exports = options.exports ?? [];
43
+ const providers = [{ token: Router, useClass: FastifyRouter }, ContextService];
44
+ if (options.providers)
45
+ providers.push(...options.providers);
31
46
  providers.push({ token: FASTIFY_CONFIG, useFactory: options.useFactory, inject: options.inject });
32
47
  const fastifyFactory = (config) => createFastifyInstance(config, options.fastifyFactory);
33
48
  providers.push({ token: FASTIFY_INSTANCE, useFactory: fastifyFactory, inject: [FASTIFY_CONFIG] });
34
- Module({ imports, providers, exports: [Router] })(FastifyModule);
35
- return FastifyModule;
49
+ const Class = class extends FastifyModule {
50
+ };
51
+ Object.defineProperty(Class, 'name', { value: FastifyModule.name });
52
+ Module({ imports, controllers, providers, exports: [Router, ContextService, ...exports] })(Class);
53
+ return Class;
36
54
  }
37
55
  }
@@ -1,18 +1,31 @@
1
- import assert from 'assert';
1
+ import assert from 'node:assert';
2
2
  import { ValidationError, throwError, utils } from '@shadow-library/common';
3
3
  import Ajv from 'ajv';
4
4
  import { fastify } from 'fastify';
5
5
  import { ServerError, ServerErrorCode } from '../server.error.js';
6
+ const keywords = ['x-fastify'];
6
7
  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 });
8
+ const strictValidator = new Ajv({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
9
+ const lenientValidator = new Ajv({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
9
10
  const notFoundError = new ServerError(ServerErrorCode.S002);
10
11
  export const notFoundHandler = () => throwError(notFoundError);
12
+ function compileSchema(ajv, schema) {
13
+ if (!schema.$id)
14
+ return ajv.compile(schema);
15
+ const schemas = [utils.object.omitKeys(schema, ['definitions']), ...Object.values(schema.definitions ?? {})];
16
+ for (const schema of schemas) {
17
+ if (schema.$id && !ajv.getSchema(schema.$id))
18
+ ajv.addSchema(schema, schema.$id);
19
+ }
20
+ return ajv.getSchema(schema.$id);
21
+ }
11
22
  export function compileValidator(routeSchema) {
12
23
  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);
24
+ if (routeSchema.httpPart === 'body')
25
+ return compileSchema(strictValidator, routeSchema.schema);
26
+ if (routeSchema.httpPart === 'params')
27
+ return compileSchema(lenientValidator, routeSchema.schema);
28
+ const validate = compileSchema(lenientValidator, routeSchema.schema);
16
29
  return (data) => {
17
30
  validate(data);
18
31
  for (const error of validate.errors ?? []) {
@@ -13,4 +13,6 @@ export declare class ServerErrorCode extends ErrorCode {
13
13
  static readonly S005: ServerErrorCode;
14
14
  static readonly S006: ServerErrorCode;
15
15
  static readonly S007: ServerErrorCode;
16
+ static readonly S008: ServerErrorCode;
17
+ static readonly S009: ServerErrorCode;
16
18
  }
@@ -23,11 +23,13 @@ export class ServerErrorCode extends ErrorCode {
23
23
  getStatusCode() {
24
24
  return this.statusCode;
25
25
  }
26
- static S001 = new ServerErrorCode('S001', ErrorType.SERVER_ERROR, 'Unexpected Server Error');
27
- static S002 = new ServerErrorCode('S002', ErrorType.NOT_FOUND, 'Not Found');
28
- static S003 = new ServerErrorCode('S003', ErrorType.VALIDATION_ERROR, 'Invalid Input');
29
- static S004 = new ServerErrorCode('S004', ErrorType.CLIENT_ERROR, 'Request body is too large', 413);
30
- static S005 = new ServerErrorCode('S005', ErrorType.CLIENT_ERROR, 'Request body size did not match Content-Length');
31
- static S006 = new ServerErrorCode('S006', ErrorType.CLIENT_ERROR, "Body cannot be empty when content-type is set to 'application/json'");
32
- static S007 = new ServerErrorCode('S007', ErrorType.CLIENT_ERROR, 'Received media type is not supported', 415);
26
+ static S001 = new ServerErrorCode('S001', ErrorType.SERVER_ERROR, 'An unexpected server error occurred while processing the request');
27
+ static S002 = new ServerErrorCode('S002', ErrorType.NOT_FOUND, 'The requested endpoint does not exist');
28
+ static S003 = new ServerErrorCode('S003', ErrorType.VALIDATION_ERROR, 'The provided input data is invalid or does not meet validation requirements');
29
+ static S004 = new ServerErrorCode('S004', ErrorType.UNAUTHENTICATED, 'Authentication credentials are required to access this resource');
30
+ static S005 = new ServerErrorCode('S005', ErrorType.UNAUTHORIZED, 'Access denied due to insufficient permissions to perform this operation');
31
+ static S006 = new ServerErrorCode('S006', ErrorType.CLIENT_ERROR, 'The request is malformed or contains invalid parameters');
32
+ static S007 = new ServerErrorCode('S007', ErrorType.CLIENT_ERROR, 'Rate limit exceeded due to too many requests sent in a given time frame', 429);
33
+ static S008 = new ServerErrorCode('S008', ErrorType.CONFLICT, 'Resource conflict as the requested operation conflicts with existing data');
34
+ static S009 = new ServerErrorCode('S009', ErrorType.NOT_FOUND, 'The requested resource could not be found');
33
35
  }
@@ -1,13 +1,21 @@
1
- import { MiddlewareHandler } from '../interfaces/index.js';
1
+ import { onRequestHookHandler } from 'fastify';
2
+ import { HttpRequest, HttpResponse } from '../interfaces/index.js';
2
3
  type Key = string | symbol;
3
- export declare class Context {
4
+ export declare class ContextService {
5
+ static readonly name = "ContextService";
4
6
  private readonly storage;
5
- init(): MiddlewareHandler;
7
+ init(): onRequestHookHandler;
6
8
  get<T>(key: Key, throwOnMissing: true): T;
7
- get<T>(key: Key, throwOnMissing?: false): T | null;
9
+ get<T>(key: Key, throwOnMissing?: boolean): T | null;
10
+ getFromParent<T>(key: Key, throwOnMissing: true): T;
11
+ getFromParent<T>(key: Key, throwOnMissing?: boolean): T | null;
12
+ resolve<T>(key: Key, throwOnMissing: true): T;
13
+ resolve<T>(key: Key, throwOnMissing?: boolean): T | null;
8
14
  set<T>(key: Key, value: T): this;
9
- getRequest(): Request;
10
- getResponse(): Response;
15
+ setInParent<T>(key: Key, value: T): this;
16
+ isChildContext(): boolean;
17
+ getRequest(): HttpRequest;
18
+ getResponse(): HttpResponse;
11
19
  getRID(): string;
12
20
  }
13
21
  export {};
@@ -4,21 +4,34 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AsyncLocalStorage } from 'async_hooks';
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
8
  import { Injectable } from '@shadow-library/app';
9
9
  import { InternalError } from '@shadow-library/common';
10
10
  const REQUEST = Symbol('request');
11
11
  const RESPONSE = Symbol('response');
12
12
  const RID = Symbol('rid');
13
- let Context = class Context {
13
+ const PARENT_CONTEXT = Symbol('parent-context');
14
+ const CHILD_RID_COUNTER = Symbol('child-rid-counter');
15
+ let ContextService = class ContextService {
16
+ static name = 'ContextService';
14
17
  storage = new AsyncLocalStorage();
15
18
  init() {
16
- return async (req, res) => {
19
+ return (req, res, done) => {
20
+ const parentStore = this.storage.getStore();
17
21
  const store = new Map();
22
+ if (parentStore) {
23
+ const isChildContext = parentStore.has(PARENT_CONTEXT);
24
+ if (isChildContext)
25
+ throw new InternalError('Cannot create a child context within an existing child context');
26
+ const childRIDCounter = (this.get(CHILD_RID_COUNTER) ?? 0) + 1;
27
+ this.set(CHILD_RID_COUNTER, childRIDCounter);
28
+ req.id = `${this.getRID()}-${childRIDCounter}`;
29
+ store.set(PARENT_CONTEXT, parentStore);
30
+ }
18
31
  store.set(REQUEST, req);
19
32
  store.set(RESPONSE, res);
20
33
  store.set(RID, req.id);
21
- this.storage.enterWith(store);
34
+ this.storage.run(store, done);
22
35
  };
23
36
  }
24
37
  get(key, throwOnMissing) {
@@ -26,11 +39,26 @@ let Context = class Context {
26
39
  if (!store)
27
40
  throw new InternalError('Context not yet initialized');
28
41
  const value = store.get(key);
29
- if (throwOnMissing && value === undefined) {
42
+ if (throwOnMissing && value === undefined)
30
43
  throw new InternalError(`Key '${key.toString()}' not found in the context`);
31
- }
32
44
  return value ?? null;
33
45
  }
46
+ getFromParent(key, throwOnMissing) {
47
+ if (!this.isChildContext())
48
+ return this.get(key, throwOnMissing);
49
+ const parentStore = this.get(PARENT_CONTEXT, true);
50
+ const value = parentStore.get(key);
51
+ if (throwOnMissing && value === undefined)
52
+ throw new InternalError(`Key '${key.toString()}' not found in the parent context`);
53
+ return value ?? null;
54
+ }
55
+ resolve(key, throwOnMissing) {
56
+ const isChild = this.isChildContext();
57
+ const value = this.get(key, !isChild);
58
+ if (value !== null || !isChild)
59
+ return value;
60
+ return this.getFromParent(key, throwOnMissing);
61
+ }
34
62
  set(key, value) {
35
63
  const store = this.storage.getStore();
36
64
  if (!store)
@@ -38,6 +66,17 @@ let Context = class Context {
38
66
  store.set(key, value);
39
67
  return this;
40
68
  }
69
+ setInParent(key, value) {
70
+ if (!this.isChildContext())
71
+ return this.set(key, value);
72
+ const parentStore = this.get(PARENT_CONTEXT, true);
73
+ parentStore.set(key, value);
74
+ return this;
75
+ }
76
+ isChildContext() {
77
+ const parentStore = this.get(PARENT_CONTEXT);
78
+ return parentStore !== null;
79
+ }
41
80
  getRequest() {
42
81
  return this.get(REQUEST, true);
43
82
  }
@@ -48,7 +87,7 @@ let Context = class Context {
48
87
  return this.get(RID, true);
49
88
  }
50
89
  };
51
- Context = __decorate([
90
+ ContextService = __decorate([
52
91
  Injectable()
53
- ], Context);
54
- export { Context };
92
+ ], ContextService);
93
+ export { ContextService };