@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 +49 -8
- package/dist/index.d.mts +10 -21
- package/dist/index.d.ts +10 -21
- package/dist/index.js +60 -126
- package/dist/index.mjs +60 -122
- package/package.json +2 -4
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,
|
|
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:
|
|
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
|
-
|
|
262
|
+
Expressive does not bundle a logger. Instead, `bootstrap` accepts any object that satisfies the `Logger` interface:
|
|
252
263
|
|
|
253
264
|
```ts
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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>,
|
|
32
|
-
type Logger =
|
|
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
|
|
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,
|
|
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
|
|
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>,
|
|
32
|
-
type Logger =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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/
|
|
280
|
-
var
|
|
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
|
|
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/
|
|
465
|
-
var
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
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
|
|
545
|
-
|
|
480
|
+
silently: async (fn) => {
|
|
481
|
+
try {
|
|
482
|
+
await fn();
|
|
483
|
+
} catch (e) {
|
|
546
484
|
if (container.alertHandler && e instanceof Error) {
|
|
547
|
-
container.alertHandler(e
|
|
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
|
-
|
|
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/
|
|
215
|
-
import
|
|
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
|
|
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/
|
|
400
|
-
import
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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
|
|
480
|
-
|
|
419
|
+
silently: async (fn) => {
|
|
420
|
+
try {
|
|
421
|
+
await fn();
|
|
422
|
+
} catch (e) {
|
|
481
423
|
if (container.alertHandler && e instanceof Error) {
|
|
482
|
-
container.alertHandler(e
|
|
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.
|
|
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",
|