@devmoods/express-extras 0.20.1 → 0.22.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 +13 -125
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +64 -18
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -1
- package/dist/faktory.d.ts +22 -0
- package/dist/faktory.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/listen.d.ts +1 -1
- package/dist/listen.js +6 -1
- package/dist/logger.d.ts +4 -4
- package/dist/logger.js +15 -8
- package/dist/middleware.d.ts +1 -9
- package/dist/middleware.js +18 -1
- package/dist/routes.d.ts +2 -2
- package/dist/routes.js +9 -40
- package/package.json +19 -13
package/README.md
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
> Kubernetes-ready utilities for rapid Node.js application development
|
|
4
4
|
|
|
5
|
-
_Extremely opinionated_ set of utilities for cranking out Node.js apps in a timely manner.
|
|
5
|
+
_Extremely opinionated_ set of utilities for cranking out production-ready Node.js apps in a timely manner. We assume you are using PostgreSQL as the main database, Redis for data you don't want in Postgres and [Faktory](https://contribsys.com/faktory/) for background jobs.
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Only works with [ECMAScript Modules (ESM)](https://nodejs.org/api/esm.html)
|
|
8
|
+
- Thin layer on top of [Express](https://expressjs.com) with better support for `async`/`await`
|
|
9
|
+
- PostgreSQL with transactions and migrations
|
|
9
10
|
- Redis
|
|
10
|
-
- Slack
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- Slack notifications
|
|
12
|
+
- Delayed jobs with [Faktory](https://contribsys.com/faktory/)
|
|
13
|
+
- Validation of requests using [JSON Schemas](https://json-schema.org/)
|
|
14
|
+
- Logging helpers
|
|
15
|
+
- Graceful shutdown
|
|
14
16
|
- Read configuration from `.env` files
|
|
15
|
-
- Request
|
|
16
|
-
- [RFC 7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807)
|
|
17
|
+
- Request local context using [AsyncLocalStorage](https://nodejs.org/api/async_context.html)
|
|
18
|
+
- [RFC 7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807) by default
|
|
19
|
+
- Works best with the [PnPM](https://pnpm.io/) package manager
|
|
17
20
|
|
|
18
21
|
## Goals
|
|
19
22
|
|
|
@@ -21,127 +24,12 @@ _Extremely opinionated_ set of utilities for cranking out Node.js apps in a time
|
|
|
21
24
|
|
|
22
25
|
## Non-goals
|
|
23
26
|
|
|
24
|
-
-
|
|
27
|
+
- _Encourage deviating from the chosen path_
|
|
25
28
|
|
|
26
29
|
## Example
|
|
27
30
|
|
|
28
31
|
> See the [example/](./example) folder for a complete example.
|
|
29
32
|
|
|
30
|
-
```typescript
|
|
31
|
-
require('dotenv-safe').config({
|
|
32
|
-
allowEmptyValues: true,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
import http from 'http';
|
|
36
|
-
import express from 'express';
|
|
37
|
-
import * as Sentry from '@sentry/node';
|
|
38
|
-
import helmet from 'helmet';
|
|
39
|
-
import bodyParser from 'body-parser';
|
|
40
|
-
import {
|
|
41
|
-
errorHandler,
|
|
42
|
-
envPort,
|
|
43
|
-
validateBody,
|
|
44
|
-
route,
|
|
45
|
-
listen,
|
|
46
|
-
withRequestContext,
|
|
47
|
-
ApiError,
|
|
48
|
-
createLogger,
|
|
49
|
-
createPostgres,
|
|
50
|
-
getRequestId,
|
|
51
|
-
sql,
|
|
52
|
-
} from '@devmoods/express-extras';
|
|
53
|
-
|
|
54
|
-
const app = express();
|
|
55
|
-
const server = http.createServer(app);
|
|
56
|
-
const postgres = createPostgres();
|
|
57
|
-
const logger = createLogger({
|
|
58
|
-
name: 'example-app',
|
|
59
|
-
getContext() {
|
|
60
|
-
return {
|
|
61
|
-
requestId: getRequestId(),
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
const slack = createSlack({
|
|
66
|
-
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
app.set('port', envPort());
|
|
70
|
-
app.use(Sentry.Handlers.requestHandler());
|
|
71
|
-
app.use(helmet());
|
|
72
|
-
app.use(bodyParser.json());
|
|
73
|
-
app.use(withRequestContext());
|
|
74
|
-
|
|
75
|
-
app.get(
|
|
76
|
-
'/posts',
|
|
77
|
-
route(async () => {
|
|
78
|
-
const posts = await postgres.query('SELECT * FROM posts LIMIT 10');
|
|
79
|
-
return posts;
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
app.post(
|
|
84
|
-
'/posts',
|
|
85
|
-
validateBody({
|
|
86
|
-
type: 'object',
|
|
87
|
-
properties: {
|
|
88
|
-
title: {
|
|
89
|
-
type: 'string',
|
|
90
|
-
minLength: 3,
|
|
91
|
-
},
|
|
92
|
-
content: {
|
|
93
|
-
type: 'string',
|
|
94
|
-
minLength: 3,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
required: ['title', 'content'],
|
|
98
|
-
}),
|
|
99
|
-
route(async ({ body: { title, content } }) => {
|
|
100
|
-
const post = { title, content };
|
|
101
|
-
|
|
102
|
-
await postgres.insert(
|
|
103
|
-
sql`INSERT INTO posts (title, content) VALUES (${title}, ${content})`,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
slack.postMessage(`New post created with title ${title}`);
|
|
107
|
-
|
|
108
|
-
return [201, post];
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
app.use((req, res, next) => {
|
|
113
|
-
next(NotFound());
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
app.use(
|
|
117
|
-
Sentry.Handlers.errorHandler({
|
|
118
|
-
shouldHandleError(error) {
|
|
119
|
-
return true;
|
|
120
|
-
},
|
|
121
|
-
}),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
app.use(
|
|
125
|
-
errorHandler({
|
|
126
|
-
onError(error) {
|
|
127
|
-
if (error instanceof ApiError && error.status < 500) {
|
|
128
|
-
logger.warn(error.message, { error });
|
|
129
|
-
} else {
|
|
130
|
-
logger.error(error);
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
listen(server, {
|
|
137
|
-
port: app.get('port'),
|
|
138
|
-
logger,
|
|
139
|
-
async onShutdown() {
|
|
140
|
-
await Promise.all([redis.quit(), postgres.end()]);
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
33
|
## Configure
|
|
146
34
|
|
|
147
35
|
Put environment variables in `.env` at the application will automatically read those on startup. For test configuration in unit and integrations tests, use `.env.test`.
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,13 +12,15 @@ export interface AuthOptions<UserModel extends Record<string, any>> {
|
|
|
12
12
|
tokenExpiry?: number;
|
|
13
13
|
onResetRequested?(user: UserModel, temporaryToken: string): void | Promise<void>;
|
|
14
14
|
onPasswordChanged?(user: UserModel, newPassword: string): Promise<void>;
|
|
15
|
+
onUserVerified?(user: UserModel): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
export interface CreateAuth<UserModel extends Record<string, any>> {
|
|
17
18
|
createRouter: () => Router;
|
|
18
19
|
useCurrentUser: () => UserModel | undefined;
|
|
19
20
|
middleware: (authorize?: boolean) => RequestHandler;
|
|
21
|
+
createVerificationToken(email: string): Promise<string>;
|
|
20
22
|
}
|
|
21
|
-
export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, onResetRequested, onPasswordChanged, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
23
|
+
export declare function createAuth<UserModel extends Record<string, any>>({ getUserByToken, methods: { emailAndPassword }, filterUser, redis, tokenExpiry, onResetRequested, onPasswordChanged, onUserVerified, }: AuthOptions<UserModel>): CreateAuth<UserModel>;
|
|
22
24
|
/**
|
|
23
25
|
* Extract a Bearer token from the HTTP Authorization header
|
|
24
26
|
* of `req`.
|
package/dist/auth.js
CHANGED
|
@@ -14,9 +14,10 @@ const TEMP_TOKEN_EXIRES_IN = 3600;
|
|
|
14
14
|
export function createAuthToken(payload, secret = process.env.APP_JWT_SECRET) {
|
|
15
15
|
return jwt.sign(payload, secret, { expiresIn: TOKEN_EXPIRES_IN });
|
|
16
16
|
}
|
|
17
|
-
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onResetRequested, onPasswordChanged, }) {
|
|
17
|
+
export function createAuth({ getUserByToken, methods: { emailAndPassword }, filterUser = (x) => x, redis, tokenExpiry = TOKEN_EXPIRES_IN, onResetRequested, onPasswordChanged, onUserVerified, }) {
|
|
18
18
|
const useCurrentUser = () => useContext('currentUser');
|
|
19
19
|
const setCurrentUser = (user) => setContext('currentUser', user);
|
|
20
|
+
const skipPasswordCheck = async () => true;
|
|
20
21
|
const createRouter = () => {
|
|
21
22
|
const router = Router();
|
|
22
23
|
router.get('/me', middleware(), route(() => {
|
|
@@ -55,24 +56,18 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
55
56
|
setAccessTokenCookie(res, null);
|
|
56
57
|
res.redirect('/');
|
|
57
58
|
}));
|
|
58
|
-
const skipPasswordCheck = async () => true;
|
|
59
59
|
const tokenSchema = {
|
|
60
|
-
type: 'object',
|
|
61
|
-
properties: {
|
|
62
|
-
token: { type: 'string', minLength: 50, maxLength: 64 },
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
const emailSchema = {
|
|
66
60
|
type: 'string',
|
|
67
|
-
minLength:
|
|
61
|
+
minLength: 50,
|
|
62
|
+
maxLength: 64,
|
|
68
63
|
};
|
|
64
|
+
const passwordSchema = { type: 'string', minLength: 8 };
|
|
65
|
+
const emailSchema = { type: 'string', minLength: 4 };
|
|
69
66
|
if (emailAndPassword) {
|
|
70
|
-
router.post('/
|
|
67
|
+
router.post('/change-password', validateBody({
|
|
71
68
|
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
required: ['password'],
|
|
69
|
+
properties: { token: tokenSchema, password: passwordSchema },
|
|
70
|
+
required: ['token', 'password'],
|
|
76
71
|
}), route(async (req) => {
|
|
77
72
|
const { password } = req.body;
|
|
78
73
|
const { token } = req.params;
|
|
@@ -88,8 +83,40 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
88
83
|
logger.info('user changed password');
|
|
89
84
|
return 200;
|
|
90
85
|
}));
|
|
86
|
+
router.post('/verify-email', validateBody({
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: { token: tokenSchema, email: emailSchema },
|
|
89
|
+
required: ['token', 'email'],
|
|
90
|
+
}), route(async (req) => {
|
|
91
|
+
const { token } = req.body;
|
|
92
|
+
const email = await redis.get(token);
|
|
93
|
+
if (!email) {
|
|
94
|
+
logger.info('invalid verification token', { token });
|
|
95
|
+
throw BadRequest('invalid verification token');
|
|
96
|
+
}
|
|
97
|
+
if (email !== req.body.email) {
|
|
98
|
+
logger.info('email verification mismatch', {
|
|
99
|
+
expected: email,
|
|
100
|
+
actual: req.body.email,
|
|
101
|
+
});
|
|
102
|
+
throw BadRequest('invalid verification token');
|
|
103
|
+
}
|
|
104
|
+
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
105
|
+
setCurrentUser(user);
|
|
106
|
+
await onUserVerified?.(user);
|
|
107
|
+
await redis.del(token);
|
|
108
|
+
logger.info('user verified');
|
|
109
|
+
return 200;
|
|
110
|
+
}));
|
|
91
111
|
}
|
|
92
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Check that a reset token is valid.
|
|
114
|
+
*/
|
|
115
|
+
router.get('/reset/:token', validateParams({
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: { token: tokenSchema },
|
|
118
|
+
required: ['token'],
|
|
119
|
+
}), route(async (req) => {
|
|
93
120
|
const token = req.params.token;
|
|
94
121
|
const email = await redis.get(token);
|
|
95
122
|
if (!email) {
|
|
@@ -98,11 +125,12 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
98
125
|
}
|
|
99
126
|
return 200;
|
|
100
127
|
}));
|
|
128
|
+
/**
|
|
129
|
+
* Request password reset
|
|
130
|
+
*/
|
|
101
131
|
router.post('/reset', middleware(false), validateBody({
|
|
102
132
|
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
email: emailSchema,
|
|
105
|
-
},
|
|
133
|
+
properties: { email: emailSchema },
|
|
106
134
|
required: ['email'],
|
|
107
135
|
}), route(async (req) => {
|
|
108
136
|
const email = req.body.email;
|
|
@@ -170,10 +198,28 @@ export function createAuth({ getUserByToken, methods: { emailAndPassword }, filt
|
|
|
170
198
|
}
|
|
171
199
|
};
|
|
172
200
|
}
|
|
201
|
+
async function createVerificationToken(email) {
|
|
202
|
+
if (!emailAndPassword) {
|
|
203
|
+
throw new Error('Not applicable for login method');
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const user = await emailAndPassword(email, skipPasswordCheck);
|
|
207
|
+
const temporaryToken = crypto.randomBytes(32).toString('hex');
|
|
208
|
+
await redis.set(temporaryToken, user.email, {
|
|
209
|
+
EX: TEMP_TOKEN_EXIRES_IN,
|
|
210
|
+
});
|
|
211
|
+
return temporaryToken;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger.error(error, 'could not create verification token');
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
173
218
|
return {
|
|
174
219
|
createRouter,
|
|
175
220
|
useCurrentUser,
|
|
176
221
|
middleware,
|
|
222
|
+
createVerificationToken,
|
|
177
223
|
};
|
|
178
224
|
}
|
|
179
225
|
/**
|
package/dist/context.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export declare function useContext<T>(key: string): T | undefined;
|
|
|
13
13
|
/**
|
|
14
14
|
* Set a value on the request context
|
|
15
15
|
*/
|
|
16
|
-
export declare function setContext<T>(key: string, value: T):
|
|
16
|
+
export declare function setContext<T>(key: string, value: T): void;
|
|
17
17
|
export declare function useRequestId(): string | undefined;
|
package/dist/context.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface JobsOptions {
|
|
2
|
+
queueName: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function createJobs({ queueName }: JobsOptions): {
|
|
5
|
+
job: <K extends Record<string, any>, T extends (input: K) => void | Promise<any>>(fn: T) => {
|
|
6
|
+
$job: boolean;
|
|
7
|
+
jobName: string;
|
|
8
|
+
readonly executionCount: number;
|
|
9
|
+
delay(args?: Parameters<T>[0] | undefined): Promise<void>;
|
|
10
|
+
execute(args: Parameters<T>[0]): Promise<any>;
|
|
11
|
+
};
|
|
12
|
+
queueName: string;
|
|
13
|
+
};
|
|
14
|
+
export type Task = ReturnType<ReturnType<typeof createJobs>['job']>;
|
|
15
|
+
export interface FaktoryWorkerOptions {
|
|
16
|
+
jobs: unknown[];
|
|
17
|
+
queueName: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function useJobId(): string | undefined;
|
|
20
|
+
export declare function createFaktoryWorker({ queueName, jobs, }: FaktoryWorkerOptions): Promise<{
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
}>;
|
package/dist/faktory.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { create } from 'faktory-worker';
|
|
4
|
+
import { getLogger } from './logger.js';
|
|
5
|
+
const logger = getLogger('faktory');
|
|
6
|
+
const JOB_ID = 'jobId';
|
|
7
|
+
export function createJobs({ queueName }) {
|
|
8
|
+
const faktory = create();
|
|
9
|
+
function job(fn) {
|
|
10
|
+
const jobName = fn.name;
|
|
11
|
+
let executionCount = 0;
|
|
12
|
+
if (jobName == null) {
|
|
13
|
+
throw new Error('Can not create job without a name');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
$job: true,
|
|
17
|
+
jobName,
|
|
18
|
+
get executionCount() {
|
|
19
|
+
return executionCount;
|
|
20
|
+
},
|
|
21
|
+
async delay(args) {
|
|
22
|
+
const jid = randomUUID();
|
|
23
|
+
try {
|
|
24
|
+
const client = await faktory.connect();
|
|
25
|
+
const j = client.job(jobName, args);
|
|
26
|
+
j.jid = jid;
|
|
27
|
+
j.queue = queueName;
|
|
28
|
+
await j.push();
|
|
29
|
+
await client.close();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error(error, 'Failed to create job', {
|
|
33
|
+
jobId: jid,
|
|
34
|
+
jobName,
|
|
35
|
+
args: JSON.stringify(args),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async execute(args) {
|
|
40
|
+
try {
|
|
41
|
+
const result = await fn(args);
|
|
42
|
+
executionCount += 1;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error(error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { job, queueName };
|
|
53
|
+
}
|
|
54
|
+
const context = new AsyncLocalStorage();
|
|
55
|
+
export function useJobId() {
|
|
56
|
+
const store = context.getStore();
|
|
57
|
+
if (store) {
|
|
58
|
+
return store.get(JOB_ID);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function createFaktoryWorker({ queueName, jobs, }) {
|
|
62
|
+
const faktory = create();
|
|
63
|
+
const filter = (j) => !!Object.prototype.hasOwnProperty.call(j, '$job');
|
|
64
|
+
const availableJobs = jobs.filter(filter);
|
|
65
|
+
for (const job of availableJobs) {
|
|
66
|
+
faktory.register(job.jobName, job.execute);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
async start() {
|
|
70
|
+
try {
|
|
71
|
+
logger.info('faktory started', {
|
|
72
|
+
queueName,
|
|
73
|
+
availableJobs: availableJobs.map((j) => j.jobName),
|
|
74
|
+
});
|
|
75
|
+
const getDurationInMilliseconds = (start) => {
|
|
76
|
+
const NS_PER_SEC = 1e9;
|
|
77
|
+
const NS_TO_S = 1e9;
|
|
78
|
+
const diff = process.hrtime(start);
|
|
79
|
+
return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_S;
|
|
80
|
+
};
|
|
81
|
+
faktory.use(async (ctx, next) => {
|
|
82
|
+
const jobId = ctx.job.jid;
|
|
83
|
+
const start = process.hrtime();
|
|
84
|
+
await context.run(new Map(), async () => {
|
|
85
|
+
const store = context.getStore();
|
|
86
|
+
if (store) {
|
|
87
|
+
store.set(JOB_ID, jobId);
|
|
88
|
+
}
|
|
89
|
+
await next();
|
|
90
|
+
});
|
|
91
|
+
const duration = getDurationInMilliseconds(start);
|
|
92
|
+
logger.info('job succeeded', {
|
|
93
|
+
jobName: ctx.job.jobtype,
|
|
94
|
+
jobId,
|
|
95
|
+
duration,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
await faktory.work({
|
|
99
|
+
queues: [queueName],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.error(error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/listen.d.ts
CHANGED
package/dist/listen.js
CHANGED
|
@@ -34,13 +34,18 @@ export function listen(server, options) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
await options?.healthCheck?.();
|
|
37
|
+
const meta = await options?.healthCheck?.();
|
|
38
|
+
return {
|
|
39
|
+
meta,
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
};
|
|
38
42
|
}
|
|
39
43
|
catch (error) {
|
|
40
44
|
logger.error(error, 'health check failed');
|
|
41
45
|
throw error;
|
|
42
46
|
}
|
|
43
47
|
},
|
|
48
|
+
verbatim: true,
|
|
44
49
|
},
|
|
45
50
|
});
|
|
46
51
|
const port = options.port;
|
package/dist/logger.d.ts
CHANGED
|
@@ -21,14 +21,14 @@ export declare function createLogger({ name, getContext, silent, }: LoggerOption
|
|
|
21
21
|
debug(message: string, context?: LoggerContext): void;
|
|
22
22
|
info(message: string, context?: LoggerContext): void;
|
|
23
23
|
warn(message: string, context?: LoggerContext): void;
|
|
24
|
-
error(error: Error | null, message?: string): void;
|
|
25
|
-
fatal(error: Error | null, message?: string): void;
|
|
24
|
+
error(error: Error | null, message?: string, context?: LoggerContext): void;
|
|
25
|
+
fatal(error: Error | null, message?: string, context?: LoggerContext): void;
|
|
26
26
|
};
|
|
27
27
|
export declare function getLogger(name?: string): Logger;
|
|
28
28
|
export declare function getLoggers(): Map<string, {
|
|
29
29
|
debug(message: string, context?: LoggerContext): void;
|
|
30
30
|
info(message: string, context?: LoggerContext): void;
|
|
31
31
|
warn(message: string, context?: LoggerContext): void;
|
|
32
|
-
error(error: Error | null, message?: string | undefined): void;
|
|
33
|
-
fatal(error: Error | null, message?: string | undefined): void;
|
|
32
|
+
error(error: Error | null, message?: string | undefined, context?: LoggerContext): void;
|
|
33
|
+
fatal(error: Error | null, message?: string | undefined, context?: LoggerContext): void;
|
|
34
34
|
}>;
|
package/dist/logger.js
CHANGED
|
@@ -43,6 +43,7 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
|
|
|
43
43
|
level: LEVELS_TO_NAME[level],
|
|
44
44
|
hostname,
|
|
45
45
|
pid,
|
|
46
|
+
name,
|
|
46
47
|
message,
|
|
47
48
|
...context,
|
|
48
49
|
time: new Date().toISOString(),
|
|
@@ -62,23 +63,29 @@ export function createLogger({ name, getContext, silent = () => process.env.NODE
|
|
|
62
63
|
warn(message, context = {}) {
|
|
63
64
|
return log(LEVELS.WARN, message, getFinalContext(context));
|
|
64
65
|
},
|
|
65
|
-
error(error, message) {
|
|
66
|
-
const
|
|
66
|
+
error(error, message, context = {}) {
|
|
67
|
+
const finalContext = getFinalContext(context);
|
|
67
68
|
if (_sentryClient != null) {
|
|
68
69
|
_sentryClient.captureException(error, {
|
|
69
|
-
tags:
|
|
70
|
+
tags: finalContext,
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
|
-
return log(LEVELS.ERROR, message || error?.message || '',
|
|
73
|
+
return log(LEVELS.ERROR, message || error?.message || '', {
|
|
74
|
+
...finalContext,
|
|
75
|
+
err: error?.stack,
|
|
76
|
+
});
|
|
73
77
|
},
|
|
74
|
-
fatal(error, message) {
|
|
75
|
-
const
|
|
78
|
+
fatal(error, message, context = {}) {
|
|
79
|
+
const finalContext = getFinalContext(context);
|
|
76
80
|
if (_sentryClient != null) {
|
|
77
81
|
_sentryClient.captureException(error, {
|
|
78
|
-
tags:
|
|
82
|
+
tags: finalContext,
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
|
-
return log(LEVELS.FATAL, message || error?.message || '',
|
|
85
|
+
return log(LEVELS.FATAL, message || error?.message || '', {
|
|
86
|
+
...finalContext,
|
|
87
|
+
err: error?.stack,
|
|
88
|
+
});
|
|
82
89
|
},
|
|
83
90
|
};
|
|
84
91
|
_loggers.set(name, api);
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import bodyParser from 'body-parser';
|
|
2
2
|
import type { RequestHandler } from 'express';
|
|
3
|
-
import { type ErrorHandlerOptions } from './errors.js';
|
|
4
3
|
type UrlencodedParser = typeof bodyParser.urlencoded;
|
|
5
4
|
type JsonParser = typeof bodyParser.json;
|
|
6
5
|
export declare const urlencodedParser: UrlencodedParser;
|
|
@@ -23,17 +22,10 @@ export declare function before({ cookies, helmet: useHelmet, json: useJson, allo
|
|
|
23
22
|
* unknown routes.
|
|
24
23
|
*/
|
|
25
24
|
export declare function notFound(): RequestHandler;
|
|
26
|
-
export interface AfterMiddlewareOptions {
|
|
27
|
-
onError?: ErrorHandlerOptions['onError'];
|
|
28
|
-
}
|
|
29
25
|
/**
|
|
30
26
|
* Register middlewares that must come after the app routes.
|
|
31
|
-
*
|
|
32
|
-
* Currently attaches:
|
|
33
|
-
* - notFound()
|
|
34
27
|
*/
|
|
35
|
-
|
|
36
|
-
export declare function after(): After;
|
|
28
|
+
export declare function after(): RequestHandler;
|
|
37
29
|
/**
|
|
38
30
|
* Alias of `route()`
|
|
39
31
|
*/
|
package/dist/middleware.js
CHANGED
|
@@ -84,8 +84,25 @@ export function notFound() {
|
|
|
84
84
|
next(NotFound(`Could not ${req.method} ${req.path}`));
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* In production, we could directly proxy the url
|
|
89
|
+
* to an analytics service from nginx, but in dev
|
|
90
|
+
* we serve a dummy script to avoid 404.
|
|
91
|
+
*/
|
|
92
|
+
function analyticsProxy() {
|
|
93
|
+
return (req, res, next) => {
|
|
94
|
+
if (req.originalUrl === '/analytics.js') {
|
|
95
|
+
res.setHeader('Content-type', 'text/javascript');
|
|
96
|
+
return res.status(200).end('// no-op analytics proxy');
|
|
97
|
+
}
|
|
98
|
+
next();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Register middlewares that must come after the app routes.
|
|
103
|
+
*/
|
|
87
104
|
export function after() {
|
|
88
|
-
return notFound();
|
|
105
|
+
return compose([analyticsProxy(), notFound()]);
|
|
89
106
|
}
|
|
90
107
|
/**
|
|
91
108
|
* Alias of `route()`
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { Express, RequestHandler } from 'express';
|
|
2
|
+
export declare function createDocsRouter(app: Express, extras?: Record<string, any>): RequestHandler;
|
package/dist/routes.js
CHANGED
|
@@ -1,41 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.toString()
|
|
11
|
-
.replace('/?', '')
|
|
12
|
-
.replace('(?=/|$)', '$')
|
|
13
|
-
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\/]|[^.*+?^${}()|[\]\\/])*)\$\//);
|
|
14
|
-
return match
|
|
15
|
-
? match[1].replace(/\\(.)/g, '$1')
|
|
16
|
-
: '<complex:' + value.toString() + '>';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function getRoutesOfLayer(path, layer) {
|
|
20
|
-
if (layer.method) {
|
|
21
|
-
return [{ method: layer.method.toUpperCase(), path }];
|
|
22
|
-
}
|
|
23
|
-
else if (layer.route) {
|
|
24
|
-
return getRoutesOfLayer(path + split(layer.route.path), layer.route.stack[0]);
|
|
25
|
-
}
|
|
26
|
-
else if (layer.name === 'router' && layer.handle.stack) {
|
|
27
|
-
let routes = [];
|
|
28
|
-
layer.handle.stack.forEach(function (stackItem) {
|
|
29
|
-
routes = routes.concat(getRoutesOfLayer(path + split(layer.regexp), stackItem));
|
|
30
|
-
});
|
|
31
|
-
return routes;
|
|
32
|
-
}
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
export function listRoutes(app) {
|
|
36
|
-
const routes = [];
|
|
37
|
-
for (const layer of app._router.stack) {
|
|
38
|
-
routes.push(...getRoutesOfLayer('', layer));
|
|
39
|
-
}
|
|
40
|
-
return routes;
|
|
1
|
+
import { parseExpressApp } from 'express-route-parser';
|
|
2
|
+
import { route } from './middleware.js';
|
|
3
|
+
export function createDocsRouter(app, extras = {}) {
|
|
4
|
+
return route(() => {
|
|
5
|
+
return {
|
|
6
|
+
...extras,
|
|
7
|
+
routes: parseExpressApp(app),
|
|
8
|
+
};
|
|
9
|
+
});
|
|
41
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -8,17 +8,6 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"migrate": "bin/migrate"
|
|
10
10
|
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"start": "ts-node --esm example/index.ts",
|
|
13
|
-
"clean": "rm -rf dist",
|
|
14
|
-
"lint": "eslint src",
|
|
15
|
-
"test": "NODE_ENV=test vitest",
|
|
16
|
-
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
17
|
-
"prepublishOnly": "pnpm run clean && pnpm run build",
|
|
18
|
-
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
19
|
-
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|
|
20
|
-
"format": "pnpm run import-sort && pnpm run prettier"
|
|
21
|
-
},
|
|
22
11
|
"author": {
|
|
23
12
|
"name": "hanse",
|
|
24
13
|
"email": "hanse@koren.im"
|
|
@@ -47,6 +36,7 @@
|
|
|
47
36
|
"eslint-plugin-prettier": "^5.0.0",
|
|
48
37
|
"eslint-plugin-react": "^7.33.1",
|
|
49
38
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
39
|
+
"faktory-worker": "^4.5.1",
|
|
50
40
|
"import-sort-cli": "6.0.0",
|
|
51
41
|
"import-sort-style-module": "6.0.0",
|
|
52
42
|
"isomorphic-fetch": "3.0.0",
|
|
@@ -65,6 +55,7 @@
|
|
|
65
55
|
"@types/cookie-parser": "^1.4.3",
|
|
66
56
|
"@types/express": "^4.17.17",
|
|
67
57
|
"@types/jsonwebtoken": "^9.0.2",
|
|
58
|
+
"@types/koa-compose": "^3.2.5",
|
|
68
59
|
"@types/nodemailer": "^6.4.9",
|
|
69
60
|
"@types/pg": "^8.10.2",
|
|
70
61
|
"@types/uuid": "^9.0.2",
|
|
@@ -75,6 +66,7 @@
|
|
|
75
66
|
"cookie-parser": "^1.4.6",
|
|
76
67
|
"dotenv": "^16.3.1",
|
|
77
68
|
"express": "^4.18.2",
|
|
69
|
+
"express-route-parser": "^1.0.5",
|
|
78
70
|
"helmet": "^7.0.0",
|
|
79
71
|
"jsonwebtoken": "^9.0.1",
|
|
80
72
|
"meow": "^12.0.1",
|
|
@@ -99,5 +91,19 @@
|
|
|
99
91
|
"@typescript-eslint/no-shadow": "off",
|
|
100
92
|
"react-hooks/rules-of-hooks": "off"
|
|
101
93
|
}
|
|
94
|
+
},
|
|
95
|
+
"peerDependencies": {
|
|
96
|
+
"faktory-worker": "^4.5.1"
|
|
97
|
+
},
|
|
98
|
+
"scripts": {
|
|
99
|
+
"start": "ts-node --esm example/index.ts",
|
|
100
|
+
"start:worker": "ts-node --esm example/worker.ts",
|
|
101
|
+
"clean": "rm -rf dist",
|
|
102
|
+
"lint": "eslint src",
|
|
103
|
+
"test": "NODE_ENV=test vitest",
|
|
104
|
+
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
105
|
+
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
106
|
+
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|
|
107
|
+
"format": "pnpm run import-sort && pnpm run prettier"
|
|
102
108
|
}
|
|
103
|
-
}
|
|
109
|
+
}
|