@xylabs/express 5.0.22 → 5.0.23

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.
@@ -0,0 +1,8 @@
1
+ import type { RequestHandler } from 'express';
2
+ export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head';
3
+ export interface RouteDefinition<H extends RequestHandler = RequestHandler> {
4
+ handlers: H[] | H;
5
+ method: HttpMethod;
6
+ path: string | RegExp;
7
+ }
8
+ //# sourceMappingURL=RouteDefinition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteDefinition.d.ts","sourceRoot":"","sources":["../../../src/Handler/RouteDefinition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,MAAM,UAAU,GAChB,KAAK,GACL,MAAM,GACN,KAAK,GACL,OAAO,GACP,QAAQ,GACR,SAAS,GACT,MAAM,CAAA;AAEZ,MAAM,WAAW,eAAe,CAC9B,CAAC,SAAS,cAAc,GAAG,cAAc;IAEzC,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,CAAA;IACjB,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CACtB"}
@@ -1,4 +1,5 @@
1
1
  export * from './asyncHandler.ts';
2
2
  export * from './errorToJsonHandler.ts';
3
+ export * from './RouteDefinition.ts';
3
4
  export * from './StatusCodeHandlers/index.ts';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Handler/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,yBAAyB,CAAA;AACvC,cAAc,+BAA+B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Handler/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,+BAA+B,CAAA"}
@@ -1,5 +1,14 @@
1
1
  export type ParseFunc<T = number> = (value: string) => T;
2
+ /**
3
+ * @deprecated use zod instead
4
+ */
2
5
  export declare const tryParse: <T = number>(func: ParseFunc<T>, value?: string) => (T & {}) | undefined;
6
+ /**
7
+ * @deprecated use zod instead
8
+ */
3
9
  export declare const tryParseFloat: (value?: string) => number | undefined;
10
+ /**
11
+ * @deprecated use zod instead
12
+ */
4
13
  export declare const tryParseInt: (value?: string) => number | undefined;
5
14
  //# sourceMappingURL=tryParse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tryParse.d.ts","sourceRoot":"","sources":["../../../src/Util/tryParse.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAA;AAExD,eAAO,MAAM,QAAQ,GAAI,CAAC,GAAG,MAAM,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,MAAM,yBAStE,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,uBAE3C,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,uBAEzC,CAAA"}
