@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
@@ -47,7 +47,7 @@ __decorate([
47
47
  __metadata("design:type", String)
48
48
  ], ErrorResponseDto.prototype, "message", void 0);
49
49
  __decorate([
50
- (0, class_schema_1.Field)(() => [ErrorFieldDto], { required: false }),
50
+ (0, class_schema_1.Field)(() => [ErrorFieldDto], { optional: true }),
51
51
  __metadata("design:type", Array)
52
52
  ], ErrorResponseDto.prototype, "fields", void 0);
53
53
  exports.ErrorResponseDto = 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 {};
@@ -16,24 +16,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.FastifyRouter = void 0;
19
- const assert_1 = __importDefault(require("assert"));
19
+ const node_assert_1 = __importDefault(require("node:assert"));
20
20
  const app_1 = require("@shadow-library/app");
21
+ const class_schema_1 = require("@shadow-library/class-schema");
21
22
  const common_1 = require("@shadow-library/common");
22
23
  const deepmerge_1 = __importDefault(require("deepmerge"));
24
+ const find_my_way_1 = __importDefault(require("find-my-way"));
25
+ const json_stable_stringify_1 = __importDefault(require("json-stable-stringify"));
23
26
  const constants_1 = require("../constants.js");
24
27
  const decorators_1 = require("../decorators/index.js");
25
28
  const services_1 = require("../services/index.js");
26
29
  const httpMethods = Object.values(decorators_1.HttpMethod).filter(m => m !== decorators_1.HttpMethod.ALL);
30
+ const DEFAULT_ARTIFACTS = { transforms: {} };
27
31
  let FastifyRouter = class FastifyRouter extends app_1.Router {
28
32
  config;
29
33
  instance;
30
34
  context;
35
+ static name = 'FastifyRouter';
31
36
  logger = common_1.Logger.getLogger(constants_1.NAMESPACE, 'FastifyRouter');
37
+ cachedDynamicMiddlewares = new Map();
38
+ childRouter = null;
39
+ sensitiveTransformer = new class_schema_1.TransformerFactory(s => s['x-fastify']?.sensitive === true);
32
40
  constructor(config, instance, context) {
33
41
  super();
34
42
  this.config = config;
35
43
  this.instance = instance;
36
44
  this.context = context;
45
+ if (config.enableChildRoutes) {
46
+ const options = common_1.utils.object.pickKeys(config, ['ignoreTrailingSlash', 'ignoreDuplicateSlashes', 'allowUnsafeRegex', 'caseSensitive', 'maxParamLength', 'querystringParser']);
47
+ this.childRouter = (0, find_my_way_1.default)(options);
48
+ }
37
49
  }
38
50
  getInstance() {
39
51
  return this.instance;
@@ -43,21 +55,34 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
43
55
  const parser = this.instance.getDefaultJsonParser('error', 'error');
44
56
  this.instance.addContentTypeParser('application/json', opts, (req, body, done) => {
45
57
  const { metadata } = req.routeOptions.config;
46
- if (metadata.rawBody)
58
+ if (metadata?.rawBody)
47
59
  req.rawBody = body;
48
60
  return parser(req, body.toString(), done);
49
61
  });
50
62
  }
63
+ maskField(value, schema) {
64
+ const type = schema['x-fastify']?.type;
65
+ const stringified = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value);
66
+ if (type === 'email')
67
+ return common_1.utils.string.maskEmail(stringified);
68
+ if (type === 'number')
69
+ return common_1.utils.string.maskNumber(stringified);
70
+ if (type === 'words')
71
+ return common_1.utils.string.maskWords(stringified);
72
+ return '****';
73
+ }
51
74
  getRequestLogger() {
52
75
  return (req, res, done) => {
53
76
  const startTime = process.hrtime();
54
77
  res.raw.on('finish', () => {
55
78
  const isLoggingDisabled = this.context.get('DISABLE_REQUEST_LOGGING') ?? false;
56
79
  if (isLoggingDisabled)
57
- return done();
80
+ return;
81
+ const { url, config } = req.routeOptions;
82
+ const { transforms } = config.artifacts ?? DEFAULT_ARTIFACTS;
58
83
  const metadata = {};
59
84
  metadata.rid = this.context.getRID();
60
- metadata.url = req.url;
85
+ metadata.url = url ?? req.raw.url;
61
86
  metadata.method = req.method;
62
87
  metadata.status = res.statusCode;
63
88
  metadata.service = req.headers['x-service'];
@@ -66,13 +91,15 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
66
91
  metadata.resLen = res.getHeader('content-length');
67
92
  const resTime = process.hrtime(startTime);
68
93
  metadata.timeTaken = (resTime[0] * 1e3 + resTime[1] * 1e-6).toFixed(3);
69
- if (req.query)
70
- metadata.query = req.query;
71
94
  if (req.body)
72
- metadata.body = req.body;
73
- this.logger.http('http', metadata);
95
+ metadata.body = transforms.maskBody ? transforms.maskBody(structuredClone(req.body)) : req.body;
96
+ if (req.query)
97
+ metadata.query = transforms.maskQuery ? transforms.maskQuery(structuredClone(req.query)) : req.query;
98
+ if (req.params)
99
+ metadata.params = transforms.maskParams ? transforms.maskParams(structuredClone(req.params)) : req.params;
100
+ this.logger.http(`${req.method} ${metadata.url} -> ${res.statusCode} (${metadata.timeTaken}ms)`, metadata);
74
101
  });
75
- return done();
102
+ done();
76
103
  };
77
104
  }
