@yolo-croket-dev/core 1.9.14 → 1.10.0-jun.1
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/decorators/grpc-param.decorator.js +2 -5
- package/filters/grpc-exception.filter.js +3 -2
- package/functions/amqp-handler.function.js +6 -13
- package/functions/grpc-param-metadata.function.d.ts +22 -0
- package/functions/grpc-param-metadata.function.js +127 -0
- package/functions/index.d.ts +1 -0
- package/functions/index.js +1 -0
- package/interceptors/logging-service.interceptor.js +11 -11
- package/interceptors/rpc.interceptor.js +2 -2
- package/interfaces/exception.js +3 -3
- package/interfaces/log-level.enum.js +1 -1
- package/interfaces/msa-exception.d.ts +1 -11
- package/interfaces/msa-exception.js +1 -6
- package/modules/graceful-shutdown/graceful-shutdown.module.js +2 -2
- package/modules/graceful-shutdown/graceful-shutdown.rmq.d.ts +4 -10
- package/modules/graceful-shutdown/graceful-shutdown.rmq.js +2 -51
- package/modules/graceful-shutdown/graceful-shutdown.service.js +2 -2
- package/modules/redis.module.js +2 -2
- package/package.json +5 -2
- package/pipes/rpc-validation.pipe.js +2 -2
- package/util.js +4 -2
- package/CHANGELOG.md +0 -11
- package/tests/amqp-handler-timeout.test.cjs +0 -47
- package/tests/graceful-shutdown-rmq.test.cjs +0 -102
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GrpcParam = void 0;
|
|
4
4
|
const common_1 = require("@nestjs/common");
|
|
5
|
+
const grpc_param_metadata_function_1 = require("../functions/grpc-param-metadata.function");
|
|
5
6
|
exports.GrpcParam = (0, common_1.createParamDecorator)((data, ctx) => {
|
|
6
7
|
const metadata = ctx.switchToRpc().getContext();
|
|
7
|
-
|
|
8
|
-
if (!param || !param[0]) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
return JSON.parse(param[0]);
|
|
8
|
+
return (0, grpc_param_metadata_function_1.parseGrpcParamMetadata)(metadata);
|
|
12
9
|
});
|
|
@@ -60,6 +60,7 @@ let GrpcExceptionFilter = class GrpcExceptionFilter extends microservices_1.Base
|
|
|
60
60
|
message: msaException.errMessage || exception.message,
|
|
61
61
|
status: httpStatus,
|
|
62
62
|
action: msaException.errAction,
|
|
63
|
+
errOption: msaException.errOption,
|
|
63
64
|
};
|
|
64
65
|
metadata.set('error', Buffer.from(JSON.stringify(errInfo)).toString('base64'));
|
|
65
66
|
metadata.set('status', httpStatus.toString());
|
|
@@ -130,7 +131,7 @@ let GrpcExceptionFilter = class GrpcExceptionFilter extends microservices_1.Base
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
};
|
|
133
|
-
|
|
134
|
-
exports.GrpcExceptionFilter = GrpcExceptionFilter = __decorate([
|
|
134
|
+
GrpcExceptionFilter = __decorate([
|
|
135
135
|
(0, common_1.Catch)()
|
|
136
136
|
], GrpcExceptionFilter);
|
|
137
|
+
exports.GrpcExceptionFilter = GrpcExceptionFilter;
|
|
@@ -17,18 +17,11 @@ class HttpExceptionCustom extends common_1.HttpException {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
exports.HttpExceptionCustom = HttpExceptionCustom;
|
|
20
|
-
function resolveAmqpTimeout(timeoutMs) {
|
|
21
|
-
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
22
|
-
return DEFAULT_TIMEOUT;
|
|
23
|
-
}
|
|
24
|
-
return Math.min(timeoutMs, DEFAULT_TIMEOUT);
|
|
25
|
-
}
|
|
26
20
|
async function AmqpHandler(client, messagePattern, payload) {
|
|
27
|
-
var _a, _b, _c, _d
|
|
21
|
+
var _a, _b, _c, _d;
|
|
28
22
|
let data;
|
|
29
|
-
const timeoutMs = resolveAmqpTimeout((_a = payload === null || payload === void 0 ? void 0 : payload.param) === null || _a === void 0 ? void 0 : _a.timeout);
|
|
30
23
|
try {
|
|
31
|
-
data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(
|
|
24
|
+
data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(DEFAULT_TIMEOUT)));
|
|
32
25
|
if (data.isError) {
|
|
33
26
|
const status = data.status || 500;
|
|
34
27
|
const subMessage = data.subMessage;
|
|
@@ -36,7 +29,7 @@ async function AmqpHandler(client, messagePattern, payload) {
|
|
|
36
29
|
* 파일 객체 처리
|
|
37
30
|
* 다른 서비스에서 이미 처리가 될 수 있음
|
|
38
31
|
* */
|
|
39
|
-
(0, _1.reWriteFileValue)((
|
|
32
|
+
(0, _1.reWriteFileValue)((_a = data === null || data === void 0 ? void 0 : data.arg) === null || _a === void 0 ? void 0 : _a.file);
|
|
40
33
|
const errInfo = data.errInfo ? data.errInfo : {
|
|
41
34
|
message: data.errMessage,
|
|
42
35
|
type: data.errType,
|
|
@@ -57,7 +50,7 @@ async function AmqpHandler(client, messagePattern, payload) {
|
|
|
57
50
|
catch (err) {
|
|
58
51
|
if (typeof err === 'string' && err.includes('There is no matching message handler defined in the remote service.')) {
|
|
59
52
|
msaLogger.error('NOT_EXISTS_MESSAGE_PATTERN', {
|
|
60
|
-
traceId: ((
|
|
53
|
+
traceId: ((_b = payload === null || payload === void 0 ? void 0 : payload.param) === null || _b === void 0 ? void 0 : _b.traceId) || '-',
|
|
61
54
|
errInfo: {
|
|
62
55
|
errType: 'NOT_EXISTS_MESSAGE_PATTERN',
|
|
63
56
|
message: `${messagePattern} 해당 메시지 패턴은 존재하지 않습니다.`,
|
|
@@ -71,7 +64,7 @@ async function AmqpHandler(client, messagePattern, payload) {
|
|
|
71
64
|
}
|
|
72
65
|
else if (err instanceof rxjs_1.TimeoutError || err.message === DEFAULT_TIMEOUT_MESSAGE) {
|
|
73
66
|
msaLogger.error('TIMEOUT_ERROR', {
|
|
74
|
-
traceId: ((
|
|
67
|
+
traceId: ((_c = payload === null || payload === void 0 ? void 0 : payload.param) === null || _c === void 0 ? void 0 : _c.traceId) || '-',
|
|
75
68
|
errInfo: {
|
|
76
69
|
errType: 'TIMEOUT_ERROR',
|
|
77
70
|
message: DEFAULT_TIMEOUT_MESSAGE,
|
|
@@ -84,7 +77,7 @@ async function AmqpHandler(client, messagePattern, payload) {
|
|
|
84
77
|
});
|
|
85
78
|
}
|
|
86
79
|
else {
|
|
87
|
-
msaLogger.warn('AMQP_HANDLER_ERROR', { traceId: ((
|
|
80
|
+
msaLogger.warn('AMQP_HANDLER_ERROR', { traceId: ((_d = payload === null || payload === void 0 ? void 0 : payload.param) === null || _d === void 0 ? void 0 : _d.traceId) || '-' }, { err });
|
|
88
81
|
throw err;
|
|
89
82
|
}
|
|
90
83
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Metadata } from '@grpc/grpc-js';
|
|
2
|
+
import { AbstractParam } from '../interfaces';
|
|
3
|
+
export declare const GRPC_PARAM_METADATA_KEY = "param";
|
|
4
|
+
export declare type GrpcParamMetadata = AbstractParam;
|
|
5
|
+
/**
|
|
6
|
+
* @author jun
|
|
7
|
+
* @description gRPC metadata에 AMQP param 호환 컨텍스트를 저장합니다.
|
|
8
|
+
* @process 1. AbstractParam 검증 -> 2. JSON 직렬화 -> 3. metadata param key에 저장
|
|
9
|
+
*/
|
|
10
|
+
export declare function setGrpcParamMetadata(metadata: Metadata, param: GrpcParamMetadata): void;
|
|
11
|
+
/**
|
|
12
|
+
* @author jun
|
|
13
|
+
* @description gRPC metadata에서 AMQP param 호환 컨텍스트를 안전하게 읽습니다.
|
|
14
|
+
* @process 1. metadata param key 조회 -> 2. 문자열 검증 -> 3. JSON 파싱 실패 시 null 반환
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseGrpcParamMetadata(metadata: Metadata): GrpcParamMetadata | null;
|
|
17
|
+
/**
|
|
18
|
+
* @author jun
|
|
19
|
+
* @description gRPC metadata에서 필수 param 컨텍스트를 읽고 없거나 잘못되면 예외를 던집니다.
|
|
20
|
+
* @process 1. metadata param 파싱 -> 2. 유효성 확인 -> 3. 유효하지 않으면 BadRequest 발생
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertGrpcParamMetadata(metadata: Metadata): GrpcParamMetadata;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertGrpcParamMetadata = exports.parseGrpcParamMetadata = exports.setGrpcParamMetadata = exports.GRPC_PARAM_METADATA_KEY = void 0;
|
|
4
|
+
const bad_request_1 = require("../exceptions/bad-request");
|
|
5
|
+
exports.GRPC_PARAM_METADATA_KEY = 'param';
|
|
6
|
+
const OPTIONAL_STRING_KEYS = [
|
|
7
|
+
'userId',
|
|
8
|
+
'provider',
|
|
9
|
+
'clientId',
|
|
10
|
+
'requestIp',
|
|
11
|
+
'tossRequestId',
|
|
12
|
+
'from',
|
|
13
|
+
];
|
|
14
|
+
const OPTIONAL_BOOLEAN_KEYS = [
|
|
15
|
+
'isAdmin',
|
|
16
|
+
'isInternal',
|
|
17
|
+
];
|
|
18
|
+
const OPTIONAL_NUMBER_KEYS = [
|
|
19
|
+
'retryCount',
|
|
20
|
+
'timeout',
|
|
21
|
+
];
|
|
22
|
+
const OPTIONAL_OBJECT_KEYS = [
|
|
23
|
+
'device',
|
|
24
|
+
'externalTokenValue',
|
|
25
|
+
'requestLog',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* @author jun
|
|
29
|
+
* @description gRPC param metadata 검증을 위한 record 타입 가드입니다.
|
|
30
|
+
* @process 1. object 여부 확인 -> 2. null/array 제외 -> 3. record로 좁힘
|
|
31
|
+
*/
|
|
32
|
+
function isRecord(value) {
|
|
33
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
function isValidOptionalStringArray(value) {
|
|
36
|
+
return value === undefined || (Array.isArray(value) && value.every(item => typeof item === 'string'));
|
|
37
|
+
}
|
|
38
|
+
function hasInvalidOptionalFields(param) {
|
|
39
|
+
const hasInvalidString = OPTIONAL_STRING_KEYS.some(key => param[key] !== undefined && typeof param[key] !== 'string');
|
|
40
|
+
if (hasInvalidString) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const hasInvalidBoolean = OPTIONAL_BOOLEAN_KEYS.some(key => param[key] !== undefined && typeof param[key] !== 'boolean');
|
|
44
|
+
if (hasInvalidBoolean) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const hasInvalidNumber = OPTIONAL_NUMBER_KEYS.some(key => (param[key] !== undefined
|
|
48
|
+
&& (typeof param[key] !== 'number' || !Number.isFinite(param[key]))));
|
|
49
|
+
if (hasInvalidNumber) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const hasInvalidObject = OPTIONAL_OBJECT_KEYS.some(key => param[key] !== undefined && !isRecord(param[key]));
|
|
53
|
+
if (hasInvalidObject) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return !isValidOptionalStringArray(param.froms);
|
|
57
|
+
}
|
|
58
|
+
function toGrpcParamMetadata(value) {
|
|
59
|
+
if (!isRecord(value)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.traceId !== 'string' || value.traceId.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (hasInvalidOptionalFields(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function getMetadataStringValue(metadata) {
|
|
71
|
+
const values = metadata.get(exports.GRPC_PARAM_METADATA_KEY);
|
|
72
|
+
const value = values[0];
|
|
73
|
+
if (!value) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return value.toString('utf8');
|
|
80
|
+
}
|
|
81
|
+
function createInvalidGrpcParamMetadataError() {
|
|
82
|
+
return new bad_request_1.BadRequest('INVALID_GRPC_PARAM_METADATA', 'gRPC param metadata가 올바르지 않습니다.');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @author jun
|
|
86
|
+
* @description gRPC metadata에 AMQP param 호환 컨텍스트를 저장합니다.
|
|
87
|
+
* @process 1. AbstractParam 검증 -> 2. JSON 직렬화 -> 3. metadata param key에 저장
|
|
88
|
+
*/
|
|
89
|
+
function setGrpcParamMetadata(metadata, param) {
|
|
90
|
+
const grpcParam = toGrpcParamMetadata(param);
|
|
91
|
+
if (!grpcParam) {
|
|
92
|
+
throw createInvalidGrpcParamMetadataError();
|
|
93
|
+
}
|
|
94
|
+
metadata.set(exports.GRPC_PARAM_METADATA_KEY, JSON.stringify(grpcParam));
|
|
95
|
+
}
|
|
96
|
+
exports.setGrpcParamMetadata = setGrpcParamMetadata;
|
|
97
|
+
/**
|
|
98
|
+
* @author jun
|
|
99
|
+
* @description gRPC metadata에서 AMQP param 호환 컨텍스트를 안전하게 읽습니다.
|
|
100
|
+
* @process 1. metadata param key 조회 -> 2. 문자열 검증 -> 3. JSON 파싱 실패 시 null 반환
|
|
101
|
+
*/
|
|
102
|
+
function parseGrpcParamMetadata(metadata) {
|
|
103
|
+
const rawParam = getMetadataStringValue(metadata);
|
|
104
|
+
if (!rawParam) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return toGrpcParamMetadata(JSON.parse(rawParam));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.parseGrpcParamMetadata = parseGrpcParamMetadata;
|
|
115
|
+
/**
|
|
116
|
+
* @author jun
|
|
117
|
+
* @description gRPC metadata에서 필수 param 컨텍스트를 읽고 없거나 잘못되면 예외를 던집니다.
|
|
118
|
+
* @process 1. metadata param 파싱 -> 2. 유효성 확인 -> 3. 유효하지 않으면 BadRequest 발생
|
|
119
|
+
*/
|
|
120
|
+
function assertGrpcParamMetadata(metadata) {
|
|
121
|
+
const grpcParam = parseGrpcParamMetadata(metadata);
|
|
122
|
+
if (!grpcParam) {
|
|
123
|
+
throw createInvalidGrpcParamMetadataError();
|
|
124
|
+
}
|
|
125
|
+
return grpcParam;
|
|
126
|
+
}
|
|
127
|
+
exports.assertGrpcParamMetadata = assertGrpcParamMetadata;
|
package/functions/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './generate-trace-id.function';
|
|
|
3
3
|
export * from './amqp-publish.function';
|
|
4
4
|
export * from './rewrite-file-value.function';
|
|
5
5
|
export * from './amqp-call-manager.function';
|
|
6
|
+
export * from './grpc-param-metadata.function';
|
|
6
7
|
export * from './extract-redis-url.function';
|
|
7
8
|
export * from './abort';
|
|
8
9
|
export * from './uncaught-exception-handler';
|
package/functions/index.js
CHANGED
|
@@ -19,6 +19,7 @@ __exportStar(require("./generate-trace-id.function"), exports);
|
|
|
19
19
|
__exportStar(require("./amqp-publish.function"), exports);
|
|
20
20
|
__exportStar(require("./rewrite-file-value.function"), exports);
|
|
21
21
|
__exportStar(require("./amqp-call-manager.function"), exports);
|
|
22
|
+
__exportStar(require("./grpc-param-metadata.function"), exports);
|
|
22
23
|
__exportStar(require("./extract-redis-url.function"), exports);
|
|
23
24
|
__exportStar(require("./abort"), exports);
|
|
24
25
|
__exportStar(require("./uncaught-exception-handler"), exports);
|
|
@@ -37,6 +37,7 @@ exports.LoggingServiceInterceptor = void 0;
|
|
|
37
37
|
/* eslint-disable no-param-reassign */
|
|
38
38
|
const common_1 = require("@nestjs/common");
|
|
39
39
|
const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
|
|
40
|
+
const grpc_param_metadata_function_1 = require("@yolo-croket-dev/core/functions/grpc-param-metadata.function");
|
|
40
41
|
const operators_1 = require("rxjs/operators");
|
|
41
42
|
const dd_trace_1 = __importDefault(require("dd-trace"));
|
|
42
43
|
const { SERVER_NAME } = process.env;
|
|
@@ -75,6 +76,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
75
76
|
return data;
|
|
76
77
|
}
|
|
77
78
|
intercept(context, next) {
|
|
79
|
+
var _a;
|
|
78
80
|
const now = Date.now();
|
|
79
81
|
const args = context.getArgs();
|
|
80
82
|
const rpcContext = context.switchToRpc();
|
|
@@ -87,13 +89,11 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
87
89
|
let pattern; /** @xxx: Grpc: path , Mq: 메시지 패턴명 */
|
|
88
90
|
let payload;
|
|
89
91
|
if (isGrpc) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
param = JSON.parse(metadataParam);
|
|
92
|
+
param = (_a = (0, grpc_param_metadata_function_1.parseGrpcParamMetadata)(metadata)) !== null && _a !== void 0 ? _a : {
|
|
93
|
+
traceId: '-',
|
|
94
|
+
from: '-',
|
|
95
|
+
froms: ['-'],
|
|
96
|
+
};
|
|
97
97
|
originalMessage = args[0];
|
|
98
98
|
const gRPCArg = args[2];
|
|
99
99
|
pattern = gRPCArg.path;
|
|
@@ -116,11 +116,11 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
116
116
|
const tmpFroms = JSON.parse(JSON.stringify(pastFroms));
|
|
117
117
|
tmpFroms.push(tmpFrom);
|
|
118
118
|
if (isGrpc) {
|
|
119
|
-
|
|
119
|
+
(0, grpc_param_metadata_function_1.setGrpcParamMetadata)(metadata, {
|
|
120
120
|
...pastParam,
|
|
121
121
|
from: tmpFrom,
|
|
122
122
|
froms: tmpFroms,
|
|
123
|
-
})
|
|
123
|
+
});
|
|
124
124
|
}
|
|
125
125
|
else {
|
|
126
126
|
data.param.from = tmpFrom;
|
|
@@ -219,7 +219,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
219
219
|
}));
|
|
220
220
|
}
|
|
221
221
|
};
|
|
222
|
-
|
|
223
|
-
exports.LoggingServiceInterceptor = LoggingServiceInterceptor = __decorate([
|
|
222
|
+
LoggingServiceInterceptor = __decorate([
|
|
224
223
|
(0, common_1.Injectable)()
|
|
225
224
|
], LoggingServiceInterceptor);
|
|
225
|
+
exports.LoggingServiceInterceptor = LoggingServiceInterceptor;
|
|
@@ -370,9 +370,9 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
370
370
|
})));
|
|
371
371
|
}
|
|
372
372
|
};
|
|
373
|
-
|
|
374
|
-
exports.RPCInterceptor = RPCInterceptor = __decorate([
|
|
373
|
+
RPCInterceptor = __decorate([
|
|
375
374
|
(0, common_1.Injectable)(),
|
|
376
375
|
__metadata("design:paramtypes", [core_1.Reflector,
|
|
377
376
|
graceful_shutdown_service_1.GracefulShutdownService])
|
|
378
377
|
], RPCInterceptor);
|
|
378
|
+
exports.RPCInterceptor = RPCInterceptor;
|
package/interfaces/exception.js
CHANGED
|
@@ -18,10 +18,9 @@ var ErrActionTypeEnum;
|
|
|
18
18
|
ErrActionTypeEnum["LEGACY_SHOW_TOAST"] = "LEGACY_SHOW_TOAST";
|
|
19
19
|
ErrActionTypeEnum["FORCE_UPDATE"] = "FORCE_UPDATE";
|
|
20
20
|
ErrActionTypeEnum["UPDATE_ACCESS_TOKEN"] = "UPDATE_ACCESS_TOKEN";
|
|
21
|
-
})(ErrActionTypeEnum
|
|
21
|
+
})(ErrActionTypeEnum = exports.ErrActionTypeEnum || (exports.ErrActionTypeEnum = {}));
|
|
22
22
|
class ErrAction {
|
|
23
23
|
}
|
|
24
|
-
exports.ErrAction = ErrAction;
|
|
25
24
|
__decorate([
|
|
26
25
|
(0, swagger_1.ApiProperty)({
|
|
27
26
|
description: '액션 타입',
|
|
@@ -58,9 +57,9 @@ __decorate([
|
|
|
58
57
|
}),
|
|
59
58
|
__metadata("design:type", String)
|
|
60
59
|
], ErrAction.prototype, "webViewUrl", void 0);
|
|
60
|
+
exports.ErrAction = ErrAction;
|
|
61
61
|
class ErrInfo {
|
|
62
62
|
}
|
|
63
|
-
exports.ErrInfo = ErrInfo;
|
|
64
63
|
__decorate([
|
|
65
64
|
(0, swagger_1.ApiProperty)({
|
|
66
65
|
description: '에러 종류 식별자',
|
|
@@ -89,3 +88,4 @@ __decorate([
|
|
|
89
88
|
}),
|
|
90
89
|
__metadata("design:type", Object)
|
|
91
90
|
], ErrInfo.prototype, "errOption", void 0);
|
|
91
|
+
exports.ErrInfo = ErrInfo;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { HttpException } from '@nestjs/common';
|
|
2
2
|
import { LogLevelEnum } from './log-level.enum';
|
|
3
3
|
import type { ErrAction } from './exception';
|
|
4
|
-
export declare enum MsaExceptionMessageAckBehaviorEnum {
|
|
5
|
-
ACK = "ACK",
|
|
6
|
-
NACK = "NACK"
|
|
7
|
-
}
|
|
8
4
|
export interface MsaExceptionOption {
|
|
9
5
|
/**
|
|
10
6
|
* 로그 레벨
|
|
@@ -12,12 +8,6 @@ export interface MsaExceptionOption {
|
|
|
12
8
|
* - 기본값은 ERROR 입니다.
|
|
13
9
|
* */
|
|
14
10
|
logErrorLevel?: LogLevelEnum;
|
|
15
|
-
/**
|
|
16
|
-
* 메시지 consumer 실패 시 ACK/NACK override 옵션
|
|
17
|
-
* - 현재는 amqp-rabbit-mq subscribe consumer에서만 해석합니다.
|
|
18
|
-
* - option이 없으면 consumer handler의 기본 errorBehavior를 따릅니다.
|
|
19
|
-
*/
|
|
20
|
-
messageAckBehavior?: MsaExceptionMessageAckBehaviorEnum;
|
|
21
11
|
}
|
|
22
12
|
/**
|
|
23
13
|
* @author jun
|
|
@@ -37,7 +27,7 @@ export interface MsaException {
|
|
|
37
27
|
errOption?: MsaExceptionOption;
|
|
38
28
|
}
|
|
39
29
|
/** HttpException과 MsaException을 결합한 타입 */
|
|
40
|
-
export type MsaHttpException = MsaException & HttpException;
|
|
30
|
+
export declare type MsaHttpException = MsaException & HttpException;
|
|
41
31
|
/**
|
|
42
32
|
* @description MsaException 타입 가드
|
|
43
33
|
* @param error 검사할 에러 객체
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isMsaException =
|
|
3
|
+
exports.isMsaException = void 0;
|
|
4
4
|
const common_1 = require("@nestjs/common");
|
|
5
|
-
var MsaExceptionMessageAckBehaviorEnum;
|
|
6
|
-
(function (MsaExceptionMessageAckBehaviorEnum) {
|
|
7
|
-
MsaExceptionMessageAckBehaviorEnum["ACK"] = "ACK";
|
|
8
|
-
MsaExceptionMessageAckBehaviorEnum["NACK"] = "NACK";
|
|
9
|
-
})(MsaExceptionMessageAckBehaviorEnum || (exports.MsaExceptionMessageAckBehaviorEnum = MsaExceptionMessageAckBehaviorEnum = {}));
|
|
10
5
|
/**
|
|
11
6
|
* @description MsaException 타입 가드
|
|
12
7
|
* @param error 검사할 에러 객체
|
|
@@ -48,8 +48,7 @@ let GracefulShutdownModule = class GracefulShutdownModule {
|
|
|
48
48
|
msaLogger.info('GracefulShutdownService.onModuleDestroy: all interceptors finished, shutting down', { traceId: 'ON_MODULE_DESTROY' });
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
-
|
|
52
|
-
exports.GracefulShutdownModule = GracefulShutdownModule = __decorate([
|
|
51
|
+
GracefulShutdownModule = __decorate([
|
|
53
52
|
(0, common_1.Global)(),
|
|
54
53
|
(0, common_1.Module)({
|
|
55
54
|
providers: [graceful_shutdown_service_1.GracefulShutdownService],
|
|
@@ -57,3 +56,4 @@ exports.GracefulShutdownModule = GracefulShutdownModule = __decorate([
|
|
|
57
56
|
}),
|
|
58
57
|
__metadata("design:paramtypes", [graceful_shutdown_service_1.GracefulShutdownService])
|
|
59
58
|
], GracefulShutdownModule);
|
|
59
|
+
exports.GracefulShutdownModule = GracefulShutdownModule;
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import { ServerRMQ } from '@nestjs/microservices';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
export type GracefulRmqOptions = RmqOptions['options'] & {
|
|
1
|
+
import { RmqOptions, ServerRMQ } from '@nestjs/microservices';
|
|
2
|
+
import { Channel } from 'amqp-connection-manager';
|
|
3
|
+
import { ConsumeMessage } from 'amqplib';
|
|
4
|
+
export declare type GracefulRmqOptions = RmqOptions['options'] & {
|
|
6
5
|
waitingEndingHandlersTimeoutMs?: number;
|
|
7
6
|
waitingEndingHandlersIntervalMs?: number;
|
|
8
7
|
};
|
|
9
|
-
/**
|
|
10
|
-
* @author jun
|
|
11
|
-
* @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
|
|
12
|
-
* @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
|
|
13
|
-
*/
|
|
14
8
|
export declare class GracefulServerRMQ extends ServerRMQ {
|
|
15
9
|
protected readonly options: GracefulRmqOptions;
|
|
16
10
|
protected runningMessages: number;
|
|
@@ -1,67 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.GracefulServerRMQ = void 0;
|
|
7
4
|
const microservices_1 = require("@nestjs/microservices");
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
5
|
const node_util_1 = require("node:util");
|
|
10
6
|
const sleep = (0, node_util_1.promisify)(setTimeout);
|
|
11
7
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS = 5000;
|
|
12
8
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS = 500;
|
|
13
|
-
const RMQ_UNKNOWN_CONNECTION_IDENTITY = 'unknown';
|
|
14
|
-
function createRmqConnectionName(queue) {
|
|
15
|
-
const environment = process.env.ENV || process.env.NODE_ENV || RMQ_UNKNOWN_CONNECTION_IDENTITY;
|
|
16
|
-
const owner = process.env.RMQ_OWNER || process.env.USER || process.env.USERNAME || RMQ_UNKNOWN_CONNECTION_IDENTITY;
|
|
17
|
-
return [
|
|
18
|
-
queue || RMQ_UNKNOWN_CONNECTION_IDENTITY,
|
|
19
|
-
environment,
|
|
20
|
-
owner,
|
|
21
|
-
os_1.default.hostname(),
|
|
22
|
-
`pid-${process.pid}`,
|
|
23
|
-
].join('/');
|
|
24
|
-
}
|
|
25
|
-
function addRmqConnectionName(options) {
|
|
26
|
-
var _a;
|
|
27
|
-
const connectionOptions = (_a = options.socketOptions) === null || _a === void 0 ? void 0 : _a.connectionOptions;
|
|
28
|
-
const configuredClientProperties = connectionOptions === null || connectionOptions === void 0 ? void 0 : connectionOptions.clientProperties;
|
|
29
|
-
const clientProperties = configuredClientProperties
|
|
30
|
-
&& typeof configuredClientProperties === 'object'
|
|
31
|
-
&& !Array.isArray(configuredClientProperties)
|
|
32
|
-
? configuredClientProperties
|
|
33
|
-
: {};
|
|
34
|
-
const configuredConnectionName = clientProperties.connection_name;
|
|
35
|
-
const connectionName = typeof configuredConnectionName === 'string' && configuredConnectionName.trim()
|
|
36
|
-
? configuredConnectionName
|
|
37
|
-
: createRmqConnectionName(options.queue);
|
|
38
|
-
return {
|
|
39
|
-
...options,
|
|
40
|
-
socketOptions: {
|
|
41
|
-
...options.socketOptions,
|
|
42
|
-
connectionOptions: {
|
|
43
|
-
...connectionOptions,
|
|
44
|
-
clientProperties: {
|
|
45
|
-
...clientProperties,
|
|
46
|
-
connection_name: connectionName,
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* @author jun
|
|
54
|
-
* @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
|
|
55
|
-
* @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
|
|
56
|
-
*/
|
|
57
9
|
class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
58
10
|
constructor(options) {
|
|
59
|
-
|
|
60
|
-
|
|
11
|
+
super(options);
|
|
12
|
+
this.options = options;
|
|
61
13
|
this.runningMessages = 0;
|
|
62
14
|
this.closing = false;
|
|
63
15
|
this.consumerTag = null;
|
|
64
|
-
this.options = optionsWithConnectionName;
|
|
65
16
|
this.waitingEndingHandlersTimeoutMs = this.getOptionsProp(this.options, 'waitingEndingHandlersTimeoutMs')
|
|
66
17
|
|| RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS;
|
|
67
18
|
this.waitingEndingHandlersIntervalMs = this.getOptionsProp(this.options, 'waitingEndingHandlersIntervalMs')
|
|
@@ -133,7 +133,7 @@ let GracefulShutdownService = class GracefulShutdownService {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
|
-
|
|
137
|
-
exports.GracefulShutdownService = GracefulShutdownService = __decorate([
|
|
136
|
+
GracefulShutdownService = __decorate([
|
|
138
137
|
(0, common_1.Injectable)()
|
|
139
138
|
], GracefulShutdownService);
|
|
139
|
+
exports.GracefulShutdownService = GracefulShutdownService;
|
package/modules/redis.module.js
CHANGED
|
@@ -15,10 +15,10 @@ const redisProvider = {
|
|
|
15
15
|
};
|
|
16
16
|
let RedisModule = class RedisModule {
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
exports.RedisModule = RedisModule = __decorate([
|
|
18
|
+
RedisModule = __decorate([
|
|
20
19
|
(0, common_1.Module)({
|
|
21
20
|
providers: [redisProvider],
|
|
22
21
|
exports: [redisProvider],
|
|
23
22
|
})
|
|
24
23
|
], RedisModule);
|
|
24
|
+
exports.RedisModule = RedisModule;
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-croket-dev/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0-jun.1",
|
|
4
4
|
"author": "Yolo Co., Ltd.",
|
|
5
5
|
"description": "Yolo commonization module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"lint": "eslint src/**/*.ts",
|
|
10
|
-
"test": "yarn build && node --test tests/*.test.cjs",
|
|
11
10
|
"fix": "eslint --fix src/**/*.ts",
|
|
12
11
|
"clean": "tsc --build --clean",
|
|
13
12
|
"prepublishOnly": "npm run build",
|
|
@@ -16,7 +15,9 @@
|
|
|
16
15
|
"license": "UNLICENSED",
|
|
17
16
|
"peerDependencies": {
|
|
18
17
|
"@nestjs/common": "^11.0.0",
|
|
18
|
+
"@nestjs/microservices": "^11.0.0",
|
|
19
19
|
"@nestjs/swagger": "^11.0.0",
|
|
20
|
+
"@grpc/grpc-js": "^1.14.3",
|
|
20
21
|
"class-transformer": "^0.5.1",
|
|
21
22
|
"class-validator": "^0.14.0",
|
|
22
23
|
"dd-trace": "^5.0.0"
|
|
@@ -32,7 +33,9 @@
|
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@nestjs/common": "^11.1.8",
|
|
36
|
+
"@nestjs/microservices": "^11.1.8",
|
|
35
37
|
"@nestjs/swagger": "^11.2.1",
|
|
38
|
+
"@grpc/grpc-js": "^1.14.3",
|
|
36
39
|
"@types/node": "^24.9.1",
|
|
37
40
|
"@types/randomstring": "^1.1.8",
|
|
38
41
|
"@types/uuid": "^8.3.4"
|
|
@@ -31,8 +31,8 @@ let ValidationPipe = class ValidationPipe {
|
|
|
31
31
|
return value;
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
|
|
35
|
-
exports.ValidationPipe = ValidationPipe = __decorate([
|
|
34
|
+
ValidationPipe = __decorate([
|
|
36
35
|
(0, common_1.Injectable)(),
|
|
37
36
|
__metadata("design:paramtypes", [Object])
|
|
38
37
|
], ValidationPipe);
|
|
38
|
+
exports.ValidationPipe = ValidationPipe;
|
package/util.js
CHANGED
|
@@ -14,8 +14,10 @@ exports.generateRandom = generateRandom;
|
|
|
14
14
|
// ex) localhost:50051 => 50051
|
|
15
15
|
function getPortFromHost(host) {
|
|
16
16
|
const specificHost = host.includes('://') ? host.split('://')[1] : host;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const port = specificHost.split(':')[1];
|
|
18
|
+
if (port) {
|
|
19
|
+
const parsedPort = parseInt(port, 10);
|
|
20
|
+
return Number.isNaN(parsedPort) ? undefined : parsedPort;
|
|
19
21
|
}
|
|
20
22
|
return undefined;
|
|
21
23
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.9.13-beta.1
|
|
4
|
-
|
|
5
|
-
- AMQP 요청의 `param.timeout`을 최대 29분 범위에서 적용해 timeout 시 클라이언트 Observable 구독을 해제합니다.
|
|
6
|
-
|
|
7
|
-
## 1.9.12-beta.1
|
|
8
|
-
|
|
9
|
-
- `GracefulServerRMQ`가 RabbitMQ 연결에 queue, environment, owner, hostname, process ID 기반 `connection_name`을 자동으로 추가합니다.
|
|
10
|
-
- 호출자가 지정한 `connectionOptions`, `clientProperties`, 유효한 문자열 `connection_name`은 그대로 보존합니다.
|
|
11
|
-
- 문자열이 아닌 `connection_name`은 런타임 오류 없이 자동 생성값으로 대체합니다.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const { Observable } = require('rxjs');
|
|
4
|
-
|
|
5
|
-
const { AmqpHandler, HttpExceptionCustom } = require('../functions/amqp-handler.function.js');
|
|
6
|
-
|
|
7
|
-
test('param.timeout이 지나면 AMQP Observable 구독을 해제한다', async () => {
|
|
8
|
-
let isUnsubscribed = false;
|
|
9
|
-
const client = {
|
|
10
|
-
send() {
|
|
11
|
-
return new Observable(() => () => {
|
|
12
|
-
isUnsubscribed = true;
|
|
13
|
-
});
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
await assert.rejects(
|
|
18
|
-
AmqpHandler(client, 'test.timeout', {
|
|
19
|
-
payload: {},
|
|
20
|
-
param: {
|
|
21
|
-
traceId: 'trace-id',
|
|
22
|
-
timeout: 5,
|
|
23
|
-
},
|
|
24
|
-
}),
|
|
25
|
-
(error) => {
|
|
26
|
-
assert.equal(error instanceof HttpExceptionCustom, true);
|
|
27
|
-
assert.equal(error.errInfo.type, 'TIMEOUT_ERROR');
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
assert.equal(isUnsubscribed, true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('응답이 먼저 완료되면 기존 AMQP 응답을 반환한다', async () => {
|
|
36
|
-
const client = {
|
|
37
|
-
send() {
|
|
38
|
-
return new Observable((subscriber) => {
|
|
39
|
-
subscriber.next({ isError: false, data: { ok: true } });
|
|
40
|
-
subscriber.complete();
|
|
41
|
-
});
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const result = await AmqpHandler(client, 'test.success', { payload: {}, param: { traceId: 'trace-id', timeout: 50 } });
|
|
46
|
-
assert.deepEqual(result, { data: { ok: true } });
|
|
47
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const os = require('node:os');
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
GracefulServerRMQ,
|
|
7
|
-
} = require('../modules/graceful-shutdown/graceful-shutdown.rmq.js');
|
|
8
|
-
|
|
9
|
-
const CONNECTION_NAME_ENV_KEYS = [
|
|
10
|
-
'ENV',
|
|
11
|
-
'NODE_ENV',
|
|
12
|
-
'RMQ_OWNER',
|
|
13
|
-
'USER',
|
|
14
|
-
'USERNAME',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
function getClientProperties(options) {
|
|
18
|
-
const server = new GracefulServerRMQ(options);
|
|
19
|
-
return server.options.socketOptions.connectionOptions.clientProperties;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function runWithEnvironment(environment, callback) {
|
|
23
|
-
const originalEnvironment = Object.fromEntries(
|
|
24
|
-
CONNECTION_NAME_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
25
|
-
);
|
|
26
|
-
try {
|
|
27
|
-
for (const key of CONNECTION_NAME_ENV_KEYS) {
|
|
28
|
-
delete process.env[key];
|
|
29
|
-
}
|
|
30
|
-
Object.assign(process.env, environment);
|
|
31
|
-
callback();
|
|
32
|
-
} finally {
|
|
33
|
-
for (const key of CONNECTION_NAME_ENV_KEYS) {
|
|
34
|
-
const originalValue = originalEnvironment[key];
|
|
35
|
-
if (originalValue === undefined) {
|
|
36
|
-
delete process.env[key];
|
|
37
|
-
} else {
|
|
38
|
-
process.env[key] = originalValue;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
test('자동 connection_name에 큐와 실행 주체 정보를 포함한다', () => {
|
|
45
|
-
runWithEnvironment({ ENV: 'release', RMQ_OWNER: 'jun' }, () => {
|
|
46
|
-
const actualClientProperties = getClientProperties({ queue: 'STORE_ITEM_SERVER' });
|
|
47
|
-
const expectedConnectionName = `STORE_ITEM_SERVER/release/jun/${os.hostname()}/pid-${process.pid}`;
|
|
48
|
-
assert.equal(actualClientProperties.connection_name, expectedConnectionName);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('기존 connectionOptions와 clientProperties를 보존한다', () => {
|
|
53
|
-
runWithEnvironment({ NODE_ENV: 'production', USER: 'deploy' }, () => {
|
|
54
|
-
const inputOptions = {
|
|
55
|
-
queue: 'USER_SERVICE',
|
|
56
|
-
socketOptions: {
|
|
57
|
-
noDelay: true,
|
|
58
|
-
connectionOptions: {
|
|
59
|
-
locale: 'ko_KR',
|
|
60
|
-
clientProperties: {
|
|
61
|
-
product: 'croket',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
const server = new GracefulServerRMQ(inputOptions);
|
|
67
|
-
assert.equal(server.options.socketOptions.noDelay, true);
|
|
68
|
-
assert.equal(server.options.socketOptions.connectionOptions.locale, 'ko_KR');
|
|
69
|
-
assert.equal(server.options.socketOptions.connectionOptions.clientProperties.product, 'croket');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('명시적으로 설정한 문자열 connection_name을 보존한다', () => {
|
|
74
|
-
const actualClientProperties = getClientProperties({
|
|
75
|
-
queue: 'USER_SERVICE',
|
|
76
|
-
socketOptions: {
|
|
77
|
-
connectionOptions: {
|
|
78
|
-
clientProperties: {
|
|
79
|
-
connection_name: 'custom-connection-name',
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
assert.equal(actualClientProperties.connection_name, 'custom-connection-name');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('문자열이 아닌 connection_name은 자동 생성값으로 대체한다', () => {
|
|
88
|
-
runWithEnvironment({ ENV: 'release', RMQ_OWNER: 'jun' }, () => {
|
|
89
|
-
const actualClientProperties = getClientProperties({
|
|
90
|
-
queue: 'STORE_ITEM_SERVER',
|
|
91
|
-
socketOptions: {
|
|
92
|
-
connectionOptions: {
|
|
93
|
-
clientProperties: {
|
|
94
|
-
connection_name: 123,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
const expectedConnectionName = `STORE_ITEM_SERVER/release/jun/${os.hostname()}/pid-${process.pid}`;
|
|
100
|
-
assert.equal(actualClientProperties.connection_name, expectedConnectionName);
|
|
101
|
-
});
|
|
102
|
-
});
|