@devmoods/express-extras 0.35.1 → 0.37.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/dist/cache.d.ts +6 -1
- package/dist/cache.js +23 -1
- package/dist/cli.js +1 -1
- package/dist/email.d.ts +2 -2
- package/dist/email.js +2 -9
- package/dist/faktory.d.ts +14 -8
- package/dist/faktory.js +8 -1
- package/dist/listen.d.ts +1 -1
- package/dist/listen.js +12 -4
- package/dist/manage.js +15 -4
- package/dist/redis.js +5 -3
- package/dist/routes.d.ts +20 -0
- package/dist/routes.js +159 -2
- package/dist/setupTests.js +2 -0
- package/dist/validate.js +9 -3
- package/package.json +9 -10
package/dist/cache.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type RedisClient } from './redis.js';
|
|
2
|
+
export interface CacheOptions {
|
|
3
|
+
key?: string;
|
|
4
|
+
expiresMs?: number | null;
|
|
5
|
+
}
|
|
2
6
|
export interface CacheAdapter {
|
|
3
7
|
get(key: string): Promise<any>;
|
|
4
8
|
set(key: string, value: any, expires?: number | null | undefined): Promise<void>;
|
|
@@ -13,5 +17,6 @@ export declare function createCache(adapter: CacheAdapter): {
|
|
|
13
17
|
set: (key: string, value: any, expires?: number | null | undefined) => Promise<void>;
|
|
14
18
|
get: (key: string) => Promise<any>;
|
|
15
19
|
clear: (key?: string) => Promise<void>;
|
|
16
|
-
wrap: <T>(key: string,
|
|
20
|
+
wrap: <T>(key: string, options: CacheOptions | number | null, fn: () => Promise<T>) => Promise<T>;
|
|
21
|
+
decorate: <T, Arguments extends any[]>(options: CacheOptions | number | null, fn: (...args: Arguments) => T | Promise<T>) => (...args: Arguments) => Promise<T>;
|
|
17
22
|
};
|
package/dist/cache.js
CHANGED
|
@@ -53,19 +53,41 @@ export function createMemoryCacheAdapter() {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
export function createCache(adapter) {
|
|
56
|
-
async function cached(key,
|
|
56
|
+
async function cached(key, options, fn) {
|
|
57
57
|
const cachedValue = await adapter.get(key);
|
|
58
58
|
if (cachedValue != null) {
|
|
59
59
|
return cachedValue;
|
|
60
60
|
}
|
|
61
|
+
const expiresMs = typeof options === 'number' || options == null
|
|
62
|
+
? options
|
|
63
|
+
: options.expiresMs;
|
|
61
64
|
const result = await fn();
|
|
62
65
|
await adapter.set(key, result, expiresMs);
|
|
63
66
|
return result;
|
|
64
67
|
}
|
|
68
|
+
function cachedFn(options, fn) {
|
|
69
|
+
return async (...args) => {
|
|
70
|
+
const baseKey = typeof options === 'object' && options != null
|
|
71
|
+
? options.key
|
|
72
|
+
: undefined;
|
|
73
|
+
const cacheKey = `${baseKey ?? Buffer.from(fn.toString()).toString('hex')}-${JSON.stringify(args)}`;
|
|
74
|
+
const cachedValue = await adapter.get(cacheKey);
|
|
75
|
+
if (cachedValue != null) {
|
|
76
|
+
return cachedValue;
|
|
77
|
+
}
|
|
78
|
+
const expiresMs = typeof options === 'number' || options == null
|
|
79
|
+
? options
|
|
80
|
+
: options.expiresMs;
|
|
81
|
+
const result = await fn(...args);
|
|
82
|
+
await adapter.set(cacheKey, result, expiresMs);
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
65
86
|
return {
|
|
66
87
|
set: adapter.set.bind(adapter),
|
|
67
88
|
get: adapter.get.bind(adapter),
|
|
68
89
|
clear: adapter.clear.bind(adapter),
|
|
69
90
|
wrap: cached,
|
|
91
|
+
decorate: cachedFn,
|
|
70
92
|
};
|
|
71
93
|
}
|
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ export class CLI {
|
|
|
31
31
|
const timer = startTimer();
|
|
32
32
|
try {
|
|
33
33
|
await action(...args);
|
|
34
|
-
this.greenLine(`Command succeeded in ${timer.getDuration().toFixed(4)}s
|
|
34
|
+
this.greenLine(`Command succeeded in ${timer.getDuration().toFixed(4)}s`, stderr);
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
37
|
this.redLine(error.message);
|
package/dist/email.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ export interface EmailOptions {
|
|
|
6
6
|
pass?: string;
|
|
7
7
|
senderName: string;
|
|
8
8
|
senderAddress: string;
|
|
9
|
+
onEmailSent?: (payload: Record<string, any>) => void | Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
export interface SendEmailOptions extends nodemailer.SendMailOptions {
|
|
11
12
|
message: string;
|
|
12
13
|
}
|
|
13
|
-
export declare function createEmail({ host, port, user, pass, senderName, senderAddress, }: EmailOptions): {
|
|
14
|
+
export declare function createEmail({ host, port, user, pass, senderName, senderAddress, onEmailSent, }: EmailOptions): {
|
|
14
15
|
send: ({ to, subject, message, ...rest }: SendEmailOptions) => Promise<void>;
|
|
15
16
|
transporter: nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo, import("nodemailer/lib/smtp-transport/index.js").Options>;
|
|
16
|
-
readonly outbox: Record<string, any>[];
|
|
17
17
|
};
|
package/dist/email.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import * as nodemailer from 'nodemailer';
|
|
2
2
|
import { getLogger } from './logger.js';
|
|
3
3
|
const logger = getLogger('email');
|
|
4
|
-
export function createEmail({ host, port, user, pass, senderName, senderAddress, }) {
|
|
4
|
+
export function createEmail({ host, port, user, pass, senderName, senderAddress, onEmailSent, }) {
|
|
5
5
|
const transporter = nodemailer.createTransport({
|
|
6
6
|
host,
|
|
7
7
|
port: parseInt(port || '587', 10),
|
|
8
8
|
secure: false,
|
|
9
9
|
auth: { user, pass },
|
|
10
10
|
});
|
|
11
|
-
const outbox = [];
|
|
12
11
|
const send = async ({ to, subject, message, ...rest }) => {
|
|
13
12
|
const payload = {
|
|
14
13
|
from: `${senderName} <${senderAddress}>`,
|
|
@@ -18,17 +17,11 @@ export function createEmail({ host, port, user, pass, senderName, senderAddress,
|
|
|
18
17
|
...rest,
|
|
19
18
|
};
|
|
20
19
|
const result = await transporter.sendMail(payload);
|
|
21
|
-
|
|
22
|
-
if (outbox.length > 5) {
|
|
23
|
-
outbox.shift();
|
|
24
|
-
}
|
|
20
|
+
await onEmailSent?.(payload);
|
|
25
21
|
logger.info('email-sent', { email: result });
|
|
26
22
|
};
|
|
27
23
|
return {
|
|
28
24
|
send,
|
|
29
25
|
transporter,
|
|
30
|
-
get outbox() {
|
|
31
|
-
return outbox;
|
|
32
|
-
},
|
|
33
26
|
};
|
|
34
27
|
}
|
package/dist/faktory.d.ts
CHANGED
|
@@ -3,26 +3,32 @@ export interface JobsOptions {
|
|
|
3
3
|
queueName: string;
|
|
4
4
|
faktory?: typeof create;
|
|
5
5
|
}
|
|
6
|
-
export type
|
|
7
|
-
export
|
|
8
|
-
|
|
6
|
+
export type AnyJobContext = Record<string, any>;
|
|
7
|
+
export interface JobOptions {
|
|
8
|
+
retry?: number;
|
|
9
|
+
priority?: number;
|
|
10
|
+
at?: Date | string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createJobs<GlobalContext extends AnyJobContext>({ queueName, faktory: createFactory, }: JobsOptions): {
|
|
13
|
+
job: <Arguments extends Record<string, any>, Fn extends (input: Arguments, ctx: GlobalContext) => undefined | Promise<any>>(optionsOrFn: JobOptions | Fn, fnOrUndefined?: Fn) => {
|
|
9
14
|
$job: boolean;
|
|
10
15
|
jobName: string;
|
|
16
|
+
options: JobOptions;
|
|
11
17
|
readonly executionCount: number;
|
|
12
|
-
delay(args?: Parameters<
|
|
13
|
-
execute(args: Parameters<
|
|
18
|
+
delay(args?: Parameters<Fn>[0]): Promise<`${string}-${string}-${string}-${string}-${string}`>;
|
|
19
|
+
execute(args: Parameters<Fn>[0], context: GlobalContext): Promise<any>;
|
|
14
20
|
};
|
|
15
21
|
queueName: string;
|
|
16
22
|
};
|
|
17
23
|
export type Job = ReturnType<ReturnType<typeof createJobs>['job']>;
|
|
18
|
-
export interface FaktoryWorkerOptions {
|
|
24
|
+
export interface FaktoryWorkerOptions<GlobalContext> {
|
|
19
25
|
jobs: unknown[];
|
|
20
26
|
queueName: string;
|
|
21
27
|
faktory?: typeof create;
|
|
22
|
-
jobContext?:
|
|
28
|
+
jobContext?: GlobalContext;
|
|
23
29
|
}
|
|
24
30
|
export declare function useJobId(): string | undefined;
|
|
25
|
-
export declare function createFaktoryWorker({ queueName, jobs, faktory: createFactory, jobContext, }: FaktoryWorkerOptions): Promise<{
|
|
31
|
+
export declare function createFaktoryWorker<GlobalContext>({ queueName, jobs, faktory: createFactory, jobContext, }: FaktoryWorkerOptions<GlobalContext>): Promise<{
|
|
26
32
|
start(workerOptions?: WorkerOptions): Promise<void>;
|
|
27
33
|
stop(): Promise<void>;
|
|
28
34
|
}>;
|
package/dist/faktory.js
CHANGED
|
@@ -7,7 +7,12 @@ const logger = getLogger('faktory');
|
|
|
7
7
|
const JOB_ID = 'jobId';
|
|
8
8
|
export function createJobs({ queueName, faktory: createFactory = create, }) {
|
|
9
9
|
const faktory = createFactory();
|
|
10
|
-
function job(
|
|
10
|
+
function job(optionsOrFn, fnOrUndefined) {
|
|
11
|
+
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : fnOrUndefined;
|
|
12
|
+
const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn;
|
|
13
|
+
if (!fn) {
|
|
14
|
+
throw new Error('job() must get a function has the first or second argument');
|
|
15
|
+
}
|
|
11
16
|
const jobName = fn.name;
|
|
12
17
|
let executionCount = 0;
|
|
13
18
|
if (!jobName) {
|
|
@@ -16,6 +21,7 @@ export function createJobs({ queueName, faktory: createFactory = create, }) {
|
|
|
16
21
|
return {
|
|
17
22
|
$job: true,
|
|
18
23
|
jobName,
|
|
24
|
+
options,
|
|
19
25
|
get executionCount() {
|
|
20
26
|
return executionCount;
|
|
21
27
|
},
|
|
@@ -26,6 +32,7 @@ export function createJobs({ queueName, faktory: createFactory = create, }) {
|
|
|
26
32
|
const j = client.job(jobName, args);
|
|
27
33
|
j.jid = jid;
|
|
28
34
|
j.queue = queueName;
|
|
35
|
+
Object.assign(j, options);
|
|
29
36
|
await j.push();
|
|
30
37
|
await client.close();
|
|
31
38
|
}
|
package/dist/listen.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface StoppableServer extends Server {
|
|
|
7
7
|
export declare function stoppable(server: Server, grace?: number): StoppableServer;
|
|
8
8
|
interface ExpressServerOptions {
|
|
9
9
|
onShutdown?(): Promise<void> | void;
|
|
10
|
-
healthCheck?(): Promise<unknown>;
|
|
10
|
+
healthCheck?(type?: 'live' | 'ready'): Promise<unknown>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createExpressServer(app: Application, options?: ExpressServerOptions): {
|
|
13
13
|
start(): Promise<void>;
|
package/dist/listen.js
CHANGED
|
@@ -6,6 +6,7 @@ import {} from 'express';
|
|
|
6
6
|
import { getLogger } from './logger.js';
|
|
7
7
|
import { before, route } from './middleware.js';
|
|
8
8
|
import { envPort, readEnv } from './utils.js';
|
|
9
|
+
import { validateQuery } from './validate.js';
|
|
9
10
|
const logger = getLogger('http');
|
|
10
11
|
const READINESS_PERIOD = readEnv.number('READINESS_PERIOD_MS', 0);
|
|
11
12
|
const GRACE_PERIOD = readEnv.number('GRACE_PERIOD_MS', 20000);
|
|
@@ -96,16 +97,23 @@ export function createExpressServer(app, options = {}) {
|
|
|
96
97
|
process.on(signal, onSignal);
|
|
97
98
|
}
|
|
98
99
|
if (!app.get('__healthAttached')) {
|
|
99
|
-
app.get('/health', before(),
|
|
100
|
+
app.get('/health', before(), validateQuery({
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
type: { type: 'string', enum: ['live', 'ready'] },
|
|
104
|
+
},
|
|
105
|
+
}), route(async (req) => {
|
|
100
106
|
const timestamp = new Date().toISOString();
|
|
101
107
|
try {
|
|
102
108
|
if (stopping) {
|
|
103
109
|
throw new Error();
|
|
104
110
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
111
|
+
const type = req.query.type;
|
|
112
|
+
const meta = await options.healthCheck?.(type);
|
|
113
|
+
return { status: 'ok', type, meta, timestamp };
|
|
107
114
|
}
|
|
108
|
-
catch {
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.error(error, 'Failed health check');
|
|
109
117
|
return [503, { status: 'error', timestamp }];
|
|
110
118
|
}
|
|
111
119
|
}));
|
package/dist/manage.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { program } from 'commander';
|
|
2
2
|
import {} from 'express';
|
|
3
|
-
import { parseExpressApp } from 'express-route-parser';
|
|
4
3
|
import { hashPassword } from './auth.js';
|
|
5
4
|
import { CLI } from './cli.js';
|
|
6
5
|
import { asCli } from './migrate.js';
|
|
6
|
+
import { parseRoutes } from './routes.js';
|
|
7
7
|
export function registerManagementCommand(register) {
|
|
8
8
|
const cli = new CLI({ printStackTraces: process.argv.includes('--verbose') });
|
|
9
9
|
register(program.command.bind(program), cli);
|
|
@@ -17,12 +17,23 @@ export function registerDefaultCommands(app) {
|
|
|
17
17
|
const migrate = command('migrate');
|
|
18
18
|
asCli(migrate, cli);
|
|
19
19
|
});
|
|
20
|
-
registerManagementCommand((command, cli) => command('routes')
|
|
20
|
+
registerManagementCommand((command, cli) => command('routes')
|
|
21
|
+
.option('--openapi', 'Output OpenAPI schema')
|
|
22
|
+
.option('--json', 'Output JSON')
|
|
23
|
+
.action(cli.wrap(async (options) => {
|
|
21
24
|
if (!app)
|
|
22
25
|
return;
|
|
23
|
-
|
|
26
|
+
if (options.openapi) {
|
|
27
|
+
console.error('Not implemented yet');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const routes = parseRoutes(app);
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
24
35
|
for (const route of routes) {
|
|
25
|
-
cli.writeLine(`${route.method.
|
|
36
|
+
cli.writeLine(`${route.method.padEnd(7)} ${route.path}\n`);
|
|
26
37
|
}
|
|
27
38
|
})));
|
|
28
39
|
registerManagementCommand((command, cli) => {
|
package/dist/redis.js
CHANGED
|
@@ -2,8 +2,10 @@ import { createClient } from 'redis';
|
|
|
2
2
|
import { readEnv } from './utils.js';
|
|
3
3
|
export function createRedis({ url = readEnv('REDIS_URL', ''), onError, } = {}) {
|
|
4
4
|
const client = createClient({ url });
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Always attach a listener even if
|
|
6
|
+
// onError is not provided.
|
|
7
|
+
client.on('error', (error) => {
|
|
8
|
+
onError?.(error);
|
|
9
|
+
});
|
|
8
10
|
return client;
|
|
9
11
|
}
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import { type Express, type RequestHandler } from 'express';
|
|
2
2
|
export declare function createDocsRouter(app: Express, extras?: Record<string, any>): RequestHandler;
|
|
3
|
+
export declare function parseRoutes(app: Express): {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
params: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
[k: string]: {
|
|
10
|
+
type: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
required: string[];
|
|
14
|
+
} | null;
|
|
15
|
+
query: {} | null;
|
|
16
|
+
body: {} | null;
|
|
17
|
+
}[];
|
|
18
|
+
export interface Key {
|
|
19
|
+
name: string;
|
|
20
|
+
optional: boolean;
|
|
21
|
+
offset: number;
|
|
22
|
+
}
|
package/dist/routes.js
CHANGED
|
@@ -1,11 +1,168 @@
|
|
|
1
1
|
import {} from 'express';
|
|
2
|
-
import { parseExpressApp } from 'express-route-parser';
|
|
3
2
|
import { route } from './middleware.js';
|
|
4
3
|
export function createDocsRouter(app, extras = {}) {
|
|
5
4
|
return route(async () => {
|
|
6
5
|
return {
|
|
7
6
|
...extras,
|
|
8
|
-
routes:
|
|
7
|
+
routes: parseRoutes(app),
|
|
9
8
|
};
|
|
10
9
|
});
|
|
11
10
|
}
|
|
11
|
+
export function parseRoutes(app) {
|
|
12
|
+
return new ExpressPathParser(app).getPaths().map((routeMetadata) => {
|
|
13
|
+
const { method, path, pathParams, metadata } = routeMetadata;
|
|
14
|
+
return {
|
|
15
|
+
method: method.toLocaleUpperCase(),
|
|
16
|
+
path,
|
|
17
|
+
params: buildPathParamsSchema(pathParams),
|
|
18
|
+
query: metadata
|
|
19
|
+
?.filter((m) => typeof m === 'object' && m != null && 'querySchema' in m)
|
|
20
|
+
.at(-1)?.querySchema ?? null,
|
|
21
|
+
body: metadata
|
|
22
|
+
?.filter((m) => typeof m === 'object' && m != null && 'bodySchema' in m)
|
|
23
|
+
.at(-1)?.bodySchema ?? null,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const buildPathParamsSchema = (pathParams) => {
|
|
28
|
+
const required = [];
|
|
29
|
+
const params = [];
|
|
30
|
+
for (const param of pathParams) {
|
|
31
|
+
if (param.in !== 'path') {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
params.push(param.name);
|
|
35
|
+
if (param.required) {
|
|
36
|
+
required.push(param.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (params.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: Object.fromEntries(params.map((p) => [p, { type: 'string' }])),
|
|
45
|
+
required,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
class ExpressPathParser {
|
|
49
|
+
getPaths() {
|
|
50
|
+
return this._appPaths;
|
|
51
|
+
}
|
|
52
|
+
constructor(app) {
|
|
53
|
+
/**
|
|
54
|
+
* Recursive traversal method for the express router and middleware tree.
|
|
55
|
+
*
|
|
56
|
+
* @param path The current path segment that we have traversed so far
|
|
57
|
+
* @param layer The current 'layer' of the router tree
|
|
58
|
+
* @param keys The keys for the parameter's in the current path branch of the traversal
|
|
59
|
+
* @returns void - base case saves result to internal object
|
|
60
|
+
*/
|
|
61
|
+
this.traverse = (path, layer, keys) => {
|
|
62
|
+
keys = [...keys, ...layer.keys];
|
|
63
|
+
if (layer.name === 'router' && layer.handle) {
|
|
64
|
+
layer.handle.stack.forEach((l) => {
|
|
65
|
+
path = path || '';
|
|
66
|
+
this.traverse(`${path}/${pathRegexParser(layer.regexp, layer.keys)}`, l, keys);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!layer.route || !layer.route.stack.length) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this._appPaths.push(this.parseRouteLayer(layer, keys, path));
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Parses a route object. Route objects are the leafs of an express router tree
|
|
76
|
+
*
|
|
77
|
+
* @param layer The layer of this route object - represents the stack of middleware and other meta data
|
|
78
|
+
* @param keys The full set of keys for this particular route
|
|
79
|
+
* @param basePath The base path as it was initial declared for this route
|
|
80
|
+
* @returns A ExpressPath object holding the meta data for a given route
|
|
81
|
+
*/
|
|
82
|
+
this.parseRouteLayer = (layer, keys, basePath) => {
|
|
83
|
+
const lastRequestHandler = layer.route.stack[layer.route.stack.length - 1];
|
|
84
|
+
const pathParams = keys.map((key) => {
|
|
85
|
+
return { name: key.name, in: 'path', required: !key.optional };
|
|
86
|
+
});
|
|
87
|
+
const filtered = layer.route.stack.filter((element) => element?.handle?.metadata);
|
|
88
|
+
const path = (basePath + layer.route.path).replace(/\/{2,}/gi, '/');
|
|
89
|
+
if (filtered.length === 0) {
|
|
90
|
+
return { path, pathParams, method: lastRequestHandler.method };
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
path,
|
|
94
|
+
pathParams,
|
|
95
|
+
method: lastRequestHandler.method,
|
|
96
|
+
metadata: filtered.map((f) => f.handle.metadata),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
this._appPaths = [];
|
|
100
|
+
const router = app._router;
|
|
101
|
+
if (router) {
|
|
102
|
+
for (const layer of router.stack) {
|
|
103
|
+
this.traverse('', layer, []);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Parses an express layer's regex and converts it to the original format seen in code.
|
|
109
|
+
*
|
|
110
|
+
* @param layerRegexPath The layer's regex pattern
|
|
111
|
+
* @param keys The keys that represent the layer's path parameters
|
|
112
|
+
* @returns The path string for that layer
|
|
113
|
+
* Code inspired and modify from:
|
|
114
|
+
* https://github.com/expressjs/express/issues/3308#issuecomment-300957572
|
|
115
|
+
*/
|
|
116
|
+
const pathRegexParser = (layerRegexPath, keys) => {
|
|
117
|
+
if (typeof layerRegexPath === 'string') {
|
|
118
|
+
return layerRegexPath;
|
|
119
|
+
}
|
|
120
|
+
if (layerRegexPath.fast_slash) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
if (layerRegexPath.fast_star) {
|
|
124
|
+
return '*';
|
|
125
|
+
}
|
|
126
|
+
let mappedPath = '';
|
|
127
|
+
if (keys && keys.length) {
|
|
128
|
+
mappedPath = mapKeysToPath(layerRegexPath, keys);
|
|
129
|
+
}
|
|
130
|
+
const match = layerRegexPath
|
|
131
|
+
.toString()
|
|
132
|
+
.replace('\\/?', '')
|
|
133
|
+
.replace('(?=\\/|$)', '$')
|
|
134
|
+
.match(/^\/\^((?:\\[.*+?^${}()|[\]\\/]|[^.*+?^${}()|[\]\\/])*)\$\//);
|
|
135
|
+
if (match) {
|
|
136
|
+
return match[1].replace(/\\(.)/g, '$1').slice(1);
|
|
137
|
+
}
|
|
138
|
+
if (mappedPath) {
|
|
139
|
+
return mappedPath.slice(1);
|
|
140
|
+
}
|
|
141
|
+
return layerRegexPath.toString();
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Map's the keys/path variables to the regex inside a given path
|
|
145
|
+
*
|
|
146
|
+
* @param layerRegexPath The regex for a react router with path parameters
|
|
147
|
+
* @param keys The keys that represent the path parameters
|
|
148
|
+
* @returns The regex for a path variable converted to original string on the express route
|
|
149
|
+
*/
|
|
150
|
+
const mapKeysToPath = (layerRegexPath, keys) => {
|
|
151
|
+
if (!keys || keys.length === 0) {
|
|
152
|
+
throw Error('must include atleast one key to map');
|
|
153
|
+
}
|
|
154
|
+
let convertedSubPath = layerRegexPath.toString();
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
if (key.optional) {
|
|
157
|
+
convertedSubPath = convertedSubPath.replace('(?:\\/([^\\/]+?))?\\', `/:${key.name}?`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
convertedSubPath = convertedSubPath.replace('(?:([^\\/]+?))', `:${key.name}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return convertedSubPath
|
|
164
|
+
.replace('/?(?=\\/|$)/i', '')
|
|
165
|
+
.replace('/^', '')
|
|
166
|
+
.replace(/\\/gi, '')
|
|
167
|
+
.replace(/\/{2,}/gi, '/');
|
|
168
|
+
};
|
package/dist/setupTests.js
CHANGED
package/dist/validate.js
CHANGED
|
@@ -4,33 +4,39 @@ import { BadRequest } from './errors.js';
|
|
|
4
4
|
const Ajv = _Ajv;
|
|
5
5
|
export function validateBody(schema) {
|
|
6
6
|
const validate = compileValidateSchema(schema);
|
|
7
|
-
|
|
7
|
+
const middleware = (req, res, next) => {
|
|
8
8
|
const result = validate(req.body);
|
|
9
9
|
if (!result.isValid) {
|
|
10
10
|
return next(BadRequest({ body: result.errors }));
|
|
11
11
|
}
|
|
12
12
|
next();
|
|
13
13
|
};
|
|
14
|
+
middleware.metadata = { bodySchema: schema };
|
|
15
|
+
return middleware;
|
|
14
16
|
}
|
|
15
17
|
export function validateQuery(schema) {
|
|
16
18
|
const validate = compileValidateSchema(schema, { coerceTypes: true });
|
|
17
|
-
|
|
19
|
+
const middleware = (req, res, next) => {
|
|
18
20
|
const result = validate(req.query);
|
|
19
21
|
if (!result.isValid) {
|
|
20
22
|
return next(BadRequest({ query: result.errors }));
|
|
21
23
|
}
|
|
22
24
|
next();
|
|
23
25
|
};
|
|
26
|
+
middleware.metadata = { querySchema: schema };
|
|
27
|
+
return middleware;
|
|
24
28
|
}
|
|
25
29
|
export function validateParams(schema) {
|
|
26
30
|
const validate = compileValidateSchema(schema, { coerceTypes: true });
|
|
27
|
-
|
|
31
|
+
const middleware = (req, res, next) => {
|
|
28
32
|
const result = validate(req.params);
|
|
29
33
|
if (!result.isValid) {
|
|
30
34
|
return next(BadRequest({ params: result.errors }));
|
|
31
35
|
}
|
|
32
36
|
next();
|
|
33
37
|
};
|
|
38
|
+
middleware.metadata = { paramsSchema: schema };
|
|
39
|
+
return middleware;
|
|
34
40
|
}
|
|
35
41
|
function createAjv(options) {
|
|
36
42
|
const ajv = new Ajv({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@devmoods/eslint-config": "^1.14.0",
|
|
22
22
|
"@types/koa-compose": "^3.2.8",
|
|
23
|
-
"@types/node": "22.
|
|
23
|
+
"@types/node": "22.8.1",
|
|
24
24
|
"@types/supertest": "^6.0.2",
|
|
25
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
26
|
-
"@typescript-eslint/parser": "^8.
|
|
27
|
-
"@vitest/coverage-v8": "^2.1.
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
|
26
|
+
"@typescript-eslint/parser": "^8.11.0",
|
|
27
|
+
"@vitest/coverage-v8": "^2.1.3",
|
|
28
28
|
"chalk": "^5.3.0",
|
|
29
29
|
"eslint": "^8.57.0",
|
|
30
30
|
"eslint-plugin-import": "^2.31.0",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"ts-node": "10.9.2",
|
|
36
36
|
"tsx": "^4.19.1",
|
|
37
37
|
"typescript": "5.6.3",
|
|
38
|
-
"vitest": "^2.1.
|
|
38
|
+
"vitest": "^2.1.3",
|
|
39
39
|
"vitest-fail-on-console": "^0.7.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@devmoods/fetch": "^4.0.1",
|
|
43
|
-
"@sentry/node": "^8.
|
|
44
|
-
"@sentry/profiling-node": "^8.
|
|
43
|
+
"@sentry/node": "^8.35.0",
|
|
44
|
+
"@sentry/profiling-node": "^8.35.0",
|
|
45
45
|
"@types/bcrypt": "^5.0.2",
|
|
46
46
|
"@types/body-parser": "^1.19.5",
|
|
47
47
|
"@types/cookie-parser": "^1.4.7",
|
|
@@ -59,11 +59,10 @@
|
|
|
59
59
|
"cookie-parser": "^1.4.7",
|
|
60
60
|
"dotenv": "^16.4.5",
|
|
61
61
|
"express": "^4.21.1",
|
|
62
|
-
"express-route-parser": "^1.0.5",
|
|
63
62
|
"helmet": "^8.0.0",
|
|
64
63
|
"jsonwebtoken": "^9.0.2",
|
|
65
64
|
"nodemailer": "^6.9.15",
|
|
66
|
-
"pg": "^8.13.
|
|
65
|
+
"pg": "^8.13.1",
|
|
67
66
|
"redis": "^4.7.0",
|
|
68
67
|
"sql-template-strings": "^2.2.2",
|
|
69
68
|
"uuid": "^10.0.0"
|