1
+ {"version":3,"file":"tryParse.d.ts","sourceRoot":"","sources":["../../../src/Util/tryParse.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAA;AAExD;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,GAAG,MAAM,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,MAAM,yBAStE,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,uBAE3C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,uBAEzC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from './requestHandlerValidator.ts';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Validation/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAA"}
@@ -0,0 +1,31 @@
1
+ import type { NextFunction, Request, RequestHandler, Response } from 'express';
2
+ import { z, type ZodType } from 'zod';
3
+ /**
4
+ * Empty Zod schema for requests with no parameters.
5
+ */
6
+ export declare const EmptyParamsZod: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
7
+ /**
8
+ * Empty Zod schema for requests with no query parameters.
9
+ */
10
+ export declare const EmptyQueryParamsZod: z.ZodObject<{}, z.core.$catchall<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>>;
11
+ /**
12
+ * Default validation schemas for request handler validator.
13
+ */
14
+ export declare const ValidateRequestDefaults: {
15
+ params: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
16
+ query: z.ZodObject<{}, z.core.$catchall<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>>;
17
+ body: z.ZodOptional<z.ZodJSONSchema>;
18
+ response: z.ZodOptional<z.ZodJSONSchema>;
19
+ };
20
+ /**
21
+ * Factory for Express middleware that validates request and response objects using Zod schemas.
22
+ * @param schemas The Zod schemas to use for validation.
23
+ * @returns A middleware function for validating requests and responses.
24
+ */
25
+ export declare function requestHandlerValidator<TParams extends typeof EmptyQueryParamsZod | ZodType<Record<string, string>> = typeof EmptyQueryParamsZod, TQuery extends typeof EmptyQueryParamsZod | ZodType<Record<string, string | string[]>> = typeof EmptyQueryParamsZod, TBody extends ZodType<unknown> = ZodType<unknown>, TResponse extends ZodType<unknown> = ZodType<unknown>>(schemas?: Partial<{
26
+ body: TBody;
27
+ params: TParams;
28
+ query: TQuery;
29
+ response: TResponse;
30
+ }>): (handler: (req: Request<z.core.output<TParams>, z.core.output<TResponse>, z.core.output<TBody>, z.core.output<TQuery>>, res: Response<z.core.output<TResponse>>, next: NextFunction) => unknown) => RequestHandler;
31
+ //# sourceMappingURL=requestHandlerValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestHandlerValidator.d.ts","sourceRoot":"","sources":["../../../src/Validation/requestHandlerValidator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAChD,MAAM,SAAS,CAAA;AAEhB,OAAO,EAAE,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,KAAK,CAAA;AAIrC;;GAEG;AACH,eAAO,MAAM,cAAc,gDAAoC,CAAA;AAE/D;;GAEG;AACH,eAAO,MAAM,mBAAmB,gGAAoE,CAAA;AAEpG;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;;CAKnC,CAAA;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,SAAS,OAAO,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,mBAAmB,EACzG,MAAM,SAAS,OAAO,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,mBAAmB,EACnH,KAAK,SAAS,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,EACjD,SAAS,SAAS,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,EACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAC,IAOQ,SAAS,CAAC,GAAG,EAAE,OAAO,+FAA0B,EAAE,GAAG,EAAE,QAAQ,0BAAK,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,KAAG,cAAc,CAiE9H"}
@@ -5,4 +5,5 @@ export * from './middleware/index.ts';
5
5
  export * from './Model/index.ts';
6
6
  export * from './Performance/index.ts';
7
7
  export * from './Util/index.ts';
8
+ export * from './Validation/index.ts';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,uBAAuB,CAAA"}
@@ -365,12 +365,83 @@ var tryParseFloat = (value) => {
365
365
  var tryParseInt = (value) => {
366
366
  return tryParse(Number.parseInt, value);
367
367
  };
368
+
369
+ // src/Validation/requestHandlerValidator.ts
370
+ import { isDefined as isDefined3, isPromise } from "@xylabs/typeof";
371
+ import { ReasonPhrases as ReasonPhrases2, StatusCodes as StatusCodes2 } from "http-status-codes";
372
+ import { z } from "zod";
373
+ var EmptyParamsZod = z.object({}).catchall(z.string());
374
+ var EmptyQueryParamsZod = z.object({}).catchall(z.union([z.string(), z.array(z.string())]));
375
+ var ValidateRequestDefaults = {
376
+ params: EmptyParamsZod,
377
+ query: EmptyQueryParamsZod,
378
+ body: z.json().optional(),
379
+ response: z.json().optional()
380
+ };
381
+ function requestHandlerValidator(schemas) {
382
+ const validators = { ...ValidateRequestDefaults, ...schemas };
383
+ return (handler) => {
384
+ return async (req, res, next) => {
385
+ const originalJson = res.json.bind(res);
386
+ try {
387
+ const errors = [];
388
+ const keys = ["params", "query", "body"];
389
+ for (const key of keys) {
390
+ const validator = validators[key];
391
+ const result2 = validator.safeParse(req[key]);
392
+ if (result2.success) {
393
+ if (isDefined3(result2.data)) Object.assign(req[key], result2.data);
394
+ } else {
395
+ errors.push(
396
+ ...result2.error.issues.map(
397
+ (issue) => issue.path.length === 0 ? `${key}: ${issue.message}` : `${key}.${issue.path.join(".")}: ${issue.message}`
398
+ )
399
+ );
400
+ }
401
+ }
402
+ if (errors.length > 0) {
403
+ const message = errors.join("; ");
404
+ const err = new Error(message);
405
+ err.name = ReasonPhrases2.BAD_REQUEST;
406
+ err.statusCode = StatusCodes2.BAD_REQUEST;
407
+ next(err);
408
+ return false;
409
+ }
410
+ res.json = (data) => {
411
+ const result2 = validators.response.safeParse(data);
412
+ if (result2.success) {
413
+ return originalJson(result2.data);
414
+ } else {
415
+ const message = result2.error.issues.map(
416
+ (issue) => issue.path.length === 0 ? `response: ${issue.message}` : `response.${issue.path.join(".")}: ${issue.message}`
417
+ ).join("; ");
418
+ const err = new Error(message);
419
+ err.name = ReasonPhrases2.INTERNAL_SERVER_ERROR;
420
+ err.statusCode = StatusCodes2.INTERNAL_SERVER_ERROR;
421
+ res.json = originalJson;
422
+ throw err;
423
+ }
424
+ };
425
+ const result = handler(req, res, next);
426
+ if (result && isPromise(result)) {
427
+ await result;
428
+ }
429
+ } catch (err) {
430
+ res.json = originalJson;
431
+ next(err);
432
+ }
433
+ };
434
+ };
435
+ }
368
436
  export {
369
437
  Counters,
370
438
  DefaultJsonBodyParserOptions,
371
439
  DefaultJsonBodyParserOptionsLimit,
372
440
  DefaultJsonBodyParserOptionsTypes,
441
+ EmptyParamsZod,
442
+ EmptyQueryParamsZod,
373
443
  Profiler,
444
+ ValidateRequestDefaults,
374
445
  WrappedWinstonLogger,
375
446
  asyncHandler,
376
447
  clearRawResponseFormat,
@@ -390,6 +461,7 @@ export {
390
461
  isRawResponseFormatSet,
391
462
  jsonBodyParser,
392
463
  notImplemented,
464
+ requestHandlerValidator,
393
465
  responseProfiler,
394
466
  setRawResponseFormat,
395
467
  standardErrors,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/Handler/asyncHandler.ts","../../src/Handler/errorToJsonHandler.ts","../../src/Logger/getLogger.ts","../../src/Logger/LogFormats/LocalDev/logFormatLocalDev.ts","../../src/Logger/LogFormats/Rollbar/logFormatRollbar.ts","../../src/Logger/LogFormats/Structured/logFormatStructured.ts","../../src/Logger/toWinstonVerbosity.ts","../../src/Logger/Transports/Rollbar/canGetDefaultRollbarTransport.ts","../../src/Logger/Transports/Rollbar/getDefaultRollbarTransport.ts","../../src/Logger/Transports/Rollbar/RollbarTransport.ts","../../src/Logger/WrappedWinstonLogger.ts","../../src/Logger/getDefaultLogger.ts","../../src/Handler/StatusCodeHandlers/notImplemented.ts","../../src/HttpUtil/getHttpHeader.ts","../../src/middleware/caseInsensitiveRouting/caseInsensitiveRouting.ts","../../src/middleware/customPoweredByHeader/customPoweredByHeader.ts","../../src/middleware/jsonBodyParser/jsonBodyParser.ts","../../src/Performance/Counters.ts","../../src/Performance/Profiler.ts","../../src/middleware/metrics/counters.ts","../../src/middleware/metrics/responseProfiler.ts","../../src/middleware/standardResponses/getResponseMetadata.ts","../../src/middleware/standardResponses/standardErrors.ts","../../src/middleware/standardResponses/standardResponses.ts","../../src/Util/compactObject.ts","../../src/Util/tryParse.ts"],"sourcesContent":["import type {\n NextFunction, Request, RequestHandler, Response,\n} from 'express'\nimport type { ParamsDictionary, Query } from 'express-serve-static-core'\n\nexport function asyncHandler<P = NoReqParams, ResBody = NoResBody, ReqBody = NoReqBody, ReqQuery = NoReqQuery, Locals extends NoLocals = NoLocals>(\n fn: RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>,\n) {\n return (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>, res: Response<ResBody, Locals>, next: NextFunction) => {\n return Promise.resolve(fn(req, res, next)).catch(next)\n }\n}\n\nexport interface Empty {}\n\nexport type NoReqParams = ParamsDictionary\nexport type NoResBody = Empty\nexport type NoReqBody = Empty\nexport type NoReqQuery = Query\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type NoLocals = Record<string, any>\n","import { isError, isNumber } from '@xylabs/typeof'\nimport type {\n NextFunction, Request, Response,\n} from 'express'\n\nimport { getDefaultLogger } from '../Logger/index.ts'\nimport type { ExpressError } from '../Model/index.ts'\n\nexport const errorToJsonHandler = (error: ExpressError, req: Request, res: Response, next: NextFunction) => {\n if (isError(error)) {\n getDefaultLogger().error(error.message)\n if (!isNumber(error.statusCode)) error.statusCode = 500\n res.status(error.statusCode).send({ error: error.message })\n }\n next(error)\n}\n","import type { Logger } from '@xylabs/logger'\nimport { createLogger, transports as winstonTransports } from 'winston'\nimport type TransportStream from 'winston-transport'\n\nimport { logFormatLocalDev, logFormatStructured } from './LogFormats/index.ts'\nimport type { LoggerVerbosity } from './LoggerVerbosity.ts'\nimport { toWinstonVerbosity } from './toWinstonVerbosity.ts'\nimport { canGetDefaultRollbarTransport, getDefaultRollbarTransport } from './Transports/index.ts'\nimport type { WinstonVerbosity } from './WinstonVerbosity.ts'\nimport { WrappedWinstonLogger } from './WrappedWinstonLogger.ts'\n\nconst exitOnError = false\nconst handleRejections = true\n\nconst { Console } = winstonTransports\nconst consoleTransport = new Console()\nconst format = process.env.NODE_ENV === 'development' ? logFormatLocalDev : logFormatStructured\nconst transports: TransportStream[] = [consoleTransport]\nif (canGetDefaultRollbarTransport(process.env)) {\n try {\n const rollbarTransport = getDefaultRollbarTransport(process.env)\n transports.push(rollbarTransport)\n } catch {\n // NOTE: No error here, just gracefully adding logger if ENV VARs\n // were preset\n }\n}\n\nconst loggers: Record<WinstonVerbosity, Logger | undefined> = {\n debug: undefined,\n error: undefined,\n http: undefined,\n info: undefined,\n silly: undefined,\n verbose: undefined,\n warn: undefined,\n}\n\nexport const getLogger = (minVerbosity: LoggerVerbosity = 'info'): Logger => {\n const level = toWinstonVerbosity(minVerbosity)\n const existing = loggers[level]\n if (existing) return existing\n const logger = new WrappedWinstonLogger(\n createLogger({\n exitOnError,\n format,\n handleRejections,\n level,\n rejectionHandlers: transports,\n transports,\n }),\n )\n loggers[level] = logger\n return logger\n}\n","import { format } from 'winston'\n\nconst {\n colorize, combine, timestamp, printf,\n} = format\n\nexport const logFormatLocalDev = combine(\n colorize(),\n timestamp(),\n printf(info => `[${info.timestamp} ${info.level}] ${info.message}`),\n)\n","import { format } from 'winston'\n\nconst { simple } = format\n\nexport const logFormatRollbar = simple()\n","import { format } from 'winston'\n\nconst {\n combine, timestamp, json,\n} = format\n\nexport const logFormatStructured = combine(timestamp(), json())\n","import type { LoggerVerbosity } from './LoggerVerbosity.ts'\nimport type { WinstonVerbosity } from './WinstonVerbosity.ts'\n\nexport const toWinstonVerbosity = (loggerVerbosity: LoggerVerbosity): WinstonVerbosity => {\n return loggerVerbosity === 'all' ? 'silly' : loggerVerbosity\n}\n","export const canGetDefaultRollbarTransport = (env: { [key: string]: string | undefined }): boolean => {\n return env.ROLLBAR_ACCESS_TOKEN === undefined ? false : true\n}\n","import { assertEx } from '@xylabs/assert'\nimport Rollbar from 'rollbar'\n\nimport { RollbarTransport } from './RollbarTransport.ts'\n\nexport const getDefaultRollbarTransport = (env: { [key: string]: string | undefined }): RollbarTransport => {\n const accessToken = assertEx(env.ROLLBAR_ACCESS_TOKEN, () => 'Missing ROLLBAR_ACCESS_TOKEN ENV VAR')\n const rollbar: Rollbar = new Rollbar({ accessToken })\n return new RollbarTransport({}, rollbar)\n}\n","import type Rollbar from 'rollbar'\nimport type { TransportStreamOptions } from 'winston-transport'\nimport Transport from 'winston-transport'\n\nimport { logFormatRollbar } from '../../LogFormats/index.ts'\n\nexport class RollbarTransport extends Transport {\n protected readonly rollbar?: Rollbar\n constructor(\n opts: TransportStreamOptions,\n rollbar?: Rollbar,\n ) {\n super({\n ...opts, format: logFormatRollbar, level: 'error',\n })\n this.rollbar = rollbar\n }\n\n override log(info: { message?: string }, next: () => void) {\n this.rollbar?.error(info?.message)\n this.emit('logged', info?.message)\n next()\n }\n}\n","import type { LogFunction, Logger } from '@xylabs/logger'\nimport type { Logger as Winston } from 'winston'\n\n/**\n * Wrap Winston logger methods to adapt to familiar\n * console logging methods\n */\nexport class WrappedWinstonLogger implements Logger {\n protected readonly winston: Winston\n constructor(winston: Winston) {\n this.winston = winston\n }\n\n debug: LogFunction = message => this.winston.debug(message)\n error: LogFunction = message => this.winston.error(message)\n info: LogFunction = message => this.winston.info(message)\n log: LogFunction = message => this.winston.info(message)\n trace: LogFunction = message => this.winston.debug(message)\n warn: LogFunction = message => this.winston.warn(message)\n}\n","import type { Logger } from '@xylabs/logger'\n\nimport { getLogger } from './getLogger.ts'\nimport type { WrappedWinstonLogger } from './WrappedWinstonLogger.ts'\n\n/**\n * Static instance to prevent multiple instances of the same logger\n * with the same config\n */\n\ndeclare global {\n var xy: {\n defaultLogger?: WrappedWinstonLogger\n }\n}\n\nexport const getDefaultLogger = (): Logger => {\n if (globalThis.xy === undefined) globalThis.xy = {}\n if (globalThis.xy.defaultLogger) return globalThis.xy.defaultLogger\n return getLogger()\n}\n","import type { RequestHandler } from 'express'\nimport { ReasonPhrases, StatusCodes } from 'http-status-codes'\n\nexport const notImplemented: RequestHandler = (_req, _res, next) => {\n next({ message: ReasonPhrases.NOT_IMPLEMENTED, statusCode: StatusCodes.NOT_IMPLEMENTED })\n}\n","import { isDefined } from '@xylabs/typeof'\nimport type { Request } from 'express-serve-static-core'\n\n/**\n * Since there can be multiple of certain HTTP headers or\n * to prevent ugliness if someone did send us multiple\n * instances of a header we only expect one of, this\n * method grabs the 1st/only one of the desired header\n * @param header The header to find\n * @param req The received HTTP request (with headers)\n * @returns The first or only occurrence of the specified HTTP header\n */\nexport const getHttpHeader = (header: string, req: Request): string | undefined => {\n const headerValue = req.headers[header]\n const value\n // If the header exists\n = isDefined(headerValue)\n // If there's multiple of the same header\n ? Array.isArray(headerValue)\n // Grab the first one\n ? (headerValue as string[]).shift()\n // Otherwise grab the only one\n : (headerValue as string)\n // Otherwise undefined\n : undefined\n return value\n}\n","import type { Express } from 'express'\n\nconst setting = 'case sensitive routing'\n\n/**\n * Enable case sensitivity. When enabled, \"/Foo\" and \"/foo\" are different\n * routes. When disabled, \"/Foo\" and \"/foo\" are treated the same.\n * @param app The Express app to disable the header on.\n */\nexport const enableCaseSensitiveRouting = (app: Express) => {\n app.enable(setting)\n}\n\n/**\n * Disable case sensitivity. When enabled, \"/Foo\" and \"/foo\" are different\n * routes. When disabled, \"/Foo\" and \"/foo\" are treated the same.\n * @param app The Express app to disable the header on.\n */\nexport const disableCaseSensitiveRouting = (app: Express) => {\n app.disable(setting)\n}\n","import type {\n Express, NextFunction, Request, Response,\n} from 'express'\n\nconst header = 'X-Powered-By'\nconst setting = 'x-powered-by'\n\n/**\n * By default Express appends the `X-Powered-By: Express` header to\n * all responses. Calling this method enables that behavior.\n * @param app The Express app to disable the header on.\n */\nexport const enableExpressDefaultPoweredByHeader = (app: Express) => {\n app.enable(setting)\n}\n\n/**\n * By default Express appends the `X-Powered-By: Express` header to\n * all responses. Calling this method disables that behavior.\n * @param app The Express app to disable the header on.\n */\nexport const disableExpressDefaultPoweredByHeader = (app: Express) => {\n app.disable(setting)\n}\n\nexport const customPoweredByHeader = (req: Request, res: Response, next: NextFunction) => {\n res.setHeader(header, 'XYO')\n next()\n}\n","import type { OptionsJson } from 'body-parser'\nimport bodyParser from 'body-parser'\nimport type { NextHandleFunction } from 'connect'\n\nimport { getDefaultLogger } from '../../Logger/index.ts'\n\n/**\n * The default maximum request body size for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptionsLimit = '100kb'\n\n/**\n * The default MIME types for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptionsTypes = ['application/json', 'text/json']\n\n/**\n * The default options for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptions: OptionsJson = {\n limit: DefaultJsonBodyParserOptionsLimit,\n type: DefaultJsonBodyParserOptionsTypes,\n}\n\n/**\n * Gets the default JSON Body Parser options merged with the supplied options\n * with the supplied options taking precedence\n * @param options The options to override the default JSON Body Parser options with\n * @returns The combined JSON Body Parser options with the supplied values taking\n * precedence over the default\n */\nexport const getJsonBodyParserOptions = (options?: Partial<OptionsJson>): OptionsJson => {\n return options ? { ...DefaultJsonBodyParserOptions, ...options } : DefaultJsonBodyParserOptions\n}\n\n/**\n * Get a JSON Body Parser connect middleware handler\n * @param options The options for the JSON Body Parser\n * @returns A middleware function that parses JSON bodies\n */\nexport const getJsonBodyParser = (options: OptionsJson = DefaultJsonBodyParserOptions): NextHandleFunction => {\n // Create closed instance of bodyParser to prevent instantiation of new instance on every request\n const parser = bodyParser.json(options)\n\n return (req, res, next) => {\n // If we do not trap this error, then it dumps too much to log, usually happens if request aborted\n try {\n parser(req, res, next)\n } catch (ex) {\n const error = ex as Error\n getDefaultLogger().log(`bodyParser failed [${error?.name}]: ${error?.message}`)\n }\n }\n}\n\n/**\n * A JSON Body Parser middleware handler initialized with the default options\n */\nexport const jsonBodyParser = getJsonBodyParser()\n","export class Counters {\n static counters: Record<string, number> = {}\n\n static inc(name: string, count = 1) {\n this.catchError(name, (name: string) => {\n this.counters[name] = (this.counters[name] ?? 0) + count\n })\n }\n\n static max(name: string, count: number) {\n this.catchError(name, (name: string) => {\n const currentValue = this.counters[name]\n if (currentValue === undefined || count > currentValue) {\n this.counters[name] = count\n }\n })\n }\n\n static min(name: string, count: number) {\n this.catchError(name, (name: string) => {\n const currentValue = this.counters[name]\n if (currentValue === undefined || count < currentValue) {\n this.counters[name] = count\n }\n })\n }\n\n private static catchError = (name: string, func: (name: string) => void) => {\n try {\n func(name)\n } catch {\n this.counters[name] = 0\n this.inc('CountersErrors')\n }\n }\n}\n","export class Profiler {\n stats: Record<string, number> = {}\n\n async profile<T>(name: string, promise: Promise<T>) {\n const start = Date.now()\n const result = await promise\n this.stats[name] = Date.now() - start\n return result\n }\n}\n","import type {\n Application, NextFunction, Request, Response,\n} from 'express'\n\nimport { Counters } from '../../Performance/index.ts'\n\nexport const useRequestCounters = (app: Application): void => {\n // Configure Global counters\n app.use((req: Request, res: Response, next: NextFunction) => {\n Counters.inc(req.path)\n Counters.inc('_calls')\n next()\n })\n\n app.get('/stats', (req: Request, res: Response, next: NextFunction) => {\n /* #swagger.tags = ['Metrics'] */\n /* #swagger.summary = 'Get the counters for single instance of diviner' */\n res.json({\n alive: true,\n avgTime: `${((Counters.counters._totalTime ?? 0) / (Counters.counters._calls ?? 1)).toFixed(2)}ms`,\n counters: Counters.counters,\n })\n next()\n })\n}\n","import type {\n NextFunction, Request, Response,\n} from 'express'\n\n/**\n * Connect middleware to enable profiling of response lifecycle timing. To effectively profile\n * the response timing, this middleware needs to be called first when initializing your Express\n * App\n * @example\n * const app = express()\n * app.use(responseProfiler)\n * // other initialization ...\n * @param _req The request\n * @param res The response\n * @param next The next function\n */\nexport const responseProfiler = (_req: Request, res: Response, next: NextFunction) => {\n if (!res.locals?.meta) {\n res.locals.meta = {}\n }\n res.locals.meta.profile = { startTime: Date.now() }\n next()\n}\n","import type { Response } from 'express'\n\nexport const getResponseMetadata = (res: Response): Record<string, unknown> => {\n const meta: Record<string, unknown> = res.locals?.meta || {}\n // NOTE: We should do this somewhere else to better separate concerns\n const profile = res.locals.meta?.profile\n if (profile) {\n const startTime = profile?.startTime\n if (startTime) {\n const endTime = Date.now()\n const duration = endTime - startTime\n res.locals.meta.profile = {\n duration, endTime, startTime,\n }\n }\n }\n return meta\n}\n","import { isError } from '@xylabs/typeof'\nimport type {\n NextFunction, Request, Response,\n} from 'express'\n\nimport { getDefaultLogger } from '../../Logger/index.ts'\nimport type { ExpressError } from '../../Model/index.ts'\nimport type { ApiError } from './jsonApi/index.ts'\n\nexport const standardErrors = (err: ExpressError | undefined, req: Request, res: Response, next: NextFunction) => {\n if (!isError(err)) {\n next(err)\n return\n }\n getDefaultLogger().error(err.message)\n err.statusCode = err.statusCode ?? 500\n\n const error: ApiError = {\n detail: err.message,\n status: `${err.statusCode}`,\n title: err.name,\n }\n\n res.status(err.statusCode).json(error)\n\n next(err)\n}\n","import type {\n Request, RequestHandler, Response,\n} from 'express'\nimport mung from 'express-mung'\n\nimport { getResponseMetadata } from './getResponseMetadata.js'\n\ninterface TransformResponseLocals {\n rawResponse?: boolean\n}\n\n/**\n * Flags the response to forgo the standard response envelope\n * and return the raw response body to the client\n * @param res The response to disable the standard response format on\n */\nexport const setRawResponseFormat = (res: Response): void => {\n res.locals.rawResponse = true\n}\n\n/**\n * Clears any flags on the response, allowing the response to\n * use the default standard response envelope\n * @param res The response to set to the standard response format\n */\nexport const clearRawResponseFormat = (res: Response): void => {\n res.locals.rawResponse = false\n}\n\n/**\n * Checks if there are any flags on the response that would cause it\n * to forgo the standard response envelope and return the raw response\n * body to the client\n * @param res\n * @returns True if there are any flags on the response, false otherwise\n */\nexport const isRawResponseFormatSet = (res: Response): boolean => {\n return res.locals.rawResponse ? true : false\n}\n\n/**\n * Transforms each response to conform to the standard response format (compatible with JSON API)\n * @param body The original request body\n * @param _req The request\n * @param res The response\n * @returns The transformed response body\n */\nconst transformResponse = (body: unknown, _req: Request, res: Response<unknown, TransformResponseLocals>) => {\n return isRawResponseFormatSet(res)\n ? body\n : (res.statusCode >= 200 && res.statusCode < 300)\n ? { data: body, meta: getResponseMetadata(res) }\n : { error: body, meta: getResponseMetadata(res) }\n}\n\n/**\n * Connect middleware to enable the transform of all responses to match\n * the standard response format (compatible with JSON API)\n */\n\nexport const standardResponses: RequestHandler = mung.json(transformResponse, { mungError: true })\n","export const compactObject = <T extends Record<string, unknown>>(obj: T) => {\n const result: Record<string, unknown> = {}\n for (const key in obj) {\n if (obj[key] !== undefined && obj[key] !== null) {\n result[key] = obj[key]\n }\n }\n return result as T\n}\n","import { isDefined } from '@xylabs/typeof'\n\nexport type ParseFunc<T = number> = (value: string) => T\n\nexport const tryParse = <T = number>(func: ParseFunc<T>, value?: string) => {\n try {\n const result = isDefined(value) ? func(value) : null\n if (!Number.isNaN(result) && result !== null) {\n return result\n }\n } catch {\n return\n }\n}\n\nexport const tryParseFloat = (value?: string) => {\n return tryParse(Number.parseFloat, value)\n}\n\nexport const tryParseInt = (value?: string) => {\n return tryParse(Number.parseInt, value)\n}\n"],"mappings":"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","names":["format","format","combine","timestamp","format","header","setting","name","isError","isError","isDefined"]}
1
+ {"version":3,"sources":["../../src/Handler/asyncHandler.ts","../../src/Handler/errorToJsonHandler.ts","../../src/Logger/getLogger.ts","../../src/Logger/LogFormats/LocalDev/logFormatLocalDev.ts","../../src/Logger/LogFormats/Rollbar/logFormatRollbar.ts","../../src/Logger/LogFormats/Structured/logFormatStructured.ts","../../src/Logger/toWinstonVerbosity.ts","../../src/Logger/Transports/Rollbar/canGetDefaultRollbarTransport.ts","../../src/Logger/Transports/Rollbar/getDefaultRollbarTransport.ts","../../src/Logger/Transports/Rollbar/RollbarTransport.ts","../../src/Logger/WrappedWinstonLogger.ts","../../src/Logger/getDefaultLogger.ts","../../src/Handler/StatusCodeHandlers/notImplemented.ts","../../src/HttpUtil/getHttpHeader.ts","../../src/middleware/caseInsensitiveRouting/caseInsensitiveRouting.ts","../../src/middleware/customPoweredByHeader/customPoweredByHeader.ts","../../src/middleware/jsonBodyParser/jsonBodyParser.ts","../../src/Performance/Counters.ts","../../src/Performance/Profiler.ts","../../src/middleware/metrics/counters.ts","../../src/middleware/metrics/responseProfiler.ts","../../src/middleware/standardResponses/getResponseMetadata.ts","../../src/middleware/standardResponses/standardErrors.ts","../../src/middleware/standardResponses/standardResponses.ts","../../src/Util/compactObject.ts","../../src/Util/tryParse.ts","../../src/Validation/requestHandlerValidator.ts"],"sourcesContent":["import type {\n NextFunction, Request, RequestHandler, Response,\n} from 'express'\nimport type { ParamsDictionary, Query } from 'express-serve-static-core'\n\nexport function asyncHandler<P = NoReqParams, ResBody = NoResBody, ReqBody = NoReqBody, ReqQuery = NoReqQuery, Locals extends NoLocals = NoLocals>(\n fn: RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>,\n) {\n return (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>, res: Response<ResBody, Locals>, next: NextFunction) => {\n return Promise.resolve(fn(req, res, next)).catch(next)\n }\n}\n\nexport interface Empty {}\n\nexport type NoReqParams = ParamsDictionary\nexport type NoResBody = Empty\nexport type NoReqBody = Empty\nexport type NoReqQuery = Query\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type NoLocals = Record<string, any>\n","import { isError, isNumber } from '@xylabs/typeof'\nimport type {\n NextFunction, Request, Response,\n} from 'express'\n\nimport { getDefaultLogger } from '../Logger/index.ts'\nimport type { ExpressError } from '../Model/index.ts'\n\nexport const errorToJsonHandler = (error: ExpressError, req: Request, res: Response, next: NextFunction) => {\n if (isError(error)) {\n getDefaultLogger().error(error.message)\n if (!isNumber(error.statusCode)) error.statusCode = 500\n res.status(error.statusCode).send({ error: error.message })\n }\n next(error)\n}\n","import type { Logger } from '@xylabs/logger'\nimport { createLogger, transports as winstonTransports } from 'winston'\nimport type TransportStream from 'winston-transport'\n\nimport { logFormatLocalDev, logFormatStructured } from './LogFormats/index.ts'\nimport type { LoggerVerbosity } from './LoggerVerbosity.ts'\nimport { toWinstonVerbosity } from './toWinstonVerbosity.ts'\nimport { canGetDefaultRollbarTransport, getDefaultRollbarTransport } from './Transports/index.ts'\nimport type { WinstonVerbosity } from './WinstonVerbosity.ts'\nimport { WrappedWinstonLogger } from './WrappedWinstonLogger.ts'\n\nconst exitOnError = false\nconst handleRejections = true\n\nconst { Console } = winstonTransports\nconst consoleTransport = new Console()\nconst format = process.env.NODE_ENV === 'development' ? logFormatLocalDev : logFormatStructured\nconst transports: TransportStream[] = [consoleTransport]\nif (canGetDefaultRollbarTransport(process.env)) {\n try {\n const rollbarTransport = getDefaultRollbarTransport(process.env)\n transports.push(rollbarTransport)\n } catch {\n // NOTE: No error here, just gracefully adding logger if ENV VARs\n // were preset\n }\n}\n\nconst loggers: Record<WinstonVerbosity, Logger | undefined> = {\n debug: undefined,\n error: undefined,\n http: undefined,\n info: undefined,\n silly: undefined,\n verbose: undefined,\n warn: undefined,\n}\n\nexport const getLogger = (minVerbosity: LoggerVerbosity = 'info'): Logger => {\n const level = toWinstonVerbosity(minVerbosity)\n const existing = loggers[level]\n if (existing) return existing\n const logger = new WrappedWinstonLogger(\n createLogger({\n exitOnError,\n format,\n handleRejections,\n level,\n rejectionHandlers: transports,\n transports,\n }),\n )\n loggers[level] = logger\n return logger\n}\n","import { format } from 'winston'\n\nconst {\n colorize, combine, timestamp, printf,\n} = format\n\nexport const logFormatLocalDev = combine(\n colorize(),\n timestamp(),\n printf(info => `[${info.timestamp} ${info.level}] ${info.message}`),\n)\n","import { format } from 'winston'\n\nconst { simple } = format\n\nexport const logFormatRollbar = simple()\n","import { format } from 'winston'\n\nconst {\n combine, timestamp, json,\n} = format\n\nexport const logFormatStructured = combine(timestamp(), json())\n","import type { LoggerVerbosity } from './LoggerVerbosity.ts'\nimport type { WinstonVerbosity } from './WinstonVerbosity.ts'\n\nexport const toWinstonVerbosity = (loggerVerbosity: LoggerVerbosity): WinstonVerbosity => {\n return loggerVerbosity === 'all' ? 'silly' : loggerVerbosity\n}\n","export const canGetDefaultRollbarTransport = (env: { [key: string]: string | undefined }): boolean => {\n return env.ROLLBAR_ACCESS_TOKEN === undefined ? false : true\n}\n","import { assertEx } from '@xylabs/assert'\nimport Rollbar from 'rollbar'\n\nimport { RollbarTransport } from './RollbarTransport.ts'\n\nexport const getDefaultRollbarTransport = (env: { [key: string]: string | undefined }): RollbarTransport => {\n const accessToken = assertEx(env.ROLLBAR_ACCESS_TOKEN, () => 'Missing ROLLBAR_ACCESS_TOKEN ENV VAR')\n const rollbar: Rollbar = new Rollbar({ accessToken })\n return new RollbarTransport({}, rollbar)\n}\n","import type Rollbar from 'rollbar'\nimport type { TransportStreamOptions } from 'winston-transport'\nimport Transport from 'winston-transport'\n\nimport { logFormatRollbar } from '../../LogFormats/index.ts'\n\nexport class RollbarTransport extends Transport {\n protected readonly rollbar?: Rollbar\n constructor(\n opts: TransportStreamOptions,\n rollbar?: Rollbar,\n ) {\n super({\n ...opts, format: logFormatRollbar, level: 'error',\n })\n this.rollbar = rollbar\n }\n\n override log(info: { message?: string }, next: () => void) {\n this.rollbar?.error(info?.message)\n this.emit('logged', info?.message)\n next()\n }\n}\n","import type { LogFunction, Logger } from '@xylabs/logger'\nimport type { Logger as Winston } from 'winston'\n\n/**\n * Wrap Winston logger methods to adapt to familiar\n * console logging methods\n */\nexport class WrappedWinstonLogger implements Logger {\n protected readonly winston: Winston\n constructor(winston: Winston) {\n this.winston = winston\n }\n\n debug: LogFunction = message => this.winston.debug(message)\n error: LogFunction = message => this.winston.error(message)\n info: LogFunction = message => this.winston.info(message)\n log: LogFunction = message => this.winston.info(message)\n trace: LogFunction = message => this.winston.debug(message)\n warn: LogFunction = message => this.winston.warn(message)\n}\n","import type { Logger } from '@xylabs/logger'\n\nimport { getLogger } from './getLogger.ts'\nimport type { WrappedWinstonLogger } from './WrappedWinstonLogger.ts'\n\n/**\n * Static instance to prevent multiple instances of the same logger\n * with the same config\n */\n\ndeclare global {\n var xy: {\n defaultLogger?: WrappedWinstonLogger\n }\n}\n\nexport const getDefaultLogger = (): Logger => {\n if (globalThis.xy === undefined) globalThis.xy = {}\n if (globalThis.xy.defaultLogger) return globalThis.xy.defaultLogger\n return getLogger()\n}\n","import type { RequestHandler } from 'express'\nimport { ReasonPhrases, StatusCodes } from 'http-status-codes'\n\nexport const notImplemented: RequestHandler = (_req, _res, next) => {\n next({ message: ReasonPhrases.NOT_IMPLEMENTED, statusCode: StatusCodes.NOT_IMPLEMENTED })\n}\n","import { isDefined } from '@xylabs/typeof'\nimport type { Request } from 'express-serve-static-core'\n\n/**\n * Since there can be multiple of certain HTTP headers or\n * to prevent ugliness if someone did send us multiple\n * instances of a header we only expect one of, this\n * method grabs the 1st/only one of the desired header\n * @param header The header to find\n * @param req The received HTTP request (with headers)\n * @returns The first or only occurrence of the specified HTTP header\n */\nexport const getHttpHeader = (header: string, req: Request): string | undefined => {\n const headerValue = req.headers[header]\n const value\n // If the header exists\n = isDefined(headerValue)\n // If there's multiple of the same header\n ? Array.isArray(headerValue)\n // Grab the first one\n ? (headerValue as string[]).shift()\n // Otherwise grab the only one\n : (headerValue as string)\n // Otherwise undefined\n : undefined\n return value\n}\n","import type { Express } from 'express'\n\nconst setting = 'case sensitive routing'\n\n/**\n * Enable case sensitivity. When enabled, \"/Foo\" and \"/foo\" are different\n * routes. When disabled, \"/Foo\" and \"/foo\" are treated the same.\n * @param app The Express app to disable the header on.\n */\nexport const enableCaseSensitiveRouting = (app: Express) => {\n app.enable(setting)\n}\n\n/**\n * Disable case sensitivity. When enabled, \"/Foo\" and \"/foo\" are different\n * routes. When disabled, \"/Foo\" and \"/foo\" are treated the same.\n * @param app The Express app to disable the header on.\n */\nexport const disableCaseSensitiveRouting = (app: Express) => {\n app.disable(setting)\n}\n","import type {\n Express, NextFunction, Request, Response,\n} from 'express'\n\nconst header = 'X-Powered-By'\nconst setting = 'x-powered-by'\n\n/**\n * By default Express appends the `X-Powered-By: Express` header to\n * all responses. Calling this method enables that behavior.\n * @param app The Express app to disable the header on.\n */\nexport const enableExpressDefaultPoweredByHeader = (app: Express) => {\n app.enable(setting)\n}\n\n/**\n * By default Express appends the `X-Powered-By: Express` header to\n * all responses. Calling this method disables that behavior.\n * @param app The Express app to disable the header on.\n */\nexport const disableExpressDefaultPoweredByHeader = (app: Express) => {\n app.disable(setting)\n}\n\nexport const customPoweredByHeader = (req: Request, res: Response, next: NextFunction) => {\n res.setHeader(header, 'XYO')\n next()\n}\n","import type { OptionsJson } from 'body-parser'\nimport bodyParser from 'body-parser'\nimport type { NextHandleFunction } from 'connect'\n\nimport { getDefaultLogger } from '../../Logger/index.ts'\n\n/**\n * The default maximum request body size for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptionsLimit = '100kb'\n\n/**\n * The default MIME types for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptionsTypes = ['application/json', 'text/json']\n\n/**\n * The default options for the JSON Body Parser\n */\nexport const DefaultJsonBodyParserOptions: OptionsJson = {\n limit: DefaultJsonBodyParserOptionsLimit,\n type: DefaultJsonBodyParserOptionsTypes,\n}\n\n/**\n * Gets the default JSON Body Parser options merged with the supplied options\n * with the supplied options taking precedence\n * @param options The options to override the default JSON Body Parser options with\n * @returns The combined JSON Body Parser options with the supplied values taking\n * precedence over the default\n */\nexport const getJsonBodyParserOptions = (options?: Partial<OptionsJson>): OptionsJson => {\n return options ? { ...DefaultJsonBodyParserOptions, ...options } : DefaultJsonBodyParserOptions\n}\n\n/**\n * Get a JSON Body Parser connect middleware handler\n * @param options The options for the JSON Body Parser\n * @returns A middleware function that parses JSON bodies\n */\nexport const getJsonBodyParser = (options: OptionsJson = DefaultJsonBodyParserOptions): NextHandleFunction => {\n // Create closed instance of bodyParser to prevent instantiation of new instance on every request\n const parser = bodyParser.json(options)\n\n return (req, res, next) => {\n // If we do not trap this error, then it dumps too much to log, usually happens if request aborted\n try {\n parser(req, res, next)\n } catch (ex) {\n const error = ex as Error\n getDefaultLogger().log(`bodyParser failed [${error?.name}]: ${error?.message}`)\n }\n }\n}\n\n/**\n * A JSON Body Parser middleware handler initialized with the default options\n */\nexport const jsonBodyParser = getJsonBodyParser()\n","export class Counters {\n static counters: Record<string, number> = {}\n\n static inc(name: string, count = 1) {\n this.catchError(name, (name: string) => {\n this.counters[name] = (this.counters[name] ?? 0) + count\n })\n }\n\n static max(name: string, count: number) {\n this.catchError(name, (name: string) => {\n const currentValue = this.counters[name]\n if (currentValue === undefined || count > currentValue) {\n this.counters[name] = count\n }\n })\n }\n\n static min(name: string, count: number) {\n this.catchError(name, (name: string) => {\n const currentValue = this.counters[name]\n if (currentValue === undefined || count < currentValue) {\n this.counters[name] = count\n }\n })\n }\n\n private static catchError = (name: string, func: (name: string) => void) => {\n try {\n func(name)\n } catch {\n this.counters[name] = 0\n this.inc('CountersErrors')\n }\n }\n}\n","export class Profiler {\n stats: Record<string, number> = {}\n\n async profile<T>(name: string, promise: Promise<T>) {\n const start = Date.now()\n const result = await promise\n this.stats[name] = Date.now() - start\n return result\n }\n}\n","import type {\n Application, NextFunction, Request, Response,\n} from 'express'\n\nimport { Counters } from '../../Performance/index.ts'\n\nexport const useRequestCounters = (app: Application): void => {\n // Configure Global counters\n app.use((req: Request, res: Response, next: NextFunction) => {\n Counters.inc(req.path)\n Counters.inc('_calls')\n next()\n })\n\n app.get('/stats', (req: Request, res: Response, next: NextFunction) => {\n /* #swagger.tags = ['Metrics'] */\n /* #swagger.summary = 'Get the counters for single instance of diviner' */\n res.json({\n alive: true,\n avgTime: `${((Counters.counters._totalTime ?? 0) / (Counters.counters._calls ?? 1)).toFixed(2)}ms`,\n counters: Counters.counters,\n })\n next()\n })\n}\n","import type {\n NextFunction, Request, Response,\n} from 'express'\n\n/**\n * Connect middleware to enable profiling of response lifecycle timing. To effectively profile\n * the response timing, this middleware needs to be called first when initializing your Express\n * App\n * @example\n * const app = express()\n * app.use(responseProfiler)\n * // other initialization ...\n * @param _req The request\n * @param res The response\n * @param next The next function\n */\nexport const responseProfiler = (_req: Request, res: Response, next: NextFunction) => {\n if (!res.locals?.meta) {\n res.locals.meta = {}\n }\n res.locals.meta.profile = { startTime: Date.now() }\n next()\n}\n","import type { Response } from 'express'\n\nexport const getResponseMetadata = (res: Response): Record<string, unknown> => {\n const meta: Record<string, unknown> = res.locals?.meta || {}\n // NOTE: We should do this somewhere else to better separate concerns\n const profile = res.locals.meta?.profile\n if (profile) {\n const startTime = profile?.startTime\n if (startTime) {\n const endTime = Date.now()\n const duration = endTime - startTime\n res.locals.meta.profile = {\n duration, endTime, startTime,\n }\n }\n }\n return meta\n}\n","import { isError } from '@xylabs/typeof'\nimport type {\n NextFunction, Request, Response,\n} from 'express'\n\nimport { getDefaultLogger } from '../../Logger/index.ts'\nimport type { ExpressError } from '../../Model/index.ts'\nimport type { ApiError } from './jsonApi/index.ts'\n\nexport const standardErrors = (err: ExpressError | undefined, req: Request, res: Response, next: NextFunction) => {\n if (!isError(err)) {\n next(err)\n return\n }\n getDefaultLogger().error(err.message)\n err.statusCode = err.statusCode ?? 500\n\n const error: ApiError = {\n detail: err.message,\n status: `${err.statusCode}`,\n title: err.name,\n }\n\n res.status(err.statusCode).json(error)\n\n next(err)\n}\n","import type {\n Request, RequestHandler, Response,\n} from 'express'\nimport mung from 'express-mung'\n\nimport { getResponseMetadata } from './getResponseMetadata.js'\n\ninterface TransformResponseLocals {\n rawResponse?: boolean\n}\n\n/**\n * Flags the response to forgo the standard response envelope\n * and return the raw response body to the client\n * @param res The response to disable the standard response format on\n */\nexport const setRawResponseFormat = (res: Response): void => {\n res.locals.rawResponse = true\n}\n\n/**\n * Clears any flags on the response, allowing the response to\n * use the default standard response envelope\n * @param res The response to set to the standard response format\n */\nexport const clearRawResponseFormat = (res: Response): void => {\n res.locals.rawResponse = false\n}\n\n/**\n * Checks if there are any flags on the response that would cause it\n * to forgo the standard response envelope and return the raw response\n * body to the client\n * @param res\n * @returns True if there are any flags on the response, false otherwise\n */\nexport const isRawResponseFormatSet = (res: Response): boolean => {\n return res.locals.rawResponse ? true : false\n}\n\n/**\n * Transforms each response to conform to the standard response format (compatible with JSON API)\n * @param body The original request body\n * @param _req The request\n * @param res The response\n * @returns The transformed response body\n */\nconst transformResponse = (body: unknown, _req: Request, res: Response<unknown, TransformResponseLocals>) => {\n return isRawResponseFormatSet(res)\n ? body\n : (res.statusCode >= 200 && res.statusCode < 300)\n ? { data: body, meta: getResponseMetadata(res) }\n : { error: body, meta: getResponseMetadata(res) }\n}\n\n/**\n * Connect middleware to enable the transform of all responses to match\n * the standard response format (compatible with JSON API)\n */\n\nexport const standardResponses: RequestHandler = mung.json(transformResponse, { mungError: true })\n","export const compactObject = <T extends Record<string, unknown>>(obj: T) => {\n const result: Record<string, unknown> = {}\n for (const key in obj) {\n if (obj[key] !== undefined && obj[key] !== null) {\n result[key] = obj[key]\n }\n }\n return result as T\n}\n","import { isDefined } from '@xylabs/typeof'\n\nexport type ParseFunc<T = number> = (value: string) => T\n\n/**\n * @deprecated use zod instead\n */\nexport const tryParse = <T = number>(func: ParseFunc<T>, value?: string) => {\n try {\n const result = isDefined(value) ? func(value) : null\n if (!Number.isNaN(result) && result !== null) {\n return result\n }\n } catch {\n return\n }\n}\n\n/**\n * @deprecated use zod instead\n */\nexport const tryParseFloat = (value?: string) => {\n return tryParse(Number.parseFloat, value)\n}\n\n/**\n * @deprecated use zod instead\n */\nexport const tryParseInt = (value?: string) => {\n return tryParse(Number.parseInt, value)\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { isDefined, isPromise } from '@xylabs/typeof'\nimport type {\n NextFunction, Request, RequestHandler, Response,\n} from 'express'\nimport { ReasonPhrases, StatusCodes } from 'http-status-codes'\nimport { z, type ZodType } from 'zod'\n\nimport type { ExpressError } from '../Model/index.ts'\n\n/**\n * Empty Zod schema for requests with no parameters.\n */\nexport const EmptyParamsZod = z.object({}).catchall(z.string())\n\n/**\n * Empty Zod schema for requests with no query parameters.\n */\nexport const EmptyQueryParamsZod = z.object({}).catchall(z.union([z.string(), z.array(z.string())]))\n\n/**\n * Default validation schemas for request handler validator.\n */\nexport const ValidateRequestDefaults = {\n params: EmptyParamsZod,\n query: EmptyQueryParamsZod,\n body: z.json().optional(),\n response: z.json().optional(),\n}\n\ntype ValidatableRequestKey = 'params' | 'query' | 'body'\n\n/**\n * Factory for Express middleware that validates request and response objects using Zod schemas.\n * @param schemas The Zod schemas to use for validation.\n * @returns A middleware function for validating requests and responses.\n */\nexport function requestHandlerValidator<\n TParams extends typeof EmptyQueryParamsZod | ZodType<Record<string, string>> = typeof EmptyQueryParamsZod,\n TQuery extends typeof EmptyQueryParamsZod | ZodType<Record<string, string | string[]>> = typeof EmptyQueryParamsZod,\n TBody extends ZodType<unknown> = ZodType<unknown>,\n TResponse extends ZodType<unknown> = ZodType<unknown>,\n>(schemas?: Partial<{\n body: TBody\n params: TParams\n query: TQuery\n response: TResponse\n}>) {\n type Params = z.infer<TParams>\n type Query = z.infer<TQuery>\n type Body = z.infer<TBody>\n type Res = z.infer<TResponse>\n const validators = { ...ValidateRequestDefaults, ...schemas }\n\n return (handler: (req: Request<Params, Res, Body, Query>, res: Response<Res>, next: NextFunction) => unknown): RequestHandler => {\n return async (req: Request, res: Response, next: NextFunction) => {\n const originalJson = res.json.bind(res)\n try {\n // Validate incoming request\n const errors: string[] = []\n const keys: ValidatableRequestKey[] = ['params', 'query', 'body']\n for (const key of keys) {\n const validator = validators[key]\n const result = validator.safeParse(req[key])\n if (result.success) {\n if (isDefined(result.data)) Object.assign(req[key], result.data)\n } else {\n errors.push(\n ...result.error.issues.map(\n issue => (issue.path.length === 0)\n ? `${key}: ${issue.message}`\n : `${key}.${issue.path.join('.')}: ${issue.message}`,\n ),\n )\n }\n }\n\n // If there were validation errors, short-circuit and return Bad Request\n if (errors.length > 0) {\n const message = errors.join('; ')\n const err: ExpressError = new Error(message)\n err.name = ReasonPhrases.BAD_REQUEST\n err.statusCode = StatusCodes.BAD_REQUEST\n next(err)\n return false\n }\n\n // Wrap res.json to validate outgoing response\n res.json = (data: any) => {\n const result = validators.response.safeParse(data)\n if (result.success) {\n return originalJson(result.data)\n } else {\n const message = result.error.issues.map(\n issue => (issue.path.length === 0)\n ? `response: ${issue.message}`\n : `response.${issue.path.join('.')}: ${issue.message}`,\n ).join('; ')\n const err: ExpressError = new Error(message)\n err.name = ReasonPhrases.INTERNAL_SERVER_ERROR\n err.statusCode = StatusCodes.INTERNAL_SERVER_ERROR\n\n // Restore original json function in case the error handler wants to use it\n res.json = originalJson\n throw err\n }\n }\n\n // Automatically handle async errors\n const result = handler(req as any, res as any, next)\n if (result && isPromise(result)) {\n await result\n }\n } catch (err) {\n res.json = originalJson\n next(err)\n }\n }\n }\n}\n"],"mappings":"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","names":["format","format","combine","timestamp","format","header","setting","name","isError","isError","isDefined","isDefined","ReasonPhrases","StatusCodes","result"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xylabs/express",
3
- "version": "5.0.22",
3
+ "version": "5.0.23",
4
4
  "description": "SDK for base code for Api repos that use express and deploy on AWS ECS",
5
5
  "keywords": [
6
6
  "xylabs",
@@ -52,15 +52,16 @@
52
52
  "dependencies": {
53
53
  "@types/connect": "~3.4.38",
54
54
  "@types/express-serve-static-core": "~5.1.0",
55
- "@xylabs/assert": "~5.0.22",
56
- "@xylabs/logger": "~5.0.22",
57
- "@xylabs/typeof": "~5.0.22",
55
+ "@xylabs/assert": "~5.0.23",
56
+ "@xylabs/logger": "~5.0.23",
57
+ "@xylabs/typeof": "~5.0.23",
58
58
  "body-parser": "~2.2.0",
59
59
  "express-mung": "~0.5.1",
60
60
  "http-status-codes": "~2.3.0",
61
61
  "rollbar": "~2.26.5",
62
62
  "winston": "~3.18.3",
63
- "winston-transport": "~4.9.0"
63
+ "winston-transport": "~4.9.0",
64
+ "zod": "^4.1.12"
64
65
  },
65
66
  "devDependencies": {
66
67
  "@types/body-parser": "~1.19.6",
@@ -69,7 +70,7 @@
69
70
  "@types/node": "~24.10.0",
70
71
  "@xylabs/ts-scripts-yarn3": "~7.2.8",
71
72
  "@xylabs/tsconfig": "~7.2.8",
72
- "@xylabs/vitest-extended": "~5.0.22",
73
+ "@xylabs/vitest-extended": "~5.0.23",
73
74
  "body-parser": "~2.2.0",
74
75
  "express": "~5.1.0",
75
76
  "express-mung": "~0.5.1",
@@ -0,0 +1,18 @@
1
+ import type { RequestHandler } from 'express'
2
+
3
+ export type HttpMethod
4
+ = | 'get'
5
+ | 'post'
6
+ | 'put'
7
+ | 'patch'
8
+ | 'delete'
9
+ | 'options'
10
+ | 'head'
11
+
12
+ export interface RouteDefinition<
13
+ H extends RequestHandler = RequestHandler,
14
+ > {
15
+ handlers: H[] | H
16
+ method: HttpMethod
17
+ path: string | RegExp
18
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './asyncHandler.ts'
2
2
  export * from './errorToJsonHandler.ts'
3
+ export * from './RouteDefinition.ts'
3
4
  export * from './StatusCodeHandlers/index.ts'
@@ -2,6 +2,9 @@ import { isDefined } from '@xylabs/typeof'
2
2
 
3
3
  export type ParseFunc<T = number> = (value: string) => T
4
4
 
5
+ /**
6
+ * @deprecated use zod instead
7
+ */
5
8
  export const tryParse = <T = number>(func: ParseFunc<T>, value?: string) => {
6
9
  try {
7
10
  const result = isDefined(value) ? func(value) : null
@@ -13,10 +16,16 @@ export const tryParse = <T = number>(func: ParseFunc<T>, value?: string) => {
13
16
  }
14
17
  }
15
18
 
19
+ /**
20
+ * @deprecated use zod instead
21
+ */
16
22
  export const tryParseFloat = (value?: string) => {
17
23
  return tryParse(Number.parseFloat, value)
18
24
  }
19
25
 
26
+ /**
27
+ * @deprecated use zod instead
28
+ */
20
29
  export const tryParseInt = (value?: string) => {
21
30
  return tryParse(Number.parseInt, value)
22
31
  }
@@ -0,0 +1 @@
1
+ export * from './requestHandlerValidator.ts'
@@ -0,0 +1,120 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { isDefined, isPromise } from '@xylabs/typeof'
3
+ import type {
4
+ NextFunction, Request, RequestHandler, Response,
5
+ } from 'express'
6
+ import { ReasonPhrases, StatusCodes } from 'http-status-codes'
7
+ import { z, type ZodType } from 'zod'
8
+
9
+ import type { ExpressError } from '../Model/index.ts'
10
+
11
+ /**
12
+ * Empty Zod schema for requests with no parameters.
13
+ */
14
+ export const EmptyParamsZod = z.object({}).catchall(z.string())
15
+
16
+ /**
17
+ * Empty Zod schema for requests with no query parameters.
18
+ */
19
+ export const EmptyQueryParamsZod = z.object({}).catchall(z.union([z.string(), z.array(z.string())]))
20
+
21
+ /**
22
+ * Default validation schemas for request handler validator.
23
+ */
24
+ export const ValidateRequestDefaults = {
25
+ params: EmptyParamsZod,
26
+ query: EmptyQueryParamsZod,
27
+ body: z.json().optional(),
28
+ response: z.json().optional(),
29
+ }
30
+
31
+ type ValidatableRequestKey = 'params' | 'query' | 'body'
32
+
33
+ /**
34
+ * Factory for Express middleware that validates request and response objects using Zod schemas.
35
+ * @param schemas The Zod schemas to use for validation.
36
+ * @returns A middleware function for validating requests and responses.
37
+ */
38
+ export function requestHandlerValidator<
39
+ TParams extends typeof EmptyQueryParamsZod | ZodType<Record<string, string>> = typeof EmptyQueryParamsZod,
40
+ TQuery extends typeof EmptyQueryParamsZod | ZodType<Record<string, string | string[]>> = typeof EmptyQueryParamsZod,
41
+ TBody extends ZodType<unknown> = ZodType<unknown>,
42
+ TResponse extends ZodType<unknown> = ZodType<unknown>,
43
+ >(schemas?: Partial<{
44
+ body: TBody
45
+ params: TParams
46
+ query: TQuery
47
+ response: TResponse
48
+ }>) {
49
+ type Params = z.infer<TParams>
50
+ type Query = z.infer<TQuery>
51
+ type Body = z.infer<TBody>
52
+ type Res = z.infer<TResponse>
53
+ const validators = { ...ValidateRequestDefaults, ...schemas }
54
+
55
+ return (handler: (req: Request<Params, Res, Body, Query>, res: Response<Res>, next: NextFunction) => unknown): RequestHandler => {
56
+ return async (req: Request, res: Response, next: NextFunction) => {
57
+ const originalJson = res.json.bind(res)
58
+ try {
59
+ // Validate incoming request
60
+ const errors: string[] = []
61
+ const keys: ValidatableRequestKey[] = ['params', 'query', 'body']
62
+ for (const key of keys) {
63
+ const validator = validators[key]
64
+ const result = validator.safeParse(req[key])
65
+ if (result.success) {
66
+ if (isDefined(result.data)) Object.assign(req[key], result.data)
67
+ } else {
68
+ errors.push(
69
+ ...result.error.issues.map(
70
+ issue => (issue.path.length === 0)
71
+ ? `${key}: ${issue.message}`
72
+ : `${key}.${issue.path.join('.')}: ${issue.message}`,
73
+ ),
74
+ )
75
+ }
76
+ }
77
+
78
+ // If there were validation errors, short-circuit and return Bad Request
79
+ if (errors.length > 0) {
80
+ const message = errors.join('; ')
81
+ const err: ExpressError = new Error(message)
82
+ err.name = ReasonPhrases.BAD_REQUEST
83
+ err.statusCode = StatusCodes.BAD_REQUEST
84
+ next(err)
85
+ return false
86
+ }
87
+
88
+ // Wrap res.json to validate outgoing response
89
+ res.json = (data: any) => {
90
+ const result = validators.response.safeParse(data)
91
+ if (result.success) {
92
+ return originalJson(result.data)
93
+ } else {
94
+ const message = result.error.issues.map(
95
+ issue => (issue.path.length === 0)
96
+ ? `response: ${issue.message}`
97
+ : `response.${issue.path.join('.')}: ${issue.message}`,
98
+ ).join('; ')
99
+ const err: ExpressError = new Error(message)
100
+ err.name = ReasonPhrases.INTERNAL_SERVER_ERROR
101
+ err.statusCode = StatusCodes.INTERNAL_SERVER_ERROR
102
+
103
+ // Restore original json function in case the error handler wants to use it
104
+ res.json = originalJson
105
+ throw err
106
+ }
107
+ }
108
+
109
+ // Automatically handle async errors
110
+ const result = handler(req as any, res as any, next)
111
+ if (result && isPromise(result)) {
112
+ await result
113
+ }
114
+ } catch (err) {
115
+ res.json = originalJson
116
+ next(err)
117
+ }
118
+ }
119
+ }
120
+ }
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from './middleware/index.ts'
5
5
  export * from './Model/index.ts'
6
6
  export * from './Performance/index.ts'
7
7
  export * from './Util/index.ts'
8
+ export * from './Validation/index.ts'