@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 +5 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +14 -2
- package/dist/faktory.d.ts +8 -3
- package/dist/faktory.js +6 -4
- package/dist/listen.js +15 -12
- package/package.json +8 -8
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
|
|
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 [
|
|
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 }
|
|
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,
|
|
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('
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
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": "
|
|
85
|
-
"start:worker": "
|
|
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}'"
|