@devmoods/express-extras 0.33.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,13 +8,14 @@ _Extremely opinionated_ set of utilities for cranking out production-ready Node.
8
8
  - Thin layer on top of [Express](https://expressjs.com) with better support for `async`/`await`
9
9
  - PostgreSQL with transactions and migrations
10
10
  - Redis
11
- - Slack (and Discord) client for sending notifications
11
+ - Slack (and Discord) client for sending operational notifications
12
+ - CLI commands inspired by Django's management commands
12
13
  - Delayed jobs with [Faktory](https://contribsys.com/faktory/)
13
- - Validation of HTTP requests using [JSON Schemas](https://json-schema.org/)
14
+ - Validation of HTTP requests using [JSON Schema](https://json-schema.org/)s
14
15
  - Logging helpers to write well-formatted contextual JSON logs to `stdout`
15
- - Request local context using [AsyncLocalStorage](https://nodejs.org/api/async_context.html)
16
+ - Request local context using [AsyncLocalStorage](https://nodejs.org/api/async_context.html) (e.g. request ids)
16
17
  - [RFC 7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807) by default
17
- - Works best with the [PnPM](https://pnpm.io/) package manager
18
+ - Works best with the [pnpm](https://pnpm.io/) package manager
18
19
  - Uses [Sentry](https://sentry.io/) for external error monitoring
19
20
  - Inspired by [The Twelve-Factor App](https://12factor.net/)
20
21
  - Graceful shutdown
package/dist/errors.d.ts CHANGED
@@ -36,4 +36,4 @@ export interface ErrorHandlerOptions {
36
36
  *
37
37
  * Composed together with the Sentry error handler.
38
38
  */
39
- export declare function errorHandler({ onError }: ErrorHandlerOptions): import("compose-middleware").ErrorHandler<unknown, unknown, void>;
39
+ export declare function errorHandler({ onError }?: ErrorHandlerOptions): import("compose-middleware").ErrorHandler<unknown, unknown, void>;
package/dist/errors.js CHANGED
@@ -2,6 +2,7 @@ import { STATUS_CODES } from 'node:http';
2
2
  import * as Sentry from '@sentry/node';
3
3
  import { errors } from 'compose-middleware';
4
4
  import {} from 'express';
5
+ import { getLogger } from './logger.js';
5
6
  import { DoesNotExistError, IntegrityError } from './postgres.js';
6
7
  export class ApiError extends Error {
7
8
  }
@@ -62,7 +63,18 @@ function _errorHandler({ onError }) {
62
63
  *
63
64
  * Composed together with the Sentry error handler.
64
65
  */
65
- export function errorHandler({ onError }) {
66
+ export function errorHandler({ onError } = {}) {
67
+ const logger = getLogger();
68
+ const _defaultErrorHandler = (error) => {
69
+ if (error instanceof ApiError && error.status < 500) {
70
+ logger.warn(`${error.status}: ${error.message}`, {
71
+ reason: error.payload,
72
+ });
73
+ }
74
+ else {
75
+ logger.error(error);
76
+ }
77
+ };
66
78
  return errors([
67
79
  Sentry.expressErrorHandler({
68
80
  shouldHandleError(error) {
@@ -72,6 +84,6 @@ export function errorHandler({ onError }) {
72
84
  return process.env.NODE_ENV === 'production';
73
85
  },
74
86
  }),
75
- _errorHandler({ onError }),
87
+ _errorHandler({ onError: onError || _defaultErrorHandler }),
76
88
  ]);
77
89
  }
package/dist/faktory.d.ts CHANGED
@@ -3,13 +3,16 @@ export interface JobsOptions {
3
3
  queueName: string;
4
4
  faktory?: typeof create;
5
5
  }
6
+ interface JobContext {
7
+ [key: string]: any;
8
+ }
6
9
  export declare function createJobs({ queueName, faktory: createFactory, }: JobsOptions): {
7
- job: <K extends Record<string, any>, T extends (input: K) => undefined | Promise<any>>(fn: T) => {
10
+ job: <K extends Record<string, any>, T extends (input: K, ctx: JobContext) => undefined | Promise<any>>(fn: T) => {
8
11
  $job: boolean;
9
12
  jobName: string;
10
13
  readonly executionCount: number;
11
14
  delay(args?: Parameters<T>[0]): Promise<`${string}-${string}-${string}-${string}-${string}`>;
12
- execute(args: Parameters<T>[0]): Promise<any>;
15
+ execute(args: Parameters<T>[0], context: JobContext): Promise<any>;
13
16
  };
14
17
  queueName: string;
15
18
  };
@@ -18,9 +21,11 @@ export interface FaktoryWorkerOptions {
18
21
  jobs: unknown[];
19
22
  queueName: string;
20
23
  faktory?: typeof create;
24
+ jobContext?: JobContext;
21
25
  }
22
26
  export declare function useJobId(): string | undefined;
23
- export declare function createFaktoryWorker({ queueName, jobs, faktory: createFactory, }: FaktoryWorkerOptions): Promise<{
27
+ export declare function createFaktoryWorker({ queueName, jobs, faktory: createFactory, jobContext, }: FaktoryWorkerOptions): Promise<{
24
28
  start(): Promise<void>;
25
29
  stop(): Promise<void>;
26
30
  }>;
31
+ export {};
package/dist/faktory.js CHANGED
@@ -39,9 +39,9 @@ export function createJobs({ queueName, faktory: createFactory = create, }) {
39
39
  }
40
40
  return jid;
41
41
  },
42
- async execute(args) {
42
+ async execute(args, context) {
43
43
  try {
44
- const result = await fn(args);
44
+ const result = await fn(args, context);
45
45
  executionCount += 1;
46
46
  return result;
47
47
  }
@@ -61,12 +61,14 @@ export function useJobId() {
61
61
  return store.get(JOB_ID);
62
62
  }
63
63
  }
64
- export async function createFaktoryWorker({ queueName, jobs, faktory: createFactory = create, }) {
64
+ export async function createFaktoryWorker({ queueName, jobs, faktory: createFactory = create, jobContext = {}, }) {
65
65
  const faktory = createFactory();
66
66
  const filter = (j) => !!Object.prototype.hasOwnProperty.call(j, '$job');
67
67
  const availableJobs = jobs.filter(filter);
68
68
  for (const job of availableJobs) {
69
- faktory.register(job.jobName, job.execute);
69
+ faktory.register(job.jobName, (...args) => {
70
+ return job.execute(...args, jobContext);
71
+ });
70
72
  }
71
73
  return {
72
74
  async start() {
package/dist/listen.js CHANGED
@@ -95,19 +95,22 @@ export function createExpressServer(app, options = {}) {
95
95
  for (const signal of ['SIGTERM', 'SIGINT']) {
96
96
  process.on(signal, onSignal);
97
97
  }
98
- app.get('/health', before(), route(async () => {
99
- const timestamp = new Date().toISOString();
100
- try {
101
- if (stopping) {
102
- throw new Error();
98
+ if (!app.get('__healthAttached')) {
99
+ app.get('/health', before(), route(async () => {
100
+ const timestamp = new Date().toISOString();
101
+ try {
102
+ if (stopping) {
103
+ throw new Error();
104
+ }
105
+ const meta = await options.healthCheck?.();
106
+ return { status: 'ok', meta, timestamp };
103
107
  }
104
- const meta = await options.healthCheck?.();
105
- return { status: 'ok', meta, timestamp };
106
- }
107
- catch {
108
- return [503, { status: 'error', timestamp }];
109
- }
110
- }));
108
+ catch {
109
+ return [503, { status: 'error', timestamp }];
110
+ }
111
+ }));
112
+ app.set('__healthAttached', true);
113
+ }
111
114
  return {
112
115
  start() {
113
116
  const port = app.get('port') ?? envPort(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/express-extras",
3
- "version": "0.33.1",
3
+ "version": "0.35.0",
4
4
  "description": "Kubernetes-ready utilities for rapid Node.js application development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,8 +33,10 @@
33
33
  "prettier": "3.3.3",
34
34
  "supertest": "7.0.0",
35
35
  "ts-node": "10.9.2",
36
+ "tsx": "^4.19.1",
36
37
  "typescript": "5.6.3",
37
- "vitest": "^2.1.2"
38
+ "vitest": "^2.1.2",
39
+ "vitest-fail-on-console": "^0.7.1"
38
40
  },
39
41
  "dependencies": {
40
42
  "@devmoods/fetch": "^4.0.1",
@@ -60,13 +62,11 @@
60
62
  "express-route-parser": "^1.0.5",
61
63
  "helmet": "^8.0.0",
62
64
  "jsonwebtoken": "^9.0.2",
63
- "meow": "^13.2.0",
64
65
  "nodemailer": "^6.9.15",
65
66
  "pg": "^8.13.0",
66
67
  "redis": "^4.7.0",
67
68
  "sql-template-strings": "^2.2.2",
68
- "uuid": "^10.0.0",
69
- "vitest-fail-on-console": "^0.7.1"
69
+ "uuid": "^10.0.0"
70
70
  },
71
71
  "prettier": {
72
72
  "singleQuote": true
@@ -81,10 +81,10 @@
81
81
  "node": ">=20.3.0 <23"
82
82
  },
83
83
  "scripts": {
84
- "start": "node --no-warnings --enable-source-maps --loader ts-node/esm example/src/index.ts --name example",
85
- "start:worker": "node --no-warnings --enable-source-maps --loader ts-node/esm example/worker.ts",
84
+ "start": "pushd example; pnpm run start; popd example;",
85
+ "start:worker": "pushd example; pnpm run start:worker; popd example;",
86
86
  "clean": "rm -rf dist",
87
- "lint": "eslint src example",
87
+ "lint": "eslint src example/src",
88
88
  "test": "./test.sh",
89
89
  "build": "pnpm run clean && tsc --project tsconfig.build.json",
90
90
  "format": "prettier --write '**/*.{js,css,md,ts,tsx}'"