78
105
  parseControllers(controllers) {
@@ -146,6 +173,18 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
146
173
  return response.send(data);
147
174
  };
148
175
  }
176
+ async getMiddlewareHandler(middleware, metadata) {
177
+ if (!middleware.metadata.generates)
178
+ return middleware.handler.bind(middleware.instance);
179
+ const genCacheKey = 'cacheKey' in middleware.instance && typeof middleware.instance.cacheKey === 'function' ? middleware.instance.cacheKey : json_stable_stringify_1.default;
180
+ const cacheKey = genCacheKey(metadata);
181
+ const cachedMiddleware = this.cachedDynamicMiddlewares.get(cacheKey);
182
+ if (cachedMiddleware)
183
+ return cachedMiddleware;
184
+ const handler = await middleware.handler.apply(middleware.instance, [metadata]);
185
+ this.cachedDynamicMiddlewares.set(cacheKey, handler);
186
+ return handler;
187
+ }
149
188
  async register(controllers) {
150
189
  const { middlewares, routes } = this.parseControllers(controllers);
151
190
  const defaultResponseSchemas = this.config.responseSchema ?? {};
@@ -158,18 +197,19 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
158
197
  this.logger.info('Registered global middlewares');
159
198
  for (const route of routes) {
160
199
  const metadata = route.metadata;
161
- (0, assert_1.default)(metadata.path, 'Route path is required');
162
- (0, assert_1.default)(metadata.method, 'Route method is required');
200
+ (0, node_assert_1.default)(metadata.path, 'Route path is required');
201
+ (0, node_assert_1.default)(metadata.method, 'Route method is required');
163
202
  this.logger.debug(`registering route ${metadata.method} ${metadata.path}`);
164
203
  const fastifyRouteOptions = common_1.utils.object.omitKeys(metadata, ['path', 'method', 'schemas', 'rawBody', 'status', 'headers', 'redirect', 'render']);
165
- const routeOptions = { ...fastifyRouteOptions, config: { metadata } };
204
+ const artifacts = { transforms: {} };
205
+ const routeOptions = { ...fastifyRouteOptions, config: { metadata, artifacts } };
166
206
  routeOptions.url = metadata.path;
167
207
  routeOptions.method = metadata.method === decorators_1.HttpMethod.ALL ? httpMethods : [metadata.method];
168
208
  routeOptions.handler = this.generateRouteHandler(route);
169
209
  for (const middleware of middlewares) {
170
210
  const name = middleware.metatype.name;
171
- const { generates, type } = middleware.metadata;
172
- const handler = generates ? await middleware.handler(metadata) : middleware.handler.bind(middleware);
211
+ const { type } = middleware.metadata;
212
+ const handler = await this.getMiddlewareHandler(middleware, metadata);
173
213
  if (typeof handler === 'function') {
174
214
  this.logger.debug(`applying '${type}' middleware '${name}'`);
175
215
  const middlewareHandler = routeOptions[type];
@@ -182,12 +222,35 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
182
222
  routeOptions.schema = {};
183
223
  routeOptions.attachValidation = metadata.silentValidation ?? false;
184
224
  routeOptions.schema.response = (0, deepmerge_1.default)(metadata.schemas?.response ?? {}, defaultResponseSchemas);
185
- if (metadata.schemas?.body)
186
- routeOptions.schema.body = metadata.schemas.body;
187
- if (metadata.schemas?.params)
188
- routeOptions.schema.params = metadata.schemas.params;
189
- if (metadata.schemas?.query)
190
- routeOptions.schema.querystring = metadata.schemas.query;
225
+ const { body: bodySchema, params: paramsSchema, query: querySchema } = metadata.schemas ?? {};
226
+ const isMaskEnabled = this.config.maskSensitiveData ?? true;
227
+ if (bodySchema) {
228
+ const schema = typeof bodySchema === 'function' ? class_schema_1.ClassSchema.generate(bodySchema) : bodySchema;
229
+ routeOptions.schema.body = schema;
230
+ if (class_schema_1.ClassSchema.isBranded(schema) && isMaskEnabled) {
231
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
232
+ if (transformer)
233
+ artifacts.transforms.maskBody = obj => transformer(obj, this.maskField);
234
+ }
235
+ }
236
+ if (paramsSchema) {
237
+ const schema = typeof paramsSchema === 'function' ? class_schema_1.ClassSchema.generate(paramsSchema) : paramsSchema;
238
+ routeOptions.schema.params = schema;
239
+ if (class_schema_1.ClassSchema.isBranded(schema) && isMaskEnabled) {
240
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
241
+ if (transformer)
242
+ artifacts.transforms.maskParams = obj => transformer(obj, this.maskField);
243
+ }
244
+ }
245
+ if (querySchema) {
246
+ const schema = typeof querySchema === 'function' ? class_schema_1.ClassSchema.generate(querySchema) : querySchema;
247
+ routeOptions.schema.querystring = schema;
248
+ if (class_schema_1.ClassSchema.isBranded(schema) && isMaskEnabled) {
249
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
250
+ if (transformer)
251
+ artifacts.transforms.maskQuery = obj => transformer(obj, this.maskField);
252
+ }
253
+ }
191
254
  this.logger.debug('route options', { options: routeOptions });
192
255
  this.instance.route(routeOptions);
193
256
  this.logger.info(`registered route ${metadata.method} ${routeOptions.url}`);
@@ -203,6 +266,12 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
203
266
  await this.instance.close();
204
267
  this.logger.info('server stopped');
205
268
  }
269
+ async resolveChildRoute(url) {
270
+ if (!this.childRouter)
271
+ throw new common_1.InternalError('Child routes are not enabled');
272
+ const response = await this.instance.inject({ method: 'GET', url, headers: { 'x-service': 'internal-child-route' } });
273
+ return response.json();
274
+ }
206
275
  mockRequest(options) {
207
276
  return options ? this.instance.inject(options) : this.instance.inject();
208
277
  }
@@ -212,5 +281,5 @@ exports.FastifyRouter = FastifyRouter = __decorate([
212
281
  (0, app_1.Injectable)(),
213
282
  __param(0, (0, app_1.Inject)(constants_1.FASTIFY_CONFIG)),
214
283
  __param(1, (0, app_1.Inject)(constants_1.FASTIFY_INSTANCE)),
215
- __metadata("design:paramtypes", [Object, Object, services_1.Context])
284
+ __metadata("design:paramtypes", [Object, Object, services_1.ContextService])
216
285
  ], FastifyRouter);
@@ -7,6 +7,7 @@ const common_1 = require("@shadow-library/common");
7
7
  const uuid_1 = require("uuid");
8
8
  const classes_1 = require("../classes/index.js");
9
9
  const constants_1 = require("../constants.js");
10
+ const services_1 = require("../services/index.js");
10
11
  const error_response_dto_1 = require("./error-response.dto.js");
11
12
  const fastify_router_1 = require("./fastify-router.js");
12
13
  const fastify_utils_1 = require("./fastify.utils.js");
@@ -18,24 +19,41 @@ class FastifyModule {
18
19
  port: 8080,
19
20
  responseSchema: { '4xx': errorResponseSchema, '5xx': errorResponseSchema },
20
21
  errorHandler: new classes_1.DefaultErrorHandler(),
21
- ignoreTrailingSlash: true,
22
- ignoreDuplicateSlashes: true,
22
+ maskSensitiveData: common_1.Config.isProd(),
23
23
  requestIdLogLabel: 'rid',
24
24
  genReqId: () => (0, uuid_1.v4)(),
25
+ routerOptions: {
26
+ ignoreTrailingSlash: true,
27
+ ignoreDuplicateSlashes: true,
28
+ },
25
29
  };
26
30
  }
27
31
  static forRoot(options) {
28
- const config = Object.assign({}, this.getDefaultConfig(), common_1.utils.object.omitKeys(options, ['imports', 'fastifyFactory']));
29
- return this.forRootAsync({ imports: options.imports, useFactory: () => config, fastifyFactory: options.fastifyFactory });
32
+ const config = Object.assign({}, this.getDefaultConfig(), common_1.utils.object.omitKeys(options, ['imports', 'controllers', 'providers', 'exports', 'fastifyFactory']));
33
+ return this.forRootAsync({
34
+ imports: options.imports,
35
+ controllers: options.controllers,
36
+ providers: options.providers,
37
+ exports: options.exports,
38
+ useFactory: () => config,
39
+ fastifyFactory: options.fastifyFactory,
40
+ });
30
41
  }
31
42
  static forRootAsync(options) {
32
43
  const imports = options.imports ?? [];
33
- const providers = [{ token: app_1.Router, useClass: fastify_router_1.FastifyRouter }];
44
+ const controllers = options.controllers ?? [];
45
+ const exports = options.exports ?? [];
46
+ const providers = [{ token: app_1.Router, useClass: fastify_router_1.FastifyRouter }, services_1.ContextService];
47
+ if (options.providers)
48
+ providers.push(...options.providers);
34
49
  providers.push({ token: constants_1.FASTIFY_CONFIG, useFactory: options.useFactory, inject: options.inject });
35
50
  const fastifyFactory = (config) => (0, fastify_utils_1.createFastifyInstance)(config, options.fastifyFactory);
36
51
  providers.push({ token: constants_1.FASTIFY_INSTANCE, useFactory: fastifyFactory, inject: [constants_1.FASTIFY_CONFIG] });
37
- (0, app_1.Module)({ imports, providers, exports: [app_1.Router] })(FastifyModule);
38
- return FastifyModule;
52
+ const Class = class extends FastifyModule {
53
+ };
54
+ Object.defineProperty(Class, 'name', { value: FastifyModule.name });
55
+ (0, app_1.Module)({ imports, controllers, providers, exports: [app_1.Router, services_1.ContextService, ...exports] })(Class);
56
+ return Class;
39
57
  }
40
58
  }
41
59
  exports.FastifyModule = FastifyModule;
@@ -7,22 +7,35 @@ exports.notFoundHandler = void 0;
7
7
  exports.compileValidator = compileValidator;
8
8
  exports.formatSchemaErrors = formatSchemaErrors;
9
9
  exports.createFastifyInstance = createFastifyInstance;
10
- const assert_1 = __importDefault(require("assert"));
10
+ const node_assert_1 = __importDefault(require("node:assert"));
11
11
  const common_1 = require("@shadow-library/common");
12
12
  const ajv_1 = __importDefault(require("ajv"));
13
13
  const fastify_1 = require("fastify");
14
14
  const server_error_1 = require("../server.error.js");
15
+ const keywords = ['x-fastify'];
15
16
  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 });
17
+ const strictValidator = new ajv_1.default({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
18
+ const lenientValidator = new ajv_1.default({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
18
19
  const notFoundError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S002);
19
20
  const notFoundHandler = () => (0, common_1.throwError)(notFoundError);
20
21
  exports.notFoundHandler = notFoundHandler;
22
+ function compileSchema(ajv, schema) {
23
+ if (!schema.$id)
24
+ return ajv.compile(schema);
25
+ const schemas = [common_1.utils.object.omitKeys(schema, ['definitions']), ...Object.values(schema.definitions ?? {})];
26
+ for (const schema of schemas) {
27
+ if (schema.$id && !ajv.getSchema(schema.$id))
28
+ ajv.addSchema(schema, schema.$id);
29
+ }
30
+ return ajv.getSchema(schema.$id);
31
+ }
21
32
  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);
33
+ (0, node_assert_1.default)(allowedHttpParts.includes(routeSchema.httpPart), `Invalid httpPart: ${routeSchema.httpPart}`);
34
+ if (routeSchema.httpPart === 'body')
35
+ return compileSchema(strictValidator, routeSchema.schema);
36
+ if (routeSchema.httpPart === 'params')
37
+ return compileSchema(lenientValidator, routeSchema.schema);
38
+ const validate = compileSchema(lenientValidator, routeSchema.schema);
26
39
  return (data) => {
27
40
  validate(data);
28
41
  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
  }
@@ -27,12 +27,14 @@ class ServerErrorCode extends common_1.ErrorCode {
27
27
  getStatusCode() {
28
28
  return this.statusCode;
29
29
  }
30
- static S001 = new ServerErrorCode('S001', common_1.ErrorType.SERVER_ERROR, 'Unexpected Server Error');
31
- static S002 = new ServerErrorCode('S002', common_1.ErrorType.NOT_FOUND, 'Not Found');
32
- static S003 = new ServerErrorCode('S003', common_1.ErrorType.VALIDATION_ERROR, 'Invalid Input');
33
- static S004 = new ServerErrorCode('S004', common_1.ErrorType.CLIENT_ERROR, 'Request body is too large', 413);
34
- static S005 = new ServerErrorCode('S005', common_1.ErrorType.CLIENT_ERROR, 'Request body size did not match Content-Length');
35
- static S006 = new ServerErrorCode('S006', common_1.ErrorType.CLIENT_ERROR, "Body cannot be empty when content-type is set to 'application/json'");
36
- static S007 = new ServerErrorCode('S007', common_1.ErrorType.CLIENT_ERROR, 'Received media type is not supported', 415);
30
+ static S001 = new ServerErrorCode('S001', common_1.ErrorType.SERVER_ERROR, 'An unexpected server error occurred while processing the request');
31
+ static S002 = new ServerErrorCode('S002', common_1.ErrorType.NOT_FOUND, 'The requested endpoint does not exist');
32
+ static S003 = new ServerErrorCode('S003', common_1.ErrorType.VALIDATION_ERROR, 'The provided input data is invalid or does not meet validation requirements');
33
+ static S004 = new ServerErrorCode('S004', common_1.ErrorType.UNAUTHENTICATED, 'Authentication credentials are required to access this resource');
34
+ static S005 = new ServerErrorCode('S005', common_1.ErrorType.UNAUTHORIZED, 'Access denied due to insufficient permissions to perform this operation');
35
+ static S006 = new ServerErrorCode('S006', common_1.ErrorType.CLIENT_ERROR, 'The request is malformed or contains invalid parameters');
36
+ static S007 = new ServerErrorCode('S007', common_1.ErrorType.CLIENT_ERROR, 'Rate limit exceeded due to too many requests sent in a given time frame', 429);
37
+ static S008 = new ServerErrorCode('S008', common_1.ErrorType.CONFLICT, 'Resource conflict as the requested operation conflicts with existing data');
38
+ static S009 = new ServerErrorCode('S009', common_1.ErrorType.NOT_FOUND, 'The requested resource could not be found');
37
39
  }
38
40
  exports.ServerErrorCode = ServerErrorCode;
@@ -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 {};
@@ -6,22 +6,35 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Context = void 0;
10
- const async_hooks_1 = require("async_hooks");
9
+ exports.ContextService = void 0;
10
+ const node_async_hooks_1 = require("node:async_hooks");
11
11
  const app_1 = require("@shadow-library/app");
12
12
  const common_1 = require("@shadow-library/common");
13
13
  const REQUEST = Symbol('request');
14
14
  const RESPONSE = Symbol('response');
15
15
  const RID = Symbol('rid');
16
- let Context = class Context {
17
- storage = new async_hooks_1.AsyncLocalStorage();
16
+ const PARENT_CONTEXT = Symbol('parent-context');
17
+ const CHILD_RID_COUNTER = Symbol('child-rid-counter');
18
+ let ContextService = class ContextService {
19
+ static name = 'ContextService';
20
+ storage = new node_async_hooks_1.AsyncLocalStorage();
18
21
  init() {
19
- return async (req, res) => {
22
+ return (req, res, done) => {
23
+ const parentStore = this.storage.getStore();
20
24
  const store = new Map();
25
+ if (parentStore) {
26
+ const isChildContext = parentStore.has(PARENT_CONTEXT);
27
+ if (isChildContext)
28
+ throw new common_1.InternalError('Cannot create a child context within an existing child context');
29
+ const childRIDCounter = (this.get(CHILD_RID_COUNTER) ?? 0) + 1;
30
+ this.set(CHILD_RID_COUNTER, childRIDCounter);
31
+ req.id = `${this.getRID()}-${childRIDCounter}`;
32
+ store.set(PARENT_CONTEXT, parentStore);
33
+ }
21
34
  store.set(REQUEST, req);
22
35
  store.set(RESPONSE, res);
23
36
  store.set(RID, req.id);
24
- this.storage.enterWith(store);
37
+ this.storage.run(store, done);
25
38
  };
26
39
  }
27
40
  get(key, throwOnMissing) {
@@ -29,11 +42,26 @@ let Context = class Context {
29
42
  if (!store)
30
43
  throw new common_1.InternalError('Context not yet initialized');
31
44
  const value = store.get(key);
32
- if (throwOnMissing && value === undefined) {
45
+ if (throwOnMissing && value === undefined)
33
46
  throw new common_1.InternalError(`Key '${key.toString()}' not found in the context`);
34
- }
35
47
  return value ?? null;
36
48
  }
49
+ getFromParent(key, throwOnMissing) {
50
+ if (!this.isChildContext())
51
+ return this.get(key, throwOnMissing);
52
+ const parentStore = this.get(PARENT_CONTEXT, true);
53
+ const value = parentStore.get(key);
54
+ if (throwOnMissing && value === undefined)
55
+ throw new common_1.InternalError(`Key '${key.toString()}' not found in the parent context`);
56
+ return value ?? null;
57
+ }
58
+ resolve(key, throwOnMissing) {
59
+ const isChild = this.isChildContext();
60
+ const value = this.get(key, !isChild);
61
+ if (value !== null || !isChild)
62
+ return value;
63
+ return this.getFromParent(key, throwOnMissing);
64
+ }
37
65
  set(key, value) {
38
66
  const store = this.storage.getStore();
39
67
  if (!store)
@@ -41,6 +69,17 @@ let Context = class Context {
41
69
  store.set(key, value);
42
70
  return this;
43
71
  }
72
+ setInParent(key, value) {
73
+ if (!this.isChildContext())
74
+ return this.set(key, value);
75
+ const parentStore = this.get(PARENT_CONTEXT, true);
76
+ parentStore.set(key, value);
77
+ return this;
78
+ }
79
+ isChildContext() {
80
+ const parentStore = this.get(PARENT_CONTEXT);
81
+ return parentStore !== null;
82
+ }
44
83
  getRequest() {
45
84
  return this.get(REQUEST, true);
46
85
  }
@@ -51,7 +90,7 @@ let Context = class Context {
51
90
  return this.get(RID, true);
52
91
  }
53
92
  };
54
- exports.Context = Context;
55
- exports.Context = Context = __decorate([
93
+ exports.ContextService = ContextService;
94
+ exports.ContextService = ContextService = __decorate([
56
95
  (0, app_1.Injectable)()
57
- ], Context);
96
+ ], ContextService);
@@ -1,30 +1,31 @@
1
- import { AppError, ErrorType, Logger, ValidationError } from '@shadow-library/common';
1
+ import { AppError, Logger, ValidationError } from '@shadow-library/common';
2
2
  import { NAMESPACE } from '../constants.js';
3
3
  import { ServerError, ServerErrorCode } from '../server.error.js';
4
4
  const unexpectedError = new ServerError(ServerErrorCode.S001);
5
5
  const validationError = new ServerError(ServerErrorCode.S003);
6
+ const invalidRequestError = new ServerError(ServerErrorCode.S006);
6
7
  export class DefaultErrorHandler {
7
8
  logger = Logger.getLogger(NAMESPACE, 'DefaultErrorHandler');
8
9
  parseFastifyError(err) {
9
10
  if (err.statusCode === 500)
10
11
  return { statusCode: 500, error: unexpectedError.toObject() };
11
- return { statusCode: err.statusCode, error: { code: err.code, type: ErrorType.CLIENT_ERROR, message: err.message } };
12
+ return { statusCode: err.statusCode, error: { ...invalidRequestError.toObject(), message: err.message } };
12
13
  }
13
14
  handle(err, _req, res) {
14
15
  this.logger.warn('Handling error', err);
15
16
  if (err.cause)
16
- this.logger.warn('Caused by:', err.cause);
17
+ this.logger.warn('Caused by', err.cause);
17
18
  if (err instanceof ServerError)
18
19
  return res.status(err.getStatusCode()).send(err.toObject());
19
20
  else if (err instanceof ValidationError)
20
- return res.status(400).send({ ...err.toObject(), code: validationError.getCode() });
21
+ return res.status(validationError.getStatusCode()).send({ ...err.toObject(), ...validationError.toObject() });
21
22
  else if (err instanceof AppError)
22
23
  return res.status(500).send(err.toObject());
23
24
  else if (err.name === 'FastifyError') {
24
25
  const { statusCode, error } = this.parseFastifyError(err);
25
26
  return res.status(statusCode).send(error);
26
27
  }
27
- this.logger.error('Unhandler error has occurred:', err);
28
+ this.logger.error('Unhandled error has occurred', err);
28
29
  return res.status(500).send(unexpectedError.toObject());
29
30
  }
30
31
  }
@@ -1,4 +1,5 @@
1
1
  import { JSONSchema } from '@shadow-library/class-schema';
2
+ import { Class } from 'type-fest';
2
3
  export declare enum RouteInputType {
3
4
  BODY = "body",
4
5
  PARAMS = "params",
@@ -6,7 +7,7 @@ export declare enum RouteInputType {
6
7
  REQUEST = "request",
7
8
  RESPONSE = "response"
8
9
  }
9
- export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema>>;
10
+ export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema | Class<unknown>>>;
10
11
  export declare function HttpInput(type: RouteInputType, schema?: JSONSchema): ParameterDecorator;
11
12
  export declare const Body: (schema?: JSONSchema) => ParameterDecorator;
12
13
  export declare const Params: (schema?: JSONSchema) => ParameterDecorator;
@@ -1,6 +1,5 @@
1
- import assert from 'assert';
1
+ import assert from 'node:assert';
2
2
  import { Route } from '@shadow-library/app';
3
- import { ClassSchema } from '@shadow-library/class-schema';
4
3
  import { HTTP_CONTROLLER_INPUTS, PARAMTYPES_METADATA } from '../constants.js';
5
4
  export var RouteInputType;
6
5
  (function (RouteInputType) {
@@ -18,7 +17,7 @@ export function HttpInput(type, schema) {
18
17
  inputs[index] = type;
19
18
  if (!schema) {
20
19
  const paramTypes = Reflect.getMetadata(PARAMTYPES_METADATA, target, propertyKey);
21
- schema = ClassSchema.generate(paramTypes[index]);
20
+ schema = paramTypes[index];
22
21
  }
23
22
  const descriptor = Reflect.getOwnPropertyDescriptor(target, propertyKey);
24
23
  assert(descriptor, 'Cannot apply decorator to a non-method');
@@ -3,3 +3,4 @@ export * from './http-input.decorator.js';
3
3
  export * from './http-output.decorator.js';
4
4
  export * from './http-route.decorator.js';
5
5
  export * from './middleware.decorator.js';
6
+ export * from './sensitive.decorator.js';
@@ -3,3 +3,4 @@ export * from './http-input.decorator.js';
3
3
  export * from './http-output.decorator.js';
4
4
  export * from './http-route.decorator.js';
5
5
  export * from './middleware.decorator.js';
6
+ export * from './sensitive.decorator.js';