jh-be-tools 1.0.90 → 1.0.91
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/api-request/apiRequest.d.ts +27 -0
- package/dist/api-request/apiRequest.js +110 -0
- package/dist/config/eslint.config.d.mts +3 -0
- package/dist/config/eslint.config.mjs +74 -0
- package/dist/config/tsconfig.base.json +18 -0
- package/dist/correlation-id/correlationContext.d.ts +5 -0
- package/dist/correlation-id/correlationContext.js +4 -0
- package/dist/correlation-id/correlationMiddleware.d.ts +2 -0
- package/dist/correlation-id/correlationMiddleware.js +11 -0
- package/dist/cursor/cursor.d.ts +7 -0
- package/dist/cursor/cursor.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +8 -0
- package/dist/route-handler/routeCreation.d.ts +14 -0
- package/dist/route-handler/routeCreation.js +12 -0
- package/dist/route-handler/routeHandler.d.ts +22 -0
- package/dist/route-handler/routeHandler.js +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AxiosRequestConfig, AxiosResponse, AxiosHeaders } from 'axios';
|
|
2
|
+
import { RouteDef } from '../route-handler/routeCreation.js';
|
|
3
|
+
export interface QueryData {
|
|
4
|
+
params?: Record<string, string>;
|
|
5
|
+
query?: AxiosRequestConfig['params'];
|
|
6
|
+
headers?: Record<string, string> | AxiosHeaders;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface Request {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
definition: RouteDef;
|
|
12
|
+
serviceName: string;
|
|
13
|
+
queryData?: QueryData;
|
|
14
|
+
}
|
|
15
|
+
export interface ProcessingMethods {
|
|
16
|
+
pre?: (request: QueryData) => Promise<QueryData>;
|
|
17
|
+
post?: (response: AxiosResponse | InternalError) => Promise<AxiosResponse | InternalError>;
|
|
18
|
+
logger?: (value: string) => void;
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface InternalError {
|
|
22
|
+
serviceName: string;
|
|
23
|
+
status: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const isInternalError: <T>(res: AxiosResponse<T> | InternalError) => res is InternalError;
|
|
26
|
+
export declare const isValidResponse: <T>(res: AxiosResponse<T> | InternalError) => res is AxiosResponse<T>;
|
|
27
|
+
export declare const request: <Response>(req: Request, processing?: ProcessingMethods) => Promise<Response>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
import { getCorrelationId } from '../correlation-id/correlationContext.js';
|
|
3
|
+
export const isInternalError = (res) => {
|
|
4
|
+
return res.status >= 400;
|
|
5
|
+
};
|
|
6
|
+
export const isValidResponse = (res) => {
|
|
7
|
+
return res.status < 400;
|
|
8
|
+
};
|
|
9
|
+
export const request = async (req, processing) => {
|
|
10
|
+
const { baseUrl, definition, serviceName } = req;
|
|
11
|
+
let { queryData } = req;
|
|
12
|
+
let path = `${baseUrl}${definition.path}`;
|
|
13
|
+
let func;
|
|
14
|
+
if (processing?.pre) {
|
|
15
|
+
if (!queryData) {
|
|
16
|
+
queryData = {};
|
|
17
|
+
}
|
|
18
|
+
queryData = await processing.pre(queryData);
|
|
19
|
+
}
|
|
20
|
+
switch (definition.method) {
|
|
21
|
+
case 'get':
|
|
22
|
+
if (queryData?.body) {
|
|
23
|
+
throw Error('Get requests with body are not supported.');
|
|
24
|
+
}
|
|
25
|
+
func = axios.get;
|
|
26
|
+
break;
|
|
27
|
+
case 'post':
|
|
28
|
+
func = axios.post;
|
|
29
|
+
if (queryData && !queryData?.body) {
|
|
30
|
+
queryData.body = {};
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case 'put':
|
|
34
|
+
func = axios.put;
|
|
35
|
+
if (queryData && !queryData?.body) {
|
|
36
|
+
queryData.body = {};
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case 'delete':
|
|
40
|
+
if (queryData?.body) {
|
|
41
|
+
throw Error('Delete requests with body are not supported.');
|
|
42
|
+
}
|
|
43
|
+
func = axios.delete;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (queryData?.params) {
|
|
47
|
+
path = replaceUrlParams(path, queryData.params);
|
|
48
|
+
}
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await baseQuery(func, path, queryData);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
response = errorHandler(error, { serviceName, path, queryData, debug: processing?.debug, logger: processing?.logger });
|
|
55
|
+
}
|
|
56
|
+
if (processing?.post) {
|
|
57
|
+
response = await processing.post(response);
|
|
58
|
+
}
|
|
59
|
+
if (isInternalError(response)) {
|
|
60
|
+
throw response;
|
|
61
|
+
}
|
|
62
|
+
return response.data;
|
|
63
|
+
};
|
|
64
|
+
const baseQuery = async (func, path, req) => {
|
|
65
|
+
const correlationId = getCorrelationId();
|
|
66
|
+
const options = {
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': 'application/json',
|
|
69
|
+
'x-correlation-id': correlationId,
|
|
70
|
+
...req?.headers,
|
|
71
|
+
},
|
|
72
|
+
params: req?.query,
|
|
73
|
+
};
|
|
74
|
+
if (req?.body) {
|
|
75
|
+
return await func(path, req.body, options);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return await func(path, options);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const errorHandler = (error, { serviceName, path, queryData, debug, logger }) => {
|
|
82
|
+
if (!(error instanceof AxiosError))
|
|
83
|
+
throw error;
|
|
84
|
+
const data = {
|
|
85
|
+
serviceName: serviceName,
|
|
86
|
+
status: error.status ?? 500,
|
|
87
|
+
request: {
|
|
88
|
+
path,
|
|
89
|
+
query: queryData?.query,
|
|
90
|
+
body: debug ? queryData?.body : undefined,
|
|
91
|
+
},
|
|
92
|
+
response: error.response ? {
|
|
93
|
+
status: error.response.status,
|
|
94
|
+
data: error.response.data,
|
|
95
|
+
headers: error.response.headers,
|
|
96
|
+
} : undefined,
|
|
97
|
+
};
|
|
98
|
+
if (logger) {
|
|
99
|
+
logger(JSON.stringify(data));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.error(JSON.stringify(data));
|
|
103
|
+
}
|
|
104
|
+
return data;
|
|
105
|
+
};
|
|
106
|
+
const replaceUrlParams = (url, params) => {
|
|
107
|
+
return url.replace(/:([a-zA-Z0-9_]+)/g, (match, key) => {
|
|
108
|
+
return params[key] || match;
|
|
109
|
+
});
|
|
110
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import eslint from '@eslint/js';
|
|
3
|
+
import tsEslint from 'typescript-eslint';
|
|
4
|
+
import importPlugin from 'eslint-plugin-import';
|
|
5
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
6
|
+
import eslintPluginPrettier from 'eslint-plugin-prettier';
|
|
7
|
+
import unusedImports from 'eslint-plugin-unused-imports';
|
|
8
|
+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
9
|
+
/** @type {import('@eslint/js').FlatConfig[]} */
|
|
10
|
+
const config = [
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
...tsEslint.configs.recommendedTypeChecked,
|
|
13
|
+
importPlugin.flatConfigs.recommended,
|
|
14
|
+
importPlugin.flatConfigs.typescript,
|
|
15
|
+
prettierConfig,
|
|
16
|
+
{
|
|
17
|
+
settings: {
|
|
18
|
+
'import/parsers': {
|
|
19
|
+
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
20
|
+
},
|
|
21
|
+
'import/resolver': {
|
|
22
|
+
typescript: {
|
|
23
|
+
project: tsconfigPath,
|
|
24
|
+
},
|
|
25
|
+
node: {
|
|
26
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
plugins: {
|
|
31
|
+
'unused-imports': unusedImports,
|
|
32
|
+
prettier: eslintPluginPrettier,
|
|
33
|
+
},
|
|
34
|
+
languageOptions: {
|
|
35
|
+
parser: tsEslint.parser,
|
|
36
|
+
ecmaVersion: 6,
|
|
37
|
+
sourceType: 'module',
|
|
38
|
+
parserOptions: {
|
|
39
|
+
project: [tsconfigPath],
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
ecmaFeatures: {
|
|
42
|
+
modules: true,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
rules: {
|
|
47
|
+
'@typescript-eslint/explicit-member-accessibility': 0,
|
|
48
|
+
'@typescript-eslint/explicit-function-return-type': 0,
|
|
49
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
50
|
+
'@typescript-eslint/no-unused-vars': [
|
|
51
|
+
'error',
|
|
52
|
+
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_', ignoreRestSiblings: true },
|
|
53
|
+
],
|
|
54
|
+
'@typescript-eslint/no-floating-promises': ['error', { ignoreIIFE: true, ignoreVoid: false }],
|
|
55
|
+
'no-return-await': 'off',
|
|
56
|
+
'@typescript-eslint/return-await': ['error', 'always'],
|
|
57
|
+
'require-await': 'off',
|
|
58
|
+
'@typescript-eslint/require-await': 'error',
|
|
59
|
+
'prettier/prettier': ['error', { singleQuote: true }],
|
|
60
|
+
'import/order': ['error', { 'newlines-between': 'always', alphabetize: { order: 'asc' } }],
|
|
61
|
+
'import/order:/no-named-as-default-member': 0,
|
|
62
|
+
'unused-imports/no-unused-imports': 'error',
|
|
63
|
+
'@typescript-eslint/no-non-null-assertion': 'error',
|
|
64
|
+
eqeqeq: 2,
|
|
65
|
+
'no-console': 2,
|
|
66
|
+
'no-unused-expressions': 2,
|
|
67
|
+
curly: 'error',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
ignores: ['node_modules', '**/build', '**/coverage', '**/dist/**', 'eslint.config.mjs'],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
export default config;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"incremental": true,
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"exclude": ["**/*.spec.ts", "**/gen.ts"],
|
|
17
|
+
"include": ["src/**/*.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// correlationMiddleware.js
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { als } from './correlationContext.js';
|
|
4
|
+
export function correlationMiddleware(req, res, next) {
|
|
5
|
+
const incoming = req.header('x-correlation-id');
|
|
6
|
+
// Regenerate if it looks suspicious — don't trust arbitrary client input verbatim
|
|
7
|
+
const correlationId = isValidId(incoming) ? incoming : randomUUID();
|
|
8
|
+
res.setHeader('x-correlation-id', correlationId ?? randomUUID());
|
|
9
|
+
als.run({ correlationId }, () => next());
|
|
10
|
+
}
|
|
11
|
+
const isValidId = (id) => typeof id === 'string' && /^[a-zA-Z0-9._-]{1,128}$/.test(id);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './route-handler/routeHandler.js';
|
|
2
|
+
export * from './route-handler/routeCreation.js';
|
|
3
|
+
export * from './api-request/apiRequest.js';
|
|
4
|
+
export * from './cursor/cursor.js';
|
|
5
|
+
export * from './correlation-id/correlationMiddleware.js';
|
|
6
|
+
export * from './logger.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './route-handler/routeHandler.js';
|
|
2
|
+
export * from './route-handler/routeCreation.js';
|
|
3
|
+
export * from './api-request/apiRequest.js';
|
|
4
|
+
export * from './cursor/cursor.js';
|
|
5
|
+
export * from './correlation-id/correlationMiddleware.js';
|
|
6
|
+
export * from './logger.js';
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import { RouteSchema } from './routeHandler.js';
|
|
3
|
+
type Next = (err?: unknown) => void;
|
|
4
|
+
export type Middleware = (req: Request, res: Response, next: Next) => void | Promise<void>;
|
|
5
|
+
export type RouteMethods = 'get' | 'post' | 'put' | 'delete';
|
|
6
|
+
export type RouteDef = {
|
|
7
|
+
method: RouteMethods;
|
|
8
|
+
path: string;
|
|
9
|
+
routeSchema: RouteSchema;
|
|
10
|
+
middleware?: Middleware[];
|
|
11
|
+
};
|
|
12
|
+
export type RouteData = [RouteDef, (req?: any) => any];
|
|
13
|
+
export declare const createRoutes: (routeData: RouteData[], router: Router) => void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { routeHandler } from './routeHandler.js';
|
|
2
|
+
const createRoute = (meta, route) => {
|
|
3
|
+
return (app) => {
|
|
4
|
+
const middlewares = meta.middleware ?? [];
|
|
5
|
+
app[meta.method](meta.path, ...middlewares, async (req, res) => {
|
|
6
|
+
await routeHandler(req, res, route, meta.routeSchema);
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export const createRoutes = (routeData, router) => {
|
|
11
|
+
routeData.forEach((data) => createRoute(...data)(router));
|
|
12
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export type RouteSchema<ReqBody extends z.ZodTypeAny = z.ZodTypeAny, ResBody extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
4
|
+
request?: z.ZodObject<{
|
|
5
|
+
body?: z.ZodObject<{
|
|
6
|
+
data: ReqBody;
|
|
7
|
+
}> | undefined;
|
|
8
|
+
params?: z.ZodTypeAny;
|
|
9
|
+
query?: z.ZodTypeAny;
|
|
10
|
+
headers?: z.ZodTypeAny;
|
|
11
|
+
}>;
|
|
12
|
+
response?: z.ZodObject<{
|
|
13
|
+
data?: ResBody | undefined;
|
|
14
|
+
meta?: z.ZodTypeAny;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
interface ReturnValue<T> {
|
|
18
|
+
data: T;
|
|
19
|
+
status?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const routeHandler: (req: Request, res: Response, fun: (req?: any) => Promise<ReturnValue<unknown> | void>, schema: RouteSchema) => Promise<Response<any, Record<string, any>> | undefined>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const routeHandler = async (req, res, fun, schema) => {
|
|
2
|
+
if (!(await parseRequest(schema, req, res)))
|
|
3
|
+
return;
|
|
4
|
+
let data;
|
|
5
|
+
let status;
|
|
6
|
+
try {
|
|
7
|
+
const result = await fun(req);
|
|
8
|
+
data = result;
|
|
9
|
+
const defaultStatus = result?.data ? 200 : 204;
|
|
10
|
+
status = result?.status ?? defaultStatus;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof Error) {
|
|
14
|
+
return res.status(500).json(error.message);
|
|
15
|
+
}
|
|
16
|
+
return res.status(500).json(error);
|
|
17
|
+
}
|
|
18
|
+
if (!(await parseResponse(schema, res, data)))
|
|
19
|
+
return;
|
|
20
|
+
res.status(status).json(data);
|
|
21
|
+
};
|
|
22
|
+
const parseRequest = async (schema, req, res) => {
|
|
23
|
+
if (schema.request) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = await schema.request.parseAsync({
|
|
26
|
+
body: req.body,
|
|
27
|
+
query: req.query,
|
|
28
|
+
params: req.params,
|
|
29
|
+
headers: req.headers,
|
|
30
|
+
});
|
|
31
|
+
if (parsed.body !== undefined)
|
|
32
|
+
req.body = parsed.body;
|
|
33
|
+
if (parsed.query !== undefined)
|
|
34
|
+
Object.assign(req.query, parsed.query);
|
|
35
|
+
if (parsed.params !== undefined)
|
|
36
|
+
Object.assign(req.params, parsed.params);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.log(JSON.stringify(error));
|
|
40
|
+
res.status(400).json(error);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
const parseResponse = async (schema, res, data) => {
|
|
47
|
+
if (schema.response) {
|
|
48
|
+
try {
|
|
49
|
+
await schema.response.parseAsync(data);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.log(JSON.stringify(error));
|
|
53
|
+
res.status(500).json(error);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
};
|