@shadow-library/fastify 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ import assert from 'node:assert';
17
17
  import { Inject, Injectable, Router } from '@shadow-library/app';
18
18
  import { ClassSchema, TransformerFactory } from '@shadow-library/class-schema';
19
19
  import { InternalError, Logger, utils } from '@shadow-library/common';
20
- import merge from 'deepmerge';
20
+ import { all as deepmerge } from 'deepmerge';
21
21
  import findMyWay from 'find-my-way';
22
22
  import stringify from 'json-stable-stringify';
23
23
  /**
@@ -26,11 +26,13 @@ import stringify from 'json-stable-stringify';
26
26
  import { FASTIFY_CONFIG, FASTIFY_INSTANCE, HTTP_CONTROLLER_INPUTS, HTTP_CONTROLLER_TYPE, NAMESPACE } from '../constants.js';
27
27
  import { HttpMethod } from '../decorators/index.js';
28
28
  import { ContextService } from '../services/index.js';
29
+ import { INBUILT_TRANSFORMERS } from './data-transformers.js';
29
30
  /**
30
31
  * Declaring the constants
31
32
  */
32
33
  const httpMethods = Object.values(HttpMethod).filter(m => m !== HttpMethod.ALL);
33
- const DEFAULT_ARTIFACTS = { transforms: {} };
34
+ const DEFAULT_ARTIFACTS = { masks: {}, transformers: {} };
35
+ const isClassSchema = (schema) => typeof schema === 'function' || (Array.isArray(schema) && typeof schema[0] === 'function');
34
36
  let FastifyRouter = class FastifyRouter extends Router {
35
37
  config;
36
38
  instance;
@@ -39,12 +41,17 @@ let FastifyRouter = class FastifyRouter extends Router {
39
41
  logger = Logger.getLogger(NAMESPACE, 'FastifyRouter');
40
42
  cachedDynamicMiddlewares = new Map();
41
43
  childRouter = null;
44
+ transformers = { ...INBUILT_TRANSFORMERS };
42
45
  sensitiveTransformer = new TransformerFactory(s => s['x-fastify']?.sensitive === true);
46
+ inputDataTransformer = new TransformerFactory(s => this.isTransformable('input', s));
47
+ outputDataTransformer = new TransformerFactory(s => this.isTransformable('output', s));
43
48
  constructor(config, instance, context) {
44
49
  super();
45
50
  this.config = config;
46
51
  this.instance = instance;
47
52
  this.context = context;
53
+ if (config.transformers)
54
+ Object.assign(this.transformers, config.transformers);
48
55
  if (config.enableChildRoutes) {
49
56
  const options = utils.object.pickKeys(config, ['ignoreTrailingSlash', 'ignoreDuplicateSlashes', 'allowUnsafeRegex', 'caseSensitive', 'maxParamLength', 'querystringParser']);
50
57
  this.childRouter = findMyWay(options);
@@ -53,6 +60,10 @@ let FastifyRouter = class FastifyRouter extends Router {
53
60
  getInstance() {
54
61
  return this.instance;
55
62
  }
63
+ isTransformable(type, schema) {
64
+ const transformerType = schema['x-fastify']?.transform?.[type];
65
+ return typeof transformerType === 'string' && transformerType in this.transformers;
66
+ }
56
67
  joinPaths(...parts) {
57
68
  const path = parts
58
69
  .filter(p => typeof p === 'string')
@@ -61,6 +72,13 @@ let FastifyRouter = class FastifyRouter extends Router {
61
72
  .join('/');
62
73
  return `/${path}`;
63
74
  }
75
+ addRouteHandler(routeOptions, hook, handler, action = 'append') {
76
+ const existingHandlers = Array.isArray(routeOptions[hook]) ? routeOptions[hook] : routeOptions[hook] ? [routeOptions[hook]] : [];
77
+ if (action === 'prepend')
78
+ routeOptions[hook] = [handler, ...existingHandlers];
79
+ else
80
+ routeOptions[hook] = [...existingHandlers, handler];
81
+ }
64
82
  registerRawBody() {
65
83
  const opts = { parseAs: 'buffer' };
66
84
  const parser = this.instance.getDefaultJsonParser('error', 'error');
@@ -82,7 +100,16 @@ let FastifyRouter = class FastifyRouter extends Router {
82
100
  return utils.string.maskWords(stringified);
83
101
  return '****';
84
102
  }
103
+ generateDataTransformer(type) {
104
+ return (value, schema) => {
105
+ const transformType = schema['x-fastify']?.transform?.[type];
106
+ const transformer = this.transformers[transformType];
107
+ assert(transformer, `transformer '${transformType}' not found`);
108
+ return transformer(value);
109
+ };
110
+ }
85
111
  getRequestLogger() {
112
+ const mask = this.maskField.bind(this);
86
113
  return (req, res, done) => {
87
114
  const startTime = process.hrtime();
88
115
  res.raw.on('finish', () => {
@@ -90,7 +117,7 @@ let FastifyRouter = class FastifyRouter extends Router {
90
117
  if (isLoggingDisabled)
91
118
  return;
92
119
  const { url, config } = req.routeOptions;
93
- const { transforms } = config.artifacts ?? DEFAULT_ARTIFACTS;
120
+ const { masks } = config.artifacts ?? DEFAULT_ARTIFACTS;
94
121
  const metadata = {};
95
122
  metadata.rid = this.context.getRID();
96
123
  metadata.url = url ?? req.raw.url;
@@ -103,11 +130,11 @@ let FastifyRouter = class FastifyRouter extends Router {
103
130
  const resTime = process.hrtime(startTime);
104
131
  metadata.timeTaken = (resTime[0] * 1e3 + resTime[1] * 1e-6).toFixed(3); // Converting time to milliseconds
105
132
  if (req.body)
106
- metadata.body = transforms.maskBody ? transforms.maskBody(structuredClone(req.body)) : req.body;
133
+ metadata.body = masks.body ? masks.body(structuredClone(req.body), mask) : req.body;
107
134
  if (req.query)
108
- metadata.query = transforms.maskQuery ? transforms.maskQuery(structuredClone(req.query)) : req.query;
135
+ metadata.query = masks.query ? masks.query(structuredClone(req.query), mask) : req.query;
109
136
  if (req.params)
110
- metadata.params = transforms.maskParams ? transforms.maskParams(structuredClone(req.params)) : req.params;
137
+ metadata.params = masks.params ? masks.params(structuredClone(req.params), mask) : req.params;
111
138
  this.logger.http(`${req.method} ${metadata.url} -> ${res.statusCode} (${metadata.timeTaken}ms)`, metadata);
112
139
  });
113
140
  done();
@@ -200,6 +227,52 @@ let FastifyRouter = class FastifyRouter extends Router {
200
227
  this.cachedDynamicMiddlewares.set(cacheKey, handler);
201
228
  return handler;
202
229
  }
230
+ transformResponseHandler() {
231
+ const transform = this.generateDataTransformer('output');
232
+ return async (request, reply, payload) => {
233
+ const statusCode = String(reply.statusCode);
234
+ const responseTransformers = request.routeOptions.config.artifacts?.transformers.response;
235
+ if (!responseTransformers)
236
+ return payload;
237
+ let transformer = responseTransformers[statusCode];
238
+ if (!transformer) {
239
+ const fallbackStatus = statusCode.charAt(0) + 'xx';
240
+ transformer = responseTransformers[fallbackStatus];
241
+ this.logger.debug(`using fallback response transformer for status code ${statusCode}`, { fallbackStatus });
242
+ }
243
+ if (transformer) {
244
+ this.logger.debug(`transforming response for status code ${statusCode}`);
245
+ const cloned = deepmerge([{}, payload]);
246
+ const data = transformer(cloned, transform);
247
+ this.logger.debug(`transformed response for status code ${statusCode}`, { data });
248
+ return data;
249
+ }
250
+ return payload;
251
+ };
252
+ }
253
+ transformRequestHandler() {
254
+ const transform = this.generateDataTransformer('input');
255
+ return async (request) => {
256
+ const transformers = request.routeOptions.config.artifacts?.transformers;
257
+ if (!transformers)
258
+ return;
259
+ if (transformers.body && request.body) {
260
+ this.logger.debug('transforming request body', { body: request.body });
261
+ request.body = transformers.body(request.body, transform);
262
+ this.logger.debug('transformed request body', { body: request.body });
263
+ }
264
+ if (transformers.query && request.query) {
265
+ this.logger.debug('transforming request query', { query: request.query });
266
+ request.query = transformers.query(request.query, transform);
267
+ this.logger.debug('transformed request query', { query: request.query });
268
+ }
269
+ if (transformers.params && request.params) {
270
+ this.logger.debug('transforming request params', { params: request.params });
271
+ request.params = transformers.params(request.params, transform);
272
+ this.logger.debug('transformed request params', { params: request.params });
273
+ }
274
+ };
275
+ }
203
276
  async register(controllers) {
204
277
  const { middlewares, routes } = this.parseControllers(controllers);
205
278
  const defaultResponseSchemas = this.config.responseSchema ?? {};
@@ -216,7 +289,7 @@ let FastifyRouter = class FastifyRouter extends Router {
216
289
  assert(metadata.method, 'Route method is required');
217
290
  this.logger.debug(`registering route ${metadata.method} ${metadata.path}`);
218
291
  const fastifyRouteOptions = utils.object.omitKeys(metadata, ['path', 'method', 'schemas', 'rawBody', 'status', 'headers', 'redirect', 'render']);
219
- const artifacts = { transforms: {} };
292
+ const artifacts = { masks: {}, transformers: {} };
220
293
  const routeOptions = { ...fastifyRouteOptions, config: { metadata, artifacts } };
221
294
  routeOptions.url = metadata.path;
222
295
  routeOptions.method = metadata.method === HttpMethod.ALL ? httpMethods : [metadata.method];
@@ -228,45 +301,78 @@ let FastifyRouter = class FastifyRouter extends Router {
228
301
  const handler = await this.getMiddlewareHandler(middleware, metadata);
229
302
  if (typeof handler === 'function') {
230
303
  this.logger.debug(`applying '${type}' middleware '${name}'`);
231
- const middlewareHandler = routeOptions[type];
232
- if (middlewareHandler)
233
- middlewareHandler.push(handler);
234
- else
235
- routeOptions[type] = [handler];
304
+ this.addRouteHandler(routeOptions, type, handler);
236
305
  }
237
306
  }
238
- routeOptions.schema = {};
307
+ const responseSchemas = { ...defaultResponseSchemas };
308
+ routeOptions.schema = { response: responseSchemas };
239
309
  routeOptions.attachValidation = metadata.silentValidation ?? false;
240
- routeOptions.schema.response = merge(metadata.schemas?.response ?? {}, defaultResponseSchemas);
241
- const { body: bodySchema, params: paramsSchema, query: querySchema } = metadata.schemas ?? {};
310
+ const { body: bodySchema, params: paramsSchema, query: querySchema, response: responseSchema } = metadata.schemas ?? {};
242
311
  const isMaskEnabled = this.config.maskSensitiveData ?? true;
243
312
  if (bodySchema) {
244
- const schema = typeof bodySchema === 'function' ? ClassSchema.generate(bodySchema) : bodySchema;
313
+ const schema = isClassSchema(bodySchema) ? ClassSchema.generate(bodySchema) : bodySchema;
245
314
  routeOptions.schema.body = schema;
246
- if (ClassSchema.isBranded(schema) && isMaskEnabled) {
247
- const transformer = this.sensitiveTransformer.maybeCompile(schema);
248
- if (transformer)
249
- artifacts.transforms.maskBody = obj => transformer(obj, this.maskField);
315
+ if (ClassSchema.isBranded(schema)) {
316
+ const bodyTransformer = this.inputDataTransformer.maybeCompile(schema);
317
+ if (bodyTransformer)
318
+ artifacts.transformers.body = bodyTransformer;
319
+ if (isMaskEnabled) {
320
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
321
+ if (transformer)
322
+ artifacts.masks.body = transformer;
323
+ }
250
324
  }
251
325
  }
252
326
  if (paramsSchema) {
253
- const schema = typeof paramsSchema === 'function' ? ClassSchema.generate(paramsSchema) : paramsSchema;
327
+ const schema = isClassSchema(paramsSchema) ? ClassSchema.generate(paramsSchema) : paramsSchema;
254
328
  routeOptions.schema.params = schema;
255
- if (ClassSchema.isBranded(schema) && isMaskEnabled) {
256
- const transformer = this.sensitiveTransformer.maybeCompile(schema);
257
- if (transformer)
258
- artifacts.transforms.maskParams = obj => transformer(obj, this.maskField);
329
+ if (ClassSchema.isBranded(schema)) {
330
+ const paramsTransformer = this.inputDataTransformer.maybeCompile(schema);
331
+ if (paramsTransformer)
332
+ artifacts.transformers.params = paramsTransformer;
333
+ if (isMaskEnabled) {
334
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
335
+ if (transformer)
336
+ artifacts.masks.params = transformer;
337
+ }
259
338
  }
260
339
  }
261
340
  if (querySchema) {
262
- const schema = typeof querySchema === 'function' ? ClassSchema.generate(querySchema) : querySchema;
341
+ const schema = isClassSchema(querySchema) ? ClassSchema.generate(querySchema) : querySchema;
263
342
  routeOptions.schema.querystring = schema;
264
- if (ClassSchema.isBranded(schema) && isMaskEnabled) {
265
- const transformer = this.sensitiveTransformer.maybeCompile(schema);
266
- if (transformer)
267
- artifacts.transforms.maskQuery = obj => transformer(obj, this.maskField);
343
+ if (ClassSchema.isBranded(schema)) {
344
+ const queryTransformer = this.inputDataTransformer.maybeCompile(schema);
345
+ if (queryTransformer)
346
+ artifacts.transformers.query = queryTransformer;
347
+ if (isMaskEnabled) {
348
+ const transformer = this.sensitiveTransformer.maybeCompile(schema);
349
+ if (transformer)
350
+ artifacts.masks.query = transformer;
351
+ }
268
352
  }
269
353
  }
354
+ if (responseSchema) {
355
+ const responseTransformers = {};
356
+ artifacts.transformers.response = responseTransformers;
357
+ for (const [code, schemaDef] of Object.entries(responseSchema)) {
358
+ const statusCode = code.toLowerCase();
359
+ const schema = isClassSchema(schemaDef) ? ClassSchema.generate(schemaDef) : schemaDef;
360
+ responseSchemas[statusCode] = schema;
361
+ if (ClassSchema.isBranded(schema)) {
362
+ const transformer = this.outputDataTransformer.maybeCompile(schema);
363
+ if (transformer)
364
+ responseTransformers[statusCode] = transformer;
365
+ }
366
+ }
367
+ if (Object.keys(responseTransformers).length > 0) {
368
+ const handler = this.transformResponseHandler();
369
+ this.addRouteHandler(routeOptions, 'preSerialization', handler);
370
+ }
371
+ }
372
+ if ('body' in artifacts.transformers || 'query' in artifacts.transformers || 'params' in artifacts.transformers) {
373
+ const handler = this.transformRequestHandler();
374
+ this.addRouteHandler(routeOptions, 'preHandler', handler, 'prepend');
375
+ }
270
376
  this.logger.debug('route options', { options: routeOptions });
271
377
  this.instance.route(routeOptions);
272
378
  this.logger.info(`registered route ${metadata.method} ${routeOptions.url}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shadow-library/fastify",
3
3
  "type": "module",
4
- "version": "1.4.0",
4
+ "version": "1.5.0",
5
5
  "sideEffects": false,
6
6
  "description": "A Fastify wrapper featuring decorator-based routing, middleware and error handling",
7
7
  "repository": {