@devmoods/express-extras 0.26.2 → 0.28.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/bin/migrate +1 -1
- package/bin/routes +5 -0
- package/dist/__tests__/__migrations__/1_a.js +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +5 -3
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +10 -3
- package/dist/cli/migrate.js +3 -0
- package/dist/cli/routes.d.ts +1 -0
- package/dist/cli/routes.js +10 -0
- package/dist/context.js +1 -0
- package/dist/email.d.ts +1 -0
- package/dist/email.js +17 -4
- package/dist/errors.js +6 -7
- package/dist/faktory.d.ts +7 -3
- package/dist/faktory.js +10 -5
- package/dist/listen.d.ts +18 -12
- package/dist/listen.js +116 -46
- package/dist/logger.d.ts +4 -6
- package/dist/logger.js +6 -2
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +34 -16
- package/dist/postgres.js +7 -3
- package/dist/redis.d.ts +2 -2
- package/dist/redis.js +4 -2
- package/dist/sentry.js +1 -0
- package/dist/setupTests.js +0 -5
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +7 -0
- package/dist/validate.js +2 -1
- package/package.json +16 -21
- package/dist/cli.js +0 -3
- /package/dist/{cli.d.ts → cli/migrate.d.ts} +0 -0
package/bin/migrate
CHANGED
package/bin/routes
ADDED
package/dist/auth.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function createAuthToken(payload: any, secret?: string): string;
|
|
|
7
7
|
/**
|
|
8
8
|
* Decode the given JWT token.
|
|
9
9
|
*/
|
|
10
|
-
export declare function decodeAuthToken(token: string): {
|
|
10
|
+
export declare function decodeAuthToken(token: string, secret?: string): {
|
|
11
11
|
[x: string]: any;
|
|
12
12
|
iss?: string | undefined;
|
|
13
13
|
sub?: string | undefined;
|
package/dist/auth.js
CHANGED
|
@@ -29,9 +29,10 @@ export function createAuthToken(payload, secret = AUTH_SECRET) {
|
|
|
29
29
|
/**
|
|
30
30
|
* Decode the given JWT token.
|
|
31
31
|
*/
|
|
32
|
-
export function decodeAuthToken(token) {
|
|
32
|
+
export function decodeAuthToken(token, secret = AUTH_SECRET) {
|
|
33
33
|
try {
|
|
34
|
-
const decoded = jwt.verify(token.trim(),
|
|
34
|
+
const decoded = jwt.verify(token.trim(), secret);
|
|
35
|
+
/* c8 ignore next 3 */
|
|
35
36
|
if (typeof decoded === 'string') {
|
|
36
37
|
throw new Error('decoded value not an object');
|
|
37
38
|
}
|
|
@@ -141,7 +142,7 @@ export function EmailAndPasswordAuthProvider(options) {
|
|
|
141
142
|
},
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
|
-
export function createAuth({ getUserByToken, filterUser
|
|
145
|
+
export function createAuth({ getUserByToken, filterUser, redis, providers, }) {
|
|
145
146
|
const useCurrentUser = () => useContext('currentUser');
|
|
146
147
|
const setCurrentUser = (user) => setContext('currentUser', user);
|
|
147
148
|
const providerContext = {
|
|
@@ -271,6 +272,7 @@ export function parseAuthorizationHeader(req) {
|
|
|
271
272
|
if (!token) {
|
|
272
273
|
if (type) {
|
|
273
274
|
return type;
|
|
275
|
+
/* c8 ignore next 3 */
|
|
274
276
|
}
|
|
275
277
|
return null;
|
|
276
278
|
}
|
package/dist/cache.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ export interface CacheAdapter {
|
|
|
4
4
|
set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
5
5
|
clear(key?: string): Promise<void>;
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export interface RedisCacheAdapterOptions {
|
|
8
|
+
keyPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createRedisCacheAdapter(redis: RedisClient, options?: RedisCacheAdapterOptions): CacheAdapter;
|
|
8
11
|
export declare function createMemoryCacheAdapter(): CacheAdapter;
|
|
9
12
|
export declare function createCache(adapter: CacheAdapter): {
|
|
10
13
|
set: (key: string, value: any, expires?: number | null | undefined) => Promise<void>;
|
package/dist/cache.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
import {} from './redis.js';
|
|
2
|
+
export function createRedisCacheAdapter(redis, options = {}) {
|
|
3
|
+
const { keyPrefix = 'cache:' } = options;
|
|
4
|
+
const createKey = (key) => `${keyPrefix}${key}`;
|
|
2
5
|
return {
|
|
3
6
|
async get(key) {
|
|
4
|
-
const value = await redis.get(key);
|
|
7
|
+
const value = await redis.get(createKey(key));
|
|
5
8
|
if (value == null) {
|
|
6
9
|
return null;
|
|
7
10
|
}
|
|
8
11
|
return JSON.parse(value);
|
|
9
12
|
},
|
|
10
13
|
async set(key, value, expires) {
|
|
11
|
-
await redis.set(key, JSON.stringify(value), { PX: expires });
|
|
14
|
+
await redis.set(createKey(key), JSON.stringify(value), { PX: expires });
|
|
12
15
|
},
|
|
13
16
|
async clear(key) {
|
|
14
17
|
if (key) {
|
|
18
|
+
await redis.del(createKey(key));
|
|
19
|
+
}
|
|
20
|
+
const keys = await redis.keys(`${keyPrefix}*`);
|
|
21
|
+
for (const key of keys) {
|
|
15
22
|
await redis.del(key);
|
|
16
23
|
}
|
|
17
24
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFetch } from '@devmoods/fetch';
|
|
2
|
+
const port = process.env.PORT ?? 3000;
|
|
3
|
+
const fetch = createFetch({
|
|
4
|
+
getRootUrl: () => `http://localhost:${port}`,
|
|
5
|
+
getRequestId: () => 'routes',
|
|
6
|
+
});
|
|
7
|
+
const response = await fetch('/docs');
|
|
8
|
+
for (const route of response.jsonData.routes) {
|
|
9
|
+
process.stdout.write(`${route.method.toLocaleUpperCase().padEnd(5)} ${route.path}\n`);
|
|
10
|
+
}
|
package/dist/context.js
CHANGED
package/dist/email.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export interface SendEmailOptions extends nodemailer.SendMailOptions {
|
|
|
13
13
|
export declare function createEmail({ host, port, user, pass, senderName, senderAddress, }: EmailOptions): {
|
|
14
14
|
send: ({ to, subject, message, ...rest }: SendEmailOptions) => Promise<void>;
|
|
15
15
|
transporter: nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo>;
|
|
16
|
+
readonly outbox: Record<string, any>[];
|
|
16
17
|
};
|
package/dist/email.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as nodemailer from 'nodemailer';
|
|
2
2
|
import { getLogger } from './logger.js';
|
|
3
|
+
const logger = getLogger('email');
|
|
3
4
|
export function createEmail({ host, port, user, pass, senderName, senderAddress, }) {
|
|
4
5
|
const transporter = nodemailer.createTransport({
|
|
5
6
|
host,
|
|
@@ -7,15 +8,27 @@ export function createEmail({ host, port, user, pass, senderName, senderAddress,
|
|
|
7
8
|
secure: false,
|
|
8
9
|
auth: { user, pass },
|
|
9
10
|
});
|
|
11
|
+
const outbox = [];
|
|
10
12
|
const send = async ({ to, subject, message, ...rest }) => {
|
|
11
|
-
const
|
|
13
|
+
const payload = {
|
|
12
14
|
from: `${senderName} <${senderAddress}>`,
|
|
13
15
|
to,
|
|
14
16
|
subject,
|
|
15
17
|
text: message,
|
|
16
18
|
...rest,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
+
};
|
|
20
|
+
const result = await transporter.sendMail(payload);
|
|
21
|
+
outbox.push(payload);
|
|
22
|
+
if (outbox.length > 5) {
|
|
23
|
+
outbox.shift();
|
|
24
|
+
}
|
|
25
|
+
logger.info('email-sent', { email: result });
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
send,
|
|
29
|
+
transporter,
|
|
30
|
+
get outbox() {
|
|
31
|
+
return outbox;
|
|
32
|
+
},
|
|
19
33
|
};
|
|
20
|
-
return { send, transporter };
|
|
21
34
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { STATUS_CODES } from 'node:http';
|
|
2
1
|
import * as Sentry from '@sentry/node';
|
|
3
2
|
import { errors } from 'compose-middleware';
|
|
3
|
+
import { STATUS_CODES } from 'node:http';
|
|
4
4
|
import { DoesNotExistError, IntegrityError } from './postgres.js';
|
|
5
5
|
export class ApiError extends Error {
|
|
6
6
|
}
|
|
@@ -20,14 +20,15 @@ export function makeError(status, detail, payload) {
|
|
|
20
20
|
}
|
|
21
21
|
export function mapToProblem(error) {
|
|
22
22
|
if (error instanceof ApiError) {
|
|
23
|
-
const
|
|
23
|
+
const status = error.status || 500;
|
|
24
|
+
const statusMessage = STATUS_CODES[status];
|
|
24
25
|
return {
|
|
25
26
|
type: `/api/problems/${statusMessage
|
|
26
27
|
.toLocaleLowerCase()
|
|
27
28
|
.replace(/ /g, '-')}`,
|
|
28
29
|
title: statusMessage,
|
|
29
|
-
detail: error.message,
|
|
30
|
-
status
|
|
30
|
+
detail: error.message || '',
|
|
31
|
+
status,
|
|
31
32
|
...error.payload,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
@@ -35,6 +36,7 @@ export function mapToProblem(error) {
|
|
|
35
36
|
type: 'internal-error',
|
|
36
37
|
title: 'Internal Server Error',
|
|
37
38
|
status: 500,
|
|
39
|
+
detail: '',
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
function convertToAPIError(error) {
|
|
@@ -48,9 +50,6 @@ function convertToAPIError(error) {
|
|
|
48
50
|
}
|
|
49
51
|
function _errorHandler({ onError }) {
|
|
50
52
|
return (err, req, res, next) => {
|
|
51
|
-
if (!err) {
|
|
52
|
-
err = makeError(500);
|
|
53
|
-
}
|
|
54
53
|
onError?.(err);
|
|
55
54
|
const apiError = convertToAPIError(err);
|
|
56
55
|
const problem = mapToProblem(apiError);
|
package/dist/faktory.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { create } from 'faktory-worker';
|
|
1
2
|
export interface JobsOptions {
|
|
2
3
|
queueName: string;
|
|
4
|
+
faktory?: typeof create;
|
|
3
5
|
}
|
|
4
|
-
export declare function createJobs({ queueName }: JobsOptions): {
|
|
6
|
+
export declare function createJobs({ queueName, faktory: createFactory, }: JobsOptions): {
|
|
5
7
|
job: <K extends Record<string, any>, T extends (input: K) => void | Promise<any>>(fn: T) => {
|
|
6
8
|
$job: boolean;
|
|
7
9
|
jobName: string;
|
|
8
10
|
readonly executionCount: number;
|
|
9
|
-
delay(args?: Parameters<T>[0] | undefined): Promise
|
|
11
|
+
delay(args?: Parameters<T>[0] | undefined): Promise<`${string}-${string}-${string}-${string}-${string}`>;
|
|
10
12
|
execute(args: Parameters<T>[0]): Promise<any>;
|
|
11
13
|
};
|
|
12
14
|
queueName: string;
|
|
@@ -15,8 +17,10 @@ export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
|
|
|
15
17
|
export interface FaktoryWorkerOptions {
|
|
16
18
|
jobs: unknown[];
|
|
17
19
|
queueName: string;
|
|
20
|
+
faktory?: typeof create;
|
|
18
21
|
}
|
|
19
22
|
export declare function useJobId(): string | undefined;
|
|
20
|
-
export declare function createFaktoryWorker({ queueName, jobs, }: FaktoryWorkerOptions): Promise<{
|
|
23
|
+
export declare function createFaktoryWorker({ queueName, jobs, faktory: createFactory, }: FaktoryWorkerOptions): Promise<{
|
|
21
24
|
start(): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
22
26
|
}>;
|
package/dist/faktory.js
CHANGED
|
@@ -5,12 +5,12 @@ import { getLogger } from './logger.js';
|
|
|
5
5
|
import { startTimer } from './utils.js';
|
|
6
6
|
const logger = getLogger('faktory');
|
|
7
7
|
const JOB_ID = 'jobId';
|
|
8
|
-
export function createJobs({ queueName }) {
|
|
9
|
-
const faktory =
|
|
8
|
+
export function createJobs({ queueName, faktory: createFactory = create, }) {
|
|
9
|
+
const faktory = createFactory();
|
|
10
10
|
function job(fn) {
|
|
11
11
|
const jobName = fn.name;
|
|
12
12
|
let executionCount = 0;
|
|
13
|
-
if (jobName
|
|
13
|
+
if (!jobName) {
|
|
14
14
|
throw new Error('Can not create job without a name');
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
@@ -35,7 +35,9 @@ export function createJobs({ queueName }) {
|
|
|
35
35
|
jobName,
|
|
36
36
|
args: JSON.stringify(args),
|
|
37
37
|
});
|
|
38
|
+
throw error;
|
|
38
39
|
}
|
|
40
|
+
return jid;
|
|
39
41
|
},
|
|
40
42
|
async execute(args) {
|
|
41
43
|
try {
|
|
@@ -59,8 +61,8 @@ export function useJobId() {
|
|
|
59
61
|
return store.get(JOB_ID);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
export async function createFaktoryWorker({ queueName, jobs, }) {
|
|
63
|
-
const faktory =
|
|
64
|
+
export async function createFaktoryWorker({ queueName, jobs, faktory: createFactory = create, }) {
|
|
65
|
+
const faktory = createFactory();
|
|
64
66
|
const filter = (j) => !!Object.prototype.hasOwnProperty.call(j, '$job');
|
|
65
67
|
const availableJobs = jobs.filter(filter);
|
|
66
68
|
for (const job of availableJobs) {
|
|
@@ -99,5 +101,8 @@ export async function createFaktoryWorker({ queueName, jobs, }) {
|
|
|
99
101
|
process.exit(1);
|
|
100
102
|
}
|
|
101
103
|
},
|
|
104
|
+
async stop() {
|
|
105
|
+
return faktory.stop();
|
|
106
|
+
},
|
|
102
107
|
};
|
|
103
108
|
}
|
package/dist/listen.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
import type { Application } from 'express';
|
|
3
|
+
import type { Server } from 'node:http';
|
|
4
|
+
interface StoppableServer extends Server {
|
|
5
|
+
inspect(): Record<string, any>;
|
|
6
|
+
stop(): Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export declare function stoppable(server: Server, grace?: number): StoppableServer;
|
|
9
|
+
interface ExpressServerOptions {
|
|
10
|
+
onShutdown?(): Promise<void> | void;
|
|
11
|
+
healthCheck?(): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createExpressServer(app: Application, options?: ExpressServerOptions): {
|
|
14
|
+
start(): void;
|
|
15
|
+
stop(): Promise<boolean>;
|
|
16
|
+
terminate(): Promise<void>;
|
|
17
|
+
readonly port: number;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
package/dist/listen.js
CHANGED
|
@@ -1,56 +1,126 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { delay } from '@devmoods/fetch';
|
|
2
|
+
import * as http from 'node:http';
|
|
2
3
|
import { getLogger } from './logger.js';
|
|
3
|
-
import {
|
|
4
|
+
import { route } from './middleware.js';
|
|
5
|
+
import { envPort, readEnv } from './utils.js';
|
|
4
6
|
const logger = getLogger('http');
|
|
5
7
|
const READINESS_PERIOD_MS = readEnv.number('READINESS_PERIOD_MS', 0);
|
|
6
8
|
const FORCE_EXIT_TIMEOUT = readEnv.number('FORCE_EXIT_TIMEOUT', 5000);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
export function stoppable(server, grace = Infinity) {
|
|
10
|
+
let stopped = false;
|
|
11
|
+
let gracefully = true;
|
|
12
|
+
const requestsPerSocket = new Map();
|
|
13
|
+
const onConnection = (socket) => {
|
|
14
|
+
requestsPerSocket.set(socket, 0);
|
|
15
|
+
socket.once('close', () => requestsPerSocket.delete(socket));
|
|
16
|
+
};
|
|
17
|
+
const onRequest = (req, res) => {
|
|
18
|
+
requestsPerSocket.set(req.socket, requestsPerSocket.get(req.socket) + 1);
|
|
19
|
+
res.once('finish', () => {
|
|
20
|
+
const pending = requestsPerSocket.get(req.socket) - 1;
|
|
21
|
+
requestsPerSocket.set(req.socket, pending);
|
|
22
|
+
if (stopped && pending === 0) {
|
|
23
|
+
req.socket.end();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
function stop() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
setImmediate(() => {
|
|
30
|
+
stopped = true;
|
|
31
|
+
if (grace < Infinity) {
|
|
32
|
+
setTimeout(destroyAll, grace).unref();
|
|
33
|
+
}
|
|
34
|
+
server.close((e) => {
|
|
35
|
+
if (e) {
|
|
36
|
+
return reject(e);
|
|
37
|
+
}
|
|
38
|
+
resolve(gracefully);
|
|
39
|
+
});
|
|
40
|
+
for (const [socket, requests] of requestsPerSocket) {
|
|
41
|
+
if (requests === 0) {
|
|
42
|
+
socket.end();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function destroyAll() {
|
|
49
|
+
gracefully = false;
|
|
50
|
+
for (const socket of requestsPerSocket.keys()) {
|
|
51
|
+
socket.end();
|
|
52
|
+
}
|
|
53
|
+
setImmediate(() => {
|
|
54
|
+
for (const socket of requestsPerSocket.keys()) {
|
|
55
|
+
socket.destroy();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
server.on('connection', onConnection);
|
|
60
|
+
server.on('request', onRequest);
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
server.stop = stop;
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
server.inspect = () => {
|
|
65
|
+
return {
|
|
66
|
+
sockets: requestsPerSocket.size,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
return server;
|
|
70
|
+
}
|
|
71
|
+
export function createExpressServer(app, options = {}) {
|
|
72
|
+
const server = stoppable(http.createServer(app), FORCE_EXIT_TIMEOUT);
|
|
73
|
+
let stopping = false;
|
|
74
|
+
async function onSignal(signal) {
|
|
75
|
+
if (stopping) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
stopping = true;
|
|
79
|
+
try {
|
|
80
|
+
logger.info(`app shutting down due to ${signal}`);
|
|
81
|
+
await delay(READINESS_PERIOD_MS + 1000);
|
|
82
|
+
await server.stop();
|
|
83
|
+
await options.onShutdown?.();
|
|
84
|
+
logger.info('app shut down');
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.error(error, 'error during shutdown');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
93
|
+
process.on(signal, onSignal);
|
|
94
|
+
}
|
|
95
|
+
app.get('/health', route(async () => {
|
|
96
|
+
const timestamp = new Date().toISOString();
|
|
97
|
+
try {
|
|
98
|
+
if (stopping) {
|
|
99
|
+
throw new Error();
|
|
100
|
+
}
|
|
101
|
+
const meta = await options.healthCheck?.();
|
|
102
|
+
return { status: 'ok', meta, timestamp };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return [503, { status: 'error', timestamp }];
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
start() {
|
|
110
|
+
const port = app.get('port') ?? envPort(0);
|
|
111
|
+
server.listen(port, () => {
|
|
112
|
+
logger.info('app listening', { port: this.port });
|
|
23
113
|
});
|
|
24
114
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await options?.onShutdown?.();
|
|
115
|
+
stop() {
|
|
116
|
+
return server.stop();
|
|
28
117
|
},
|
|
29
|
-
|
|
30
|
-
|
|
118
|
+
terminate() {
|
|
119
|
+
return onSignal('SIGTERM');
|
|
31
120
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
const meta = await options?.healthCheck?.();
|
|
39
|
-
return {
|
|
40
|
-
meta,
|
|
41
|
-
timestamp: new Date().toISOString(),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
logger.error(error, 'health check failed');
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
verbatim: true,
|
|
121
|
+
get port() {
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
return server.address().port;
|
|
50
124
|
},
|
|
51
|
-
}
|
|
52
|
-
const port = options.port;
|
|
53
|
-
server.listen(port, () => {
|
|
54
|
-
logger.info('app listening', { port });
|
|
55
|
-
});
|
|
125
|
+
};
|
|
56
126
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
2
3
|
import type { CreateSentry } from './sentry.js';
|
|
3
4
|
export type LoggerContext = Record<string, any>;
|
|
4
5
|
export interface LoggerOptions {
|
|
@@ -10,16 +11,13 @@ export interface LoggerOptions {
|
|
|
10
11
|
* Context to attach to each log line
|
|
11
12
|
*/
|
|
12
13
|
getContext?: () => LoggerContext;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
silent?: () => boolean;
|
|
17
|
-
output?: NodeJS.WriteStream;
|
|
14
|
+
level?: string;
|
|
15
|
+
output?: Duplex;
|
|
18
16
|
}
|
|
19
17
|
export declare function attachSentryClientToLoggers(sentryClient: CreateSentry['client']): void;
|
|
20
18
|
export declare function setGlobalLoggerContext(getContext: () => LoggerContext): void;
|
|
21
19
|
export type Logger = ReturnType<typeof createLogger>;
|
|
22
|
-
export declare function createLogger({ name, getContext,
|
|
20
|
+
export declare function createLogger({ name, getContext, level, output, }: LoggerOptions): {
|
|
23
21
|
debug(message: string, context?: LoggerContext): void;
|
|
24
22
|
info(message: string, context?: LoggerContext): void;
|
|
25
23
|
warn(message: string, context?: LoggerContext): void;
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import { useRequestId } from './context.js';
|
|
3
|
+
import { readEnv } from './utils.js';
|
|
3
4
|
const LEVELS = {
|
|
4
5
|
TRACE: 10,
|
|
5
6
|
DEBUG: 20,
|
|
@@ -24,14 +25,17 @@ function defaultGetContext() {
|
|
|
24
25
|
..._globalGetContext?.(),
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
export function createLogger({ name, getContext,
|
|
28
|
+
export function createLogger({ name, getContext, level = readEnv('LOG_LEVEL', 'INFO'), output = process.stdout, }) {
|
|
28
29
|
if (_loggers.has(name)) {
|
|
29
30
|
throw new Error(`A logger named ${name} already exists`);
|
|
30
31
|
}
|
|
31
32
|
const hostname = os.hostname();
|
|
32
33
|
const pid = process.pid;
|
|
33
34
|
const env = process.env.NODE_ENV;
|
|
34
|
-
const
|
|
35
|
+
const logLevelName = level.toUpperCase();
|
|
36
|
+
const logLevel = logLevelName in LEVELS
|
|
37
|
+
? LEVELS[logLevelName]
|
|
38
|
+
: LEVELS.INFO;
|
|
35
39
|
function getFinalContext(context) {
|
|
36
40
|
return {
|
|
37
41
|
...defaultGetContext(),
|
package/dist/migrate.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface MigrationFileAPI {
|
|
|
10
10
|
down(fn: any): Promise<string> | Promise<void> | string | void;
|
|
11
11
|
}
|
|
12
12
|
export declare function cli(): Promise<void>;
|
|
13
|
+
export declare function runCli(input: string[], flags: Record<string, any>, showHelp?: () => void): Promise<number>;
|
|
13
14
|
export interface MigratorOptions {
|
|
14
15
|
postgres?: ReturnType<typeof createPostgres>;
|
|
15
16
|
migrationTableName?: string;
|
package/dist/migrate.js
CHANGED
|
@@ -14,8 +14,31 @@ Usage
|
|
|
14
14
|
description: 'PostgreSQL migration tool',
|
|
15
15
|
importMeta: import.meta,
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
let exitCode = 0;
|
|
18
|
+
try {
|
|
19
|
+
exitCode = await runCli(cli.input, cli.flags,
|
|
20
|
+
/* c8 ignore next 2 */
|
|
21
|
+
() => cli.showHelp());
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof CliError) {
|
|
25
|
+
exitCode = error.code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
process.exit(exitCode);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class CliError extends Error {
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function runCli(input, flags, showHelp) {
|
|
40
|
+
const verbose = !flags.silent;
|
|
41
|
+
const args = input.slice();
|
|
19
42
|
const action = args.shift();
|
|
20
43
|
function isValidAction(action) {
|
|
21
44
|
return action != null && ['init', 'up', 'down', 'create'].includes(action);
|
|
@@ -24,8 +47,8 @@ Usage
|
|
|
24
47
|
async function run(action, ...args) {
|
|
25
48
|
try {
|
|
26
49
|
if (!isValidAction(action)) {
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
showHelp?.();
|
|
51
|
+
throw new CliError(`Invalid action ${action}`, 2);
|
|
29
52
|
}
|
|
30
53
|
switch (action) {
|
|
31
54
|
case 'up':
|
|
@@ -43,20 +66,19 @@ Usage
|
|
|
43
66
|
break;
|
|
44
67
|
}
|
|
45
68
|
case 'create': {
|
|
46
|
-
const createdMigration = await migrator.create(args[0],
|
|
69
|
+
const createdMigration = await migrator.create(args[0], flags.typescript ? 'ts' : 'sql');
|
|
47
70
|
migrator.log(chalk.green('Created migration', createdMigration));
|
|
48
71
|
break;
|
|
49
72
|
}
|
|
50
73
|
}
|
|
74
|
+
return 0;
|
|
51
75
|
}
|
|
52
76
|
catch (error) {
|
|
53
77
|
console.error(chalk.red(error.message));
|
|
54
|
-
|
|
55
|
-
finally {
|
|
56
|
-
process.exit(0);
|
|
78
|
+
throw new CliError(error.message, 1);
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
|
-
run(action, ...args);
|
|
81
|
+
return run(action, ...args);
|
|
60
82
|
}
|
|
61
83
|
export class Migrator {
|
|
62
84
|
constructor({ postgres = createPostgres(), pathToMigrations = path.resolve(process.cwd(), 'db', 'migrations'), migrationTableName = 'migrations', verbose = true, } = {}) {
|
|
@@ -136,9 +158,6 @@ export class Migrator {
|
|
|
136
158
|
for (const migration of pendingMigrations) {
|
|
137
159
|
this.log('Running migration', migration.filename);
|
|
138
160
|
const migrator = await this.readMigrationFile(migration.filename);
|
|
139
|
-
if (!migrator.up || !migrator.down) {
|
|
140
|
-
throw new Error('Migrations must export both an up() and a down() function.');
|
|
141
|
-
}
|
|
142
161
|
if (migrator.useTransaction !== false) {
|
|
143
162
|
await this.postgres.transaction(async (tx) => {
|
|
144
163
|
await this.migrate(tx, migrator, direction);
|
|
@@ -166,9 +185,6 @@ export class Migrator {
|
|
|
166
185
|
}
|
|
167
186
|
this.log('Rolling back migration', migrationToRollback.filename);
|
|
168
187
|
const migrator = await this.readMigrationFile(migrationToRollback.filename);
|
|
169
|
-
if (!migrator.up || !migrator.down) {
|
|
170
|
-
throw new Error('Migrations must export both an up() and a down() function.');
|
|
171
|
-
}
|
|
172
188
|
if (migrator.useTransaction !== false) {
|
|
173
189
|
await this.postgres.transaction(async (tx) => {
|
|
174
190
|
await this.migrate(tx, migrator, 'down');
|
|
@@ -182,6 +198,7 @@ export class Migrator {
|
|
|
182
198
|
});
|
|
183
199
|
}
|
|
184
200
|
return await this.getCurrentVersion();
|
|
201
|
+
/* c8 ignore next 2 */
|
|
185
202
|
}
|
|
186
203
|
throw new Error(`Invalid direction ${direction}`);
|
|
187
204
|
}
|
|
@@ -216,6 +233,7 @@ export class Migrator {
|
|
|
216
233
|
else {
|
|
217
234
|
await returnValue;
|
|
218
235
|
}
|
|
236
|
+
/* c8 ignore next 3 */
|
|
219
237
|
}
|
|
220
238
|
catch (error) {
|
|
221
239
|
throw new Error(`Migration failed: ${error.message}`);
|
|
@@ -230,7 +248,7 @@ export class Migrator {
|
|
|
230
248
|
}
|
|
231
249
|
async getMigrations() {
|
|
232
250
|
const files = await fs.readdir(this.pathToMigrations);
|
|
233
|
-
return
|
|
251
|
+
return files
|
|
234
252
|
.map((filename) => filename.match(this.filenameRegExp))
|
|
235
253
|
.filter(notNullOrUndefined)
|
|
236
254
|
.map(([filename, version, name]) => ({
|
package/dist/postgres.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import pg from 'pg';
|
|
1
|
+
import pg, {} from 'pg';
|
|
2
2
|
import { SQL } from 'sql-template-strings';
|
|
3
3
|
import { readEnv } from './utils.js';
|
|
4
4
|
export class DoesNotExistError extends Error {
|
|
@@ -38,6 +38,7 @@ export class Connection {
|
|
|
38
38
|
const result = await this.client.query(text, values);
|
|
39
39
|
if (result.rows) {
|
|
40
40
|
return result.rows;
|
|
41
|
+
/* c8 ignore next 2*/
|
|
41
42
|
}
|
|
42
43
|
return [];
|
|
43
44
|
}
|
|
@@ -68,9 +69,11 @@ export class Transaction extends Connection {
|
|
|
68
69
|
*
|
|
69
70
|
* @param config
|
|
70
71
|
*/
|
|
71
|
-
export function createPostgres({ onError
|
|
72
|
+
export function createPostgres({ onError, url = readEnv('POSTGRES_URL'), ...config } = {}) {
|
|
72
73
|
const pool = new pg.Pool({ connectionString: url, ...config });
|
|
73
|
-
|
|
74
|
+
if (onError) {
|
|
75
|
+
pool.on('error', onError);
|
|
76
|
+
}
|
|
74
77
|
async function withConnection(callback) {
|
|
75
78
|
const client = await pool.connect();
|
|
76
79
|
try {
|
|
@@ -106,6 +109,7 @@ export function createPostgres({ onError = () => { }, url = readEnv('POSTGRES_UR
|
|
|
106
109
|
catch (error) {
|
|
107
110
|
await client.query('ROLLBACK');
|
|
108
111
|
throw error;
|
|
112
|
+
/* c8 ignore next 3 */
|
|
109
113
|
}
|
|
110
114
|
finally {
|
|
111
115
|
client.release();
|
package/dist/redis.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from 'redis';
|
|
2
2
|
export interface RedisOptions {
|
|
3
3
|
url?: string;
|
|
4
|
-
onError
|
|
4
|
+
onError?: (error: Error) => void;
|
|
5
5
|
}
|
|
6
6
|
export type RedisClient = ReturnType<typeof createClient>;
|
|
7
|
-
export declare function createRedis({ url, onError, }
|
|
7
|
+
export declare function createRedis({ url, onError, }?: RedisOptions): RedisClient;
|
package/dist/redis.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createClient } from 'redis';
|
|
2
2
|
import { readEnv } from './utils.js';
|
|
3
|
-
export function createRedis({ url = readEnv('REDIS_URL', ''), onError, }) {
|
|
3
|
+
export function createRedis({ url = readEnv('REDIS_URL', ''), onError, } = {}) {
|
|
4
4
|
const client = createClient({ url });
|
|
5
|
-
|
|
5
|
+
if (onError) {
|
|
6
|
+
client.on('error', onError);
|
|
7
|
+
}
|
|
6
8
|
return client;
|
|
7
9
|
}
|
package/dist/sentry.js
CHANGED
package/dist/setupTests.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import * as crypto from 'node:crypto';
|
|
3
3
|
// @ts-ignore
|
|
4
|
-
import createFetchMock from 'vitest-fetch-mock';
|
|
5
|
-
import { vi } from 'vitest';
|
|
6
|
-
const fetchMocker = createFetchMock(vi);
|
|
7
|
-
fetchMocker.enableMocks();
|
|
8
|
-
// @ts-ignore
|
|
9
4
|
globalThis.crypto = crypto;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { EventEmitter } from 'node:events';
|
|
1
3
|
/**
|
|
2
4
|
* Start a high resolution timer
|
|
3
5
|
*
|
|
@@ -14,4 +16,5 @@ export declare namespace readEnv {
|
|
|
14
16
|
declare function readEnvNumber(key: string, defaultValue: number): number;
|
|
15
17
|
declare function readEnvBoolean(key: string, defaultValue: boolean): boolean;
|
|
16
18
|
export declare const envPort: (defaultPort?: number) => number;
|
|
19
|
+
export declare function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown>;
|
|
17
20
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -25,3 +25,10 @@ function readEnvBoolean(key, defaultValue) {
|
|
|
25
25
|
return ['true', '1', 't'].includes(readEnv(key, `${defaultValue}`).toLowerCase());
|
|
26
26
|
}
|
|
27
27
|
export const envPort = (defaultPort = 3000) => readEnv.number('PORT', defaultPort);
|
|
28
|
+
export async function waitForEvent(emitter, event) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
emitter.once(event, (...args) => {
|
|
31
|
+
resolve([args]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/validate.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,16 +24,14 @@
|
|
|
24
24
|
"license": "ISC",
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@devmoods/eslint-config": "^1.2.0",
|
|
27
|
-
"@
|
|
28
|
-
"@types/
|
|
29
|
-
"@types/node": "20.5.3",
|
|
27
|
+
"@types/koa-compose": "^3.2.5",
|
|
28
|
+
"@types/node": "20.5.9",
|
|
30
29
|
"@types/supertest": "^2.0.12",
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
32
|
-
"@typescript-eslint/parser": "^6.
|
|
33
|
-
"@vitest/coverage-v8": "^0.34.
|
|
34
|
-
"@vitest/expect": "^0.34.2",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
|
31
|
+
"@typescript-eslint/parser": "^6.5.0",
|
|
32
|
+
"@vitest/coverage-v8": "^0.34.3",
|
|
35
33
|
"chalk": "^5.3.0",
|
|
36
|
-
"eslint": "^8.
|
|
34
|
+
"eslint": "^8.48.0",
|
|
37
35
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
38
36
|
"eslint-plugin-prettier": "^5.0.0",
|
|
39
37
|
"eslint-plugin-react": "^7.33.2",
|
|
@@ -41,27 +39,24 @@
|
|
|
41
39
|
"faktory-worker": "^4.5.1",
|
|
42
40
|
"import-sort-cli": "6.0.0",
|
|
43
41
|
"import-sort-style-module": "6.0.0",
|
|
44
|
-
"prettier": "3.0.
|
|
42
|
+
"prettier": "3.0.3",
|
|
45
43
|
"supertest": "6.3.3",
|
|
46
44
|
"ts-node": "10.9.1",
|
|
47
|
-
"typescript": "5.
|
|
48
|
-
"vitest": "^0.34.
|
|
49
|
-
"vitest-fetch-mock": "^0.2.2"
|
|
45
|
+
"typescript": "5.2.2",
|
|
46
|
+
"vitest": "^0.34.3"
|
|
50
47
|
},
|
|
51
48
|
"dependencies": {
|
|
52
49
|
"@devmoods/fetch": "^3.3.0",
|
|
53
|
-
"@
|
|
54
|
-
"@sentry/node": "^7.64.0",
|
|
50
|
+
"@sentry/node": "^7.66.0",
|
|
55
51
|
"@types/bcrypt": "^5.0.0",
|
|
56
52
|
"@types/body-parser": "^1.19.2",
|
|
57
53
|
"@types/cookie-parser": "^1.4.3",
|
|
58
54
|
"@types/express": "^4.17.17",
|
|
59
55
|
"@types/jsonwebtoken": "^9.0.2",
|
|
60
|
-
"@types/koa-compose": "^3.2.5",
|
|
61
56
|
"@types/nodemailer": "^6.4.9",
|
|
62
57
|
"@types/pg": "^8.10.2",
|
|
63
58
|
"@types/superagent": "^4.1.18",
|
|
64
|
-
"@types/uuid": "^9.0.
|
|
59
|
+
"@types/uuid": "^9.0.3",
|
|
65
60
|
"ajv": "^8.12.0",
|
|
66
61
|
"bcrypt": "^5.1.1",
|
|
67
62
|
"body-parser": "^1.20.2",
|
|
@@ -71,11 +66,11 @@
|
|
|
71
66
|
"express": "^4.18.2",
|
|
72
67
|
"express-route-parser": "^1.0.5",
|
|
73
68
|
"helmet": "^7.0.0",
|
|
74
|
-
"jsonwebtoken": "^9.0.
|
|
75
|
-
"meow": "^12.1.
|
|
69
|
+
"jsonwebtoken": "^9.0.2",
|
|
70
|
+
"meow": "^12.1.1",
|
|
76
71
|
"nodemailer": "^6.9.4",
|
|
77
72
|
"pg": "^8.11.3",
|
|
78
|
-
"redis": "^4.6.
|
|
73
|
+
"redis": "^4.6.8",
|
|
79
74
|
"sql-template-strings": "^2.2.2",
|
|
80
75
|
"uuid": "^9.0.0"
|
|
81
76
|
},
|
|
@@ -103,7 +98,7 @@
|
|
|
103
98
|
"start:worker": "ts-node --esm example/worker.ts",
|
|
104
99
|
"clean": "rm -rf dist",
|
|
105
100
|
"lint": "eslint src",
|
|
106
|
-
"test": "
|
|
101
|
+
"test": "./test.sh src",
|
|
107
102
|
"build": "pnpm run clean && tsc --project tsconfig.build.json",
|
|
108
103
|
"prettier": "prettier --write '**/*.{js,css,md,ts,tsx}'",
|
|
109
104
|
"import-sort": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
|
package/dist/cli.js
DELETED
|
File without changes
|