@extk/expressive 0.4.4 → 0.5.2

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.
package/README.md CHANGED
@@ -40,17 +40,18 @@ npm install @extk/expressive express
40
40
 
41
41
  ```ts
42
42
  import express from 'express';
43
- import { bootstrap, getDefaultFileLogger, ApiResponse, NotFoundError, SWG } from '@extk/expressive';
43
+ import { bootstrap, ApiResponse, NotFoundError, SWG } from '@extk/expressive';
44
44
 
45
- // 1. Bootstrap with a logger
45
+ // 1. Bootstrap with a logger (bring your own)
46
46
  const {
47
47
  expressiveServer,
48
48
  expressiveRouter,
49
49
  swaggerBuilder,
50
50
  notFoundMiddleware,
51
51
  getErrorHandlerMiddleware,
52
+ silently,
52
53
  } = bootstrap({
53
- logger: getDefaultFileLogger('my-api'),
54
+ logger: console, // any object with info/warn/error/debug
54
55
  });
55
56
 
56
57
  // 2. Configure swagger metadata
@@ -246,16 +247,56 @@ addRoute({
246
247
 
247
248
  This way your Zod schemas serve as the single source of truth for both runtime validation and API documentation.
248
249
 
250
+ ## silently
251
+
252
+ `silently` runs a function — sync or async — and suppresses any errors it throws. Errors are forwarded to `alertHandler` (if configured) or logged via the container logger.
253
+
254
+ ```ts
255
+ // fire-and-forget without crashing the process
256
+ silently(() => sendAnalyticsEvent(req));
257
+ silently(async () => await notifySlack('Server started'));
258
+ ```
259
+
249
260
  ## Logging
250
261
 
251
- Winston-based logging with daily rotating files in production and console output in development.
262
+ Expressive does not bundle a logger. Instead, `bootstrap` accepts any object that satisfies the `Logger` interface:
252
263
 
253
264
  ```ts
254
- import { getDefaultFileLogger, getDefaultConsoleLogger } from '@extk/expressive';
265
+ export type Logger = {
266
+ info(message: string, ...args: any[]): void;
267
+ error(message: string | Error | unknown, ...args: any[]): void;
268
+ warn(message: string, ...args: any[]): void;
269
+ debug(message: string, ...args: any[]): void;
270
+ };
271
+ ```
255
272
 
256
- const logger = getDefaultFileLogger('my-service'); // logs to ./logs/my-service-YYYY-MM-DD.log
257
- logger.info('Server started on port %d', 3000);
258
- logger.error('Something went wrong: %s', err.message);
273
+ This means you can pass `console` directly, or plug in any logging library (Winston, Pino, etc.):
274
+
275
+ ```ts
276
+ bootstrap({ logger: console });
277
+ ```
278
+
279
+ The `@extk/logger-cloudwatch` package from the same org is a drop-in fit:
280
+
281
+ ```ts
282
+ import { getCloudwatchLogger, getConsoleLogger } from '@extk/logger-cloudwatch';
283
+
284
+ // development
285
+ bootstrap({ logger: getConsoleLogger() });
286
+
287
+ // production — streams structured JSON logs to AWS CloudWatch
288
+ bootstrap({
289
+ logger: getCloudwatchLogger({
290
+ aws: {
291
+ region: 'us-east-1',
292
+ logGroup: '/my-app/production',
293
+ credentials: {
294
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
295
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
296
+ },
297
+ },
298
+ }),
299
+ });
259
300
  ```
260
301
 
261
302
  ## Utilities
package/dist/index.d.mts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as express from 'express';
2
- import express__default, { RequestHandler, Request } from 'express';
2
+ import express__default, { RequestHandler } from 'express';
3
3
  import * as express_serve_static_core from 'express-serve-static-core';
4
4
  import { RouteParameters } from 'express-serve-static-core';
5
- import winston, { Logger as Logger$1 } from 'winston';
6
5
  import { HelmetOptions } from 'helmet';
7
6
  import morgan from 'morgan';
8
7
 
@@ -17,19 +16,18 @@ type Pagination = {
17
16
  limit: number;
18
17
  offset: number;
19
18
  };
20
- type ReqSnapshot = {
21
- readonly query: qs.ParsedQs;
22
- readonly path: string;
23
- readonly method: string;
24
- readonly userId: string | number | undefined;
25
- };
26
19
 
27
20
  type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'trace';
28
21
  type OtherString = string & {};
29
22
  type OtherUnknown = unknown & {};
30
23
  type ContentType = 'application/json' | 'application/xml' | 'text/plain' | 'text/html' | OtherString;
31
- type AlertHandler = (err: Error & Record<string, any>, reqSnapshot?: ReqSnapshot) => void | Promise<void>;
32
- type Logger = Logger$1;
24
+ type AlertHandler = (err: Error & Record<string, any>, context?: string) => void | Promise<void>;
25
+ type Logger = {
26
+ info(message: string, ...args: any[]): void;
27
+ error(message: string | Error | unknown, ...args: any[]): void;
28
+ warn(message: string, ...args: any[]): void;
29
+ debug(message: string, ...args: any[]): void;
30
+ };
33
31
  type Container = {
34
32
  logger: Logger;
35
33
  alertHandler?: AlertHandler;
@@ -263,21 +261,12 @@ declare function slugify(text: string): string;
263
261
  declare function getTmpDir(): string;
264
262
  declare function getTmpPath(...steps: string[]): string;
265
263
  declare function parseDefaultPagination(query: PaginationQuery): Pagination;
266
- declare function createReqSnapshot(req: Request & {
267
- user?: {
268
- id?: string | number;
269
- };
270
- }): ReqSnapshot;
271
264
 
272
265
  declare function isDev(): boolean;
273
266
  declare function isProd(): boolean;
274
267
  declare function getEnvVar(configName: string): string;
275
268
  type Env = 'dev' | 'prod' | OtherString;
276
269
 
277
- declare const createLogger: (options?: winston.LoggerOptions) => winston.Logger;
278
- declare const getDefaultFileLogger: (name?: string) => winston.Logger;
279
- declare const getDefaultConsoleLogger: (name?: string) => winston.Logger;
280
-
281
270
  declare class ApiErrorResponse<T = undefined> {
282
271
  readonly status = "error";
283
272
  readonly message: string;
@@ -294,7 +283,7 @@ declare class ApiResponse<T = undefined> {
294
283
 
295
284
  declare function bootstrap(container: Container): {
296
285
  swaggerBuilder: () => SwaggerBuilder;
297
- silently: (fn: () => Promise<void>, reqSnapshot?: ReqSnapshot) => void;
286
+ silently: (fn: () => Promise<void> | void) => Promise<void>;
298
287
  notFoundMiddleware: (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
299
288
  getErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
300
289
  expressiveServer: (configs?: {
@@ -321,4 +310,4 @@ declare function bootstrap(container: Container): {
321
310
  };
322
311
  };
323
312
 
324
- export { type AlertHandler, ApiError, ApiErrorResponse, ApiResponse, type AuthMethod, BadRequestError, type Container, type Content, type ContentType, DuplicateError, type Env, type ExpressHandler, type ExpressLocalsObj, type ExpressRoute, FileTooBigError, ForbiddenError, type HttpMethod, InternalError, InvalidCredentialsError, InvalidFileTypeError, type Logger, NotFoundError, type OtherString, type OtherUnknown, type Pagination, type PaginationQuery, type Param, type PathItem, type ReqSnapshot, SWG, type Schema, SchemaValidationError, type SecurityScheme, type Servers, type SwaggerConfig, TokenExpiredError, TooManyRequestsError, UserUnauthorizedError, bootstrap, createLogger, createReqSnapshot, getDefaultConsoleLogger, getDefaultFileLogger, getEnvVar, getTmpDir, getTmpPath, isDev, isProd, parseDefaultPagination, parseIdOrFail, parsePositiveInteger, slugify };
313
+ export { type AlertHandler, ApiError, ApiErrorResponse, ApiResponse, type AuthMethod, BadRequestError, type Container, type Content, type ContentType, DuplicateError, type Env, type ExpressHandler, type ExpressLocalsObj, type ExpressRoute, FileTooBigError, ForbiddenError, type HttpMethod, InternalError, InvalidCredentialsError, InvalidFileTypeError, type Logger, NotFoundError, type OtherString, type OtherUnknown, type Pagination, type PaginationQuery, type Param, type PathItem, SWG, type Schema, SchemaValidationError, type SecurityScheme, type Servers, type SwaggerConfig, TokenExpiredError, TooManyRequestsError, UserUnauthorizedError, bootstrap, getEnvVar, getTmpDir, getTmpPath, isDev, isProd, parseDefaultPagination, parseIdOrFail, parsePositiveInteger, slugify };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import * as express from 'express';
2
- import express__default, { RequestHandler, Request } from 'express';
2
+ import express__default, { RequestHandler } from 'express';
3
3
  import * as express_serve_static_core from 'express-serve-static-core';
4
4
  import { RouteParameters } from 'express-serve-static-core';
5
- import winston, { Logger as Logger$1 } from 'winston';
6
5
  import { HelmetOptions } from 'helmet';
7
6
  import morgan from 'morgan';
8
7
 
@@ -17,19 +16,18 @@ type Pagination = {
17
16
  limit: number;
18
17
  offset: number;
19
18
  };
20
- type ReqSnapshot = {
21
- readonly query: qs.ParsedQs;
22
- readonly path: string;
23
- readonly method: string;
24
- readonly userId: string | number | undefined;
25
- };
26
19
 
27
20
  type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'trace';
28
21
  type OtherString = string & {};
29
22
  type OtherUnknown = unknown & {};
30
23
  type ContentType = 'application/json' | 'application/xml' | 'text/plain' | 'text/html' | OtherString;
31
- type AlertHandler = (err: Error & Record<string, any>, reqSnapshot?: ReqSnapshot) => void | Promise<void>;
32
- type Logger = Logger$1;
24
+ type AlertHandler = (err: Error & Record<string, any>, context?: string) => void | Promise<void>;
25
+ type Logger = {
26
+ info(message: string, ...args: any[]): void;
27
+ error(message: string | Error | unknown, ...args: any[]): void;
28
+ warn(message: string, ...args: any[]): void;
29
+ debug(message: string, ...args: any[]): void;
30
+ };
33
31
  type Container = {
34
32
  logger: Logger;
35
33
  alertHandler?: AlertHandler;
@@ -263,21 +261,12 @@ declare function slugify(text: string): string;
263
261
  declare function getTmpDir(): string;
264
262
  declare function getTmpPath(...steps: string[]): string;
265
263
  declare function parseDefaultPagination(query: PaginationQuery): Pagination;
266
- declare function createReqSnapshot(req: Request & {
267
- user?: {
268
- id?: string | number;
269
- };
270
- }): ReqSnapshot;
271
264
 
272
265
  declare function isDev(): boolean;
273
266
  declare function isProd(): boolean;
274
267
  declare function getEnvVar(configName: string): string;
275
268
  type Env = 'dev' | 'prod' | OtherString;
276
269
 
277
- declare const createLogger: (options?: winston.LoggerOptions) => winston.Logger;
278
- declare const getDefaultFileLogger: (name?: string) => winston.Logger;
279
- declare const getDefaultConsoleLogger: (name?: string) => winston.Logger;
280
-
281
270
  declare class ApiErrorResponse<T = undefined> {
282
271
  readonly status = "error";
283
272
  readonly message: string;
@@ -294,7 +283,7 @@ declare class ApiResponse<T = undefined> {
294
283
 
295
284
  declare function bootstrap(container: Container): {
296
285
  swaggerBuilder: () => SwaggerBuilder;
297
- silently: (fn: () => Promise<void>, reqSnapshot?: ReqSnapshot) => void;
286
+ silently: (fn: () => Promise<void> | void) => Promise<void>;
298
287
  notFoundMiddleware: (_req: express.Request, res: express.Response, _next: express.NextFunction) => void;
299
288
  getErrorHandlerMiddleware: (errorMapper?: (err: Error & Record<string, unknown>) => ApiError | null | undefined) => (err: Error & Record<string, unknown>, req: express.Request, res: express.Response, _next: express.NextFunction) => Promise<void>;
300
289
  expressiveServer: (configs?: {
@@ -321,4 +310,4 @@ declare function bootstrap(container: Container): {
321
310
  };
322
311
  };
323
312
 
324
- export { type AlertHandler, ApiError, ApiErrorResponse, ApiResponse, type AuthMethod, BadRequestError, type Container, type Content, type ContentType, DuplicateError, type Env, type ExpressHandler, type ExpressLocalsObj, type ExpressRoute, FileTooBigError, ForbiddenError, type HttpMethod, InternalError, InvalidCredentialsError, InvalidFileTypeError, type Logger, NotFoundError, type OtherString, type OtherUnknown, type Pagination, type PaginationQuery, type Param, type PathItem, type ReqSnapshot, SWG, type Schema, SchemaValidationError, type SecurityScheme, type Servers, type SwaggerConfig, TokenExpiredError, TooManyRequestsError, UserUnauthorizedError, bootstrap, createLogger, createReqSnapshot, getDefaultConsoleLogger, getDefaultFileLogger, getEnvVar, getTmpDir, getTmpPath, isDev, isProd, parseDefaultPagination, parseIdOrFail, parsePositiveInteger, slugify };
313
+ export { type AlertHandler, ApiError, ApiErrorResponse, ApiResponse, type AuthMethod, BadRequestError, type Container, type Content, type ContentType, DuplicateError, type Env, type ExpressHandler, type ExpressLocalsObj, type ExpressRoute, FileTooBigError, ForbiddenError, type HttpMethod, InternalError, InvalidCredentialsError, InvalidFileTypeError, type Logger, NotFoundError, type OtherString, type OtherUnknown, type Pagination, type PaginationQuery, type Param, type PathItem, SWG, type Schema, SchemaValidationError, type SecurityScheme, type Servers, type SwaggerConfig, TokenExpiredError, TooManyRequestsError, UserUnauthorizedError, bootstrap, getEnvVar, getTmpDir, getTmpPath, isDev, isProd, parseDefaultPagination, parseIdOrFail, parsePositiveInteger, slugify };
package/dist/index.js CHANGED
@@ -47,10 +47,6 @@ __export(index_exports, {
47
47
  TooManyRequestsError: () => TooManyRequestsError,
48
48
  UserUnauthorizedError: () => UserUnauthorizedError,
49
49
  bootstrap: () => bootstrap,
50
- createLogger: () => createLogger,
51
- createReqSnapshot: () => createReqSnapshot,
52
- getDefaultConsoleLogger: () => getDefaultConsoleLogger,
53
- getDefaultFileLogger: () => getDefaultFileLogger,
54
50
  getEnvVar: () => getEnvVar,
55
51
  getTmpDir: () => getTmpDir,
56
52
  getTmpPath: () => getTmpPath,
@@ -248,8 +244,16 @@ function buildExpressive(container, swaggerDoc) {
248
244
  const route = pathOverride ?? convertExpressPath(context.path);
249
245
  const pathItem = {
250
246
  // -- defaults --
251
- responses: {},
252
- // has to be defined or else responses are not documented... ¯\_(ツ)_/¯
247
+ responses: {
248
+ // has to be defined or else responses are not documented... ¯\_(ツ)_/¯
249
+ "200": { description: "OK" },
250
+ "201": { description: "Created" },
251
+ "204": { description: "No Content" },
252
+ "400": { description: "Bad Request" },
253
+ "401": { description: "User Unauthorized" },
254
+ "403": { description: "Forbidden" },
255
+ "500": { description: "Internal Server Error" }
256
+ },
253
257
  // -- group defaults --
254
258
  ...configs?.oapi || {},
255
259
  // -- overrides --
@@ -276,8 +280,22 @@ function buildExpressive(container, swaggerDoc) {
276
280
  };
277
281
  }
278
282
 
279
- // src/common.ts
280
- var import_path = __toESM(require("path"));
283
+ // src/env.ts
284
+ var import_dotenv = __toESM(require("dotenv"));
285
+ import_dotenv.default.config();
286
+ function isDev() {
287
+ return getEnvVar("ENV") === "dev";
288
+ }
289
+ function isProd() {
290
+ return getEnvVar("ENV") === "prod";
291
+ }
292
+ function getEnvVar(configName) {
293
+ const config = process.env[configName];
294
+ if (!config) {
295
+ throw new Error(`Missing config '${configName}'`);
296
+ }
297
+ return config;
298
+ }
281
299
 
282
300
  // src/errors.ts
283
301
  var ApiError = class extends Error {
@@ -356,59 +374,6 @@ var UserUnauthorizedError = class extends ApiError {
356
374
  }
357
375
  };
358
376
 
359
- // src/common.ts
360
- function parsePositiveInteger(v, defaultValue, max) {
361
- const value = Number(v);
362
- return Number.isInteger(value) && value > 0 && (!max || value <= max) ? value : defaultValue;
363
- }
364
- function parseIdOrFail(v) {
365
- const value = Number(v);
366
- if (!Number.isInteger(value) || value <= 0) {
367
- throw new ApiError("Invalid Id", 400, "INVALID_ID");
368
- }
369
- return value;
370
- }
371
- function slugify(text) {
372
- return text.toString().normalize("NFKD").toLowerCase().trim().replace(/[\s\n_+.]/g, "-").replace(/--+/g, "-");
373
- }
374
- function getTmpDir() {
375
- return import_path.default.resolve("tmp");
376
- }
377
- function getTmpPath(...steps) {
378
- return import_path.default.join(getTmpDir(), ...steps);
379
- }
380
- function parseDefaultPagination(query) {
381
- const limit = parsePositiveInteger(query.limit, 50, 100);
382
- const page = parsePositiveInteger(query.page, 1, 1e3);
383
- const offset = (page - 1) * limit;
384
- return { limit, offset };
385
- }
386
- function createReqSnapshot(req) {
387
- return {
388
- query: req.query,
389
- path: req.path,
390
- method: req.method,
391
- userId: req?.user?.id
392
- };
393
- }
394
-
395
- // src/env.ts
396
- var import_dotenv = __toESM(require("dotenv"));
397
- import_dotenv.default.config();
398
- function isDev() {
399
- return getEnvVar("ENV") === "dev";
400
- }
401
- function isProd() {
402
- return getEnvVar("ENV") === "prod";
403
- }
404
- function getEnvVar(configName) {
405
- const config = process.env[configName];
406
- if (!config) {
407
- throw new Error(`Missing config '${configName}'`);
408
- }
409
- return config;
410
- }
411
-
412
377
  // src/response/ApiErrorResponse.ts
413
378
  var ApiErrorResponse = class {
414
379
  status = "error";
@@ -448,7 +413,7 @@ var buildMiddleware = (container) => {
448
413
  logger.error("Cause: %s", err.cause);
449
414
  }
450
415
  if (alertHandler) {
451
- alertHandler(err, createReqSnapshot(req));
416
+ alertHandler(err);
452
417
  }
453
418
  finalError = new InternalError();
454
419
  if (!isProd()) {
@@ -461,63 +426,34 @@ var buildMiddleware = (container) => {
461
426
  };
462
427
  };
463
428
 
464
- // src/logger.ts
465
- var import_winston = __toESM(require("winston"));
466
- var import_winston_daily_rotate_file = __toESM(require("winston-daily-rotate-file"));
467
- var loggerRegistry = {};
468
- var defaultFormat = import_winston.default.format.combine(
469
- import_winston.default.format.timestamp({ format: "DD/MM/YYYY HH:mm:ss" }),
470
- import_winston.default.format.splat(),
471
- // String interpolation splat for %d %s-style messages.
472
- import_winston.default.format.errors({ stack: true }),
473
- import_winston.default.format.printf(({ level, message, timestamp, stack }) => `${timestamp}[${level.toUpperCase()}]: ${stack || message}`)
474
- );
475
- var consoleTransport = new import_winston.default.transports.Console({
476
- handleExceptions: true
477
- });
478
- var createFileLogger = (filename) => {
479
- const logger = import_winston.default.createLogger({
480
- format: defaultFormat,
481
- transports: [
482
- new import_winston_daily_rotate_file.default({
483
- level: "debug",
484
- filename: `./logs/${filename}-%DATE%.log`,
485
- handleExceptions: true,
486
- // exitOnError: false, // do not exit on handled exceptions
487
- datePattern: "YYYY-MM-DD",
488
- zippedArchive: true,
489
- auditFile: "./logs/audit.json",
490
- maxSize: "20m",
491
- maxFiles: isProd() ? "30d" : "1d"
492
- // format: defaultFormat,
493
- // the nested 'format' field causes issues with logging errors;
494
- // use the 'format' field on logger, instead of transport;
495
- })
496
- ]
497
- });
498
- if (isDev()) {
499
- logger.add(consoleTransport);
500
- }
501
- return logger;
502
- };
503
- var createLogger = import_winston.default.createLogger;
504
- var getDefaultFileLogger = (name = "app") => {
505
- if (!loggerRegistry[name]) {
506
- loggerRegistry[name] = createFileLogger(name.toString());
507
- }
508
- return loggerRegistry[name];
509
- };
510
- var getDefaultConsoleLogger = (name = "console") => {
511
- if (!loggerRegistry[name]) {
512
- loggerRegistry[name] = import_winston.default.createLogger({
513
- format: defaultFormat,
514
- transports: [
515
- consoleTransport
516
- ]
517
- });
429
+ // src/common.ts
430
+ var import_path = __toESM(require("path"));
431
+ function parsePositiveInteger(v, defaultValue, max) {
432
+ const value = Number(v);
433
+ return Number.isInteger(value) && value > 0 && (!max || value <= max) ? value : defaultValue;
434
+ }
435
+ function parseIdOrFail(v) {
436
+ const value = Number(v);
437
+ if (!Number.isInteger(value) || value <= 0) {
438
+ throw new ApiError("Invalid Id", 400, "INVALID_ID");
518
439
  }
519
- return loggerRegistry[name];
520
- };
440
+ return value;
441
+ }
442
+ function slugify(text) {
443
+ return text.toString().normalize("NFKD").toLowerCase().trim().replace(/[\s\n_+.]/g, "-").replace(/--+/g, "-");
444
+ }
445
+ function getTmpDir() {
446
+ return import_path.default.resolve("tmp");
447
+ }
448
+ function getTmpPath(...steps) {
449
+ return import_path.default.join(getTmpDir(), ...steps);
450
+ }
451
+ function parseDefaultPagination(query) {
452
+ const limit = parsePositiveInteger(query.limit, 50, 100);
453
+ const page = parsePositiveInteger(query.page, 1, 1e3);
454
+ const offset = (page - 1) * limit;
455
+ return { limit, offset };
456
+ }
521
457
 
522
458
  // src/response/ApiResponse.ts
523
459
  var ApiResponse = class {
@@ -541,14 +477,16 @@ function bootstrap(container) {
541
477
  ...buildExpressive(container, swaggerDoc),
542
478
  ...buildMiddleware(container),
543
479
  swaggerBuilder: () => new SwaggerBuilder(swaggerDoc),
544
- silently: (fn, reqSnapshot) => {
545
- fn().catch((e) => {
480
+ silently: async (fn) => {
481
+ try {
482
+ await fn();
483
+ } catch (e) {
546
484
  if (container.alertHandler && e instanceof Error) {
547
- container.alertHandler(e, reqSnapshot);
485
+ container.alertHandler(e);
548
486
  } else {
549
487
  container.logger.error(e);
550
488
  }
551
- });
489
+ }
552
490
  }
553
491
  };
554
492
  }
@@ -571,10 +509,6 @@ function bootstrap(container) {
571
509
  TooManyRequestsError,
572
510
  UserUnauthorizedError,
573
511
  bootstrap,
574
- createLogger,
575
- createReqSnapshot,
576
- getDefaultConsoleLogger,
577
- getDefaultFileLogger,
578
512
  getEnvVar,
579
513
  getTmpDir,
580
514
  getTmpPath,
package/dist/index.mjs CHANGED
@@ -183,8 +183,16 @@ function buildExpressive(container, swaggerDoc) {
183
183
  const route = pathOverride ?? convertExpressPath(context.path);
184
184
  const pathItem = {
185
185
  // -- defaults --
186
- responses: {},
187
- // has to be defined or else responses are not documented... ¯\_(ツ)_/¯
186
+ responses: {
187
+ // has to be defined or else responses are not documented... ¯\_(ツ)_/¯
188
+ "200": { description: "OK" },
189
+ "201": { description: "Created" },
190
+ "204": { description: "No Content" },
191
+ "400": { description: "Bad Request" },
192
+ "401": { description: "User Unauthorized" },
193
+ "403": { description: "Forbidden" },
194
+ "500": { description: "Internal Server Error" }
195
+ },
188
196
  // -- group defaults --
189
197
  ...configs?.oapi || {},
190
198
  // -- overrides --
@@ -211,8 +219,22 @@ function buildExpressive(container, swaggerDoc) {
211
219
  };
212
220
  }
213
221
 
214
- // src/common.ts
215
- import path from "path";
222
+ // src/env.ts
223
+ import dotenv from "dotenv";
224
+ dotenv.config();
225
+ function isDev() {
226
+ return getEnvVar("ENV") === "dev";
227
+ }
228
+ function isProd() {
229
+ return getEnvVar("ENV") === "prod";
230
+ }
231
+ function getEnvVar(configName) {
232
+ const config = process.env[configName];
233
+ if (!config) {
234
+ throw new Error(`Missing config '${configName}'`);
235
+ }
236
+ return config;
237
+ }
216
238
 
217
239
  // src/errors.ts
218
240
  var ApiError = class extends Error {
@@ -291,59 +313,6 @@ var UserUnauthorizedError = class extends ApiError {
291
313
  }
292
314
  };
293
315
 
294
- // src/common.ts
295
- function parsePositiveInteger(v, defaultValue, max) {
296
- const value = Number(v);
297
- return Number.isInteger(value) && value > 0 && (!max || value <= max) ? value : defaultValue;
298
- }
299
- function parseIdOrFail(v) {
300
- const value = Number(v);
301
- if (!Number.isInteger(value) || value <= 0) {
302
- throw new ApiError("Invalid Id", 400, "INVALID_ID");
303
- }
304
- return value;
305
- }
306
- function slugify(text) {
307
- return text.toString().normalize("NFKD").toLowerCase().trim().replace(/[\s\n_+.]/g, "-").replace(/--+/g, "-");
308
- }
309
- function getTmpDir() {
310
- return path.resolve("tmp");
311
- }
312
- function getTmpPath(...steps) {
313
- return path.join(getTmpDir(), ...steps);
314
- }
315
- function parseDefaultPagination(query) {
316
- const limit = parsePositiveInteger(query.limit, 50, 100);
317
- const page = parsePositiveInteger(query.page, 1, 1e3);
318
- const offset = (page - 1) * limit;
319
- return { limit, offset };
320
- }
321
- function createReqSnapshot(req) {
322
- return {
323
- query: req.query,
324
- path: req.path,
325
- method: req.method,
326
- userId: req?.user?.id
327
- };
328
- }
329
-
330
- // src/env.ts
331
- import dotenv from "dotenv";
332
- dotenv.config();
333
- function isDev() {
334
- return getEnvVar("ENV") === "dev";
335
- }
336
- function isProd() {
337
- return getEnvVar("ENV") === "prod";
338
- }
339
- function getEnvVar(configName) {
340
- const config = process.env[configName];
341
- if (!config) {
342
- throw new Error(`Missing config '${configName}'`);
343
- }
344
- return config;
345
- }
346
-
347
316
  // src/response/ApiErrorResponse.ts
348
317
  var ApiErrorResponse = class {
349
318
  status = "error";
@@ -383,7 +352,7 @@ var buildMiddleware = (container) => {
383
352
  logger.error("Cause: %s", err.cause);
384
353
  }
385
354
  if (alertHandler) {
386
- alertHandler(err, createReqSnapshot(req));
355
+ alertHandler(err);
387
356
  }
388
357
  finalError = new InternalError();
389
358
  if (!isProd()) {
@@ -396,63 +365,34 @@ var buildMiddleware = (container) => {
396
365
  };
397
366
  };
398
367
 
399
- // src/logger.ts
400
- import winston from "winston";
401
- import DailyRotateFile from "winston-daily-rotate-file";
402
- var loggerRegistry = {};
403
- var defaultFormat = winston.format.combine(
404
- winston.format.timestamp({ format: "DD/MM/YYYY HH:mm:ss" }),
405
- winston.format.splat(),
406
- // String interpolation splat for %d %s-style messages.
407
- winston.format.errors({ stack: true }),
408
- winston.format.printf(({ level, message, timestamp, stack }) => `${timestamp}[${level.toUpperCase()}]: ${stack || message}`)
409
- );
410
- var consoleTransport = new winston.transports.Console({
411
- handleExceptions: true
412
- });
413
- var createFileLogger = (filename) => {
414
- const logger = winston.createLogger({
415
- format: defaultFormat,
416
- transports: [
417
- new DailyRotateFile({
418
- level: "debug",
419
- filename: `./logs/${filename}-%DATE%.log`,
420
- handleExceptions: true,
421
- // exitOnError: false, // do not exit on handled exceptions
422
- datePattern: "YYYY-MM-DD",
423
- zippedArchive: true,
424
- auditFile: "./logs/audit.json",
425
- maxSize: "20m",
426
- maxFiles: isProd() ? "30d" : "1d"
427
- // format: defaultFormat,
428
- // the nested 'format' field causes issues with logging errors;
429
- // use the 'format' field on logger, instead of transport;
430
- })
431
- ]
432
- });
433
- if (isDev()) {
434
- logger.add(consoleTransport);
435
- }
436
- return logger;
437
- };
438
- var createLogger = winston.createLogger;
439
- var getDefaultFileLogger = (name = "app") => {
440
- if (!loggerRegistry[name]) {
441
- loggerRegistry[name] = createFileLogger(name.toString());
442
- }
443
- return loggerRegistry[name];
444
- };
445
- var getDefaultConsoleLogger = (name = "console") => {
446
- if (!loggerRegistry[name]) {
447
- loggerRegistry[name] = winston.createLogger({
448
- format: defaultFormat,
449
- transports: [
450
- consoleTransport
451
- ]
452
- });
368
+ // src/common.ts
369
+ import path from "path";
370
+ function parsePositiveInteger(v, defaultValue, max) {
371
+ const value = Number(v);
372
+ return Number.isInteger(value) && value > 0 && (!max || value <= max) ? value : defaultValue;
373
+ }
374
+ function parseIdOrFail(v) {
375
+ const value = Number(v);
376
+ if (!Number.isInteger(value) || value <= 0) {
377
+ throw new ApiError("Invalid Id", 400, "INVALID_ID");
453
378
  }
454
- return loggerRegistry[name];
455
- };
379
+ return value;
380
+ }
381
+ function slugify(text) {
382
+ return text.toString().normalize("NFKD").toLowerCase().trim().replace(/[\s\n_+.]/g, "-").replace(/--+/g, "-");
383
+ }
384
+ function getTmpDir() {
385
+ return path.resolve("tmp");
386
+ }
387
+ function getTmpPath(...steps) {
388
+ return path.join(getTmpDir(), ...steps);
389
+ }
390
+ function parseDefaultPagination(query) {
391
+ const limit = parsePositiveInteger(query.limit, 50, 100);
392
+ const page = parsePositiveInteger(query.page, 1, 1e3);
393
+ const offset = (page - 1) * limit;
394
+ return { limit, offset };
395
+ }
456
396
 
457
397
  // src/response/ApiResponse.ts
458
398
  var ApiResponse = class {
@@ -476,14 +416,16 @@ function bootstrap(container) {
476
416
  ...buildExpressive(container, swaggerDoc),
477
417
  ...buildMiddleware(container),
478
418
  swaggerBuilder: () => new SwaggerBuilder(swaggerDoc),
479
- silently: (fn, reqSnapshot) => {
480
- fn().catch((e) => {
419
+ silently: async (fn) => {
420
+ try {
421
+ await fn();
422
+ } catch (e) {
481
423
  if (container.alertHandler && e instanceof Error) {
482
- container.alertHandler(e, reqSnapshot);
424
+ container.alertHandler(e);
483
425
  } else {
484
426
  container.logger.error(e);
485
427
  }
486
- });
428
+ }
487
429
  }
488
430
  };
489
431
  }
@@ -505,10 +447,6 @@ export {
505
447
  TooManyRequestsError,
506
448
  UserUnauthorizedError,
507
449
  bootstrap,
508
- createLogger,
509
- createReqSnapshot,
510
- getDefaultConsoleLogger,
511
- getDefaultFileLogger,
512
450
  getEnvVar,
513
451
  getTmpDir,
514
452
  getTmpPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extk/expressive",
3
- "version": "0.4.4",
3
+ "version": "0.5.2",
4
4
  "type": "commonjs",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -46,9 +46,7 @@
46
46
  "helmet": "^8.0.0",
47
47
  "morgan": "^1.10.0",
48
48
  "qs": "^6.14.1",
49
- "swagger-ui-express": "^5.0.1",
50
- "winston": "^3.11.0",
51
- "winston-daily-rotate-file": "^5.0.0"
49
+ "swagger-ui-express": "^5.0.1"
52
50
  },
53
51
  "author": "",
54
52
  "license": "ISC",