@cryptexlabs/codex-nodejs-common 0.16.4 → 0.16.7
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/WARP.md +28 -0
- package/coverage/clover.xml +102 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/client/client.ts.html +121 -0
- package/coverage/lcov-report/client/index.html +116 -0
- package/coverage/lcov-report/context/context.builder.ts.html +685 -0
- package/coverage/lcov-report/context/index.html +116 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov.info +160 -0
- package/lib/package.json +5 -5
- package/lib/src/auth/authf.guard.d.ts +1 -0
- package/lib/src/auth/authf.guard.js +22 -11
- package/lib/src/auth/authf.guard.js.map +1 -1
- package/lib/src/client/client.d.ts +1 -0
- package/lib/src/client/client.js.map +1 -1
- package/lib/src/config/default-config.d.ts +1 -0
- package/lib/src/config/default-config.js +5 -0
- package/lib/src/config/default-config.js.map +1 -1
- package/lib/src/config/index.d.ts +1 -0
- package/lib/src/config/index.js +1 -0
- package/lib/src/config/index.js.map +1 -1
- package/lib/src/context/context.builder.d.ts +1 -0
- package/lib/src/context/context.builder.js +12 -0
- package/lib/src/context/context.builder.js.map +1 -1
- package/lib/src/filter/app-http-exception-filter.d.ts +4 -1
- package/lib/src/filter/app-http-exception-filter.js +51 -29
- package/lib/src/filter/app-http-exception-filter.js.map +1 -1
- package/package.json +5 -5
- package/src/auth/authf.guard.ts +26 -11
- package/src/client/client.spec.ts +17 -0
- package/src/client/client.ts +2 -0
- package/src/config/default-config.ts +6 -0
- package/src/config/index.ts +1 -0
- package/src/context/context.builder.spec.ts +231 -0
- package/src/context/context.builder.ts +13 -0
- package/src/filter/app-http-exception-filter.ts +67 -36
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiMetaHeadersInterface,
|
|
3
|
+
ClientInterface,
|
|
4
|
+
Locale,
|
|
5
|
+
LocaleI18nInterface,
|
|
6
|
+
MessageContextInterface,
|
|
7
|
+
MessageMetaInterface,
|
|
8
|
+
} from "@cryptexlabs/codex-data-model";
|
|
9
|
+
import { LoggerService } from "@nestjs/common";
|
|
10
|
+
import { Context } from "./context";
|
|
11
|
+
import { ContextBuilder } from "./context.builder";
|
|
12
|
+
import { DefaultConfig } from "../config";
|
|
13
|
+
import { instance, mock, when } from "ts-mockito";
|
|
14
|
+
|
|
15
|
+
interface I18nApiStub {
|
|
16
|
+
getCatalog: () => Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("ContextBuilder", () => {
|
|
20
|
+
let loggerMock: LoggerService;
|
|
21
|
+
let configMock: DefaultConfig;
|
|
22
|
+
let client: ClientInterface;
|
|
23
|
+
let messageContext: MessageContextInterface;
|
|
24
|
+
let i18nData: i18nAPI;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
const logger = mock<LoggerService>();
|
|
28
|
+
loggerMock = instance(logger);
|
|
29
|
+
|
|
30
|
+
const config = mock(DefaultConfig);
|
|
31
|
+
when(config.appName).thenReturn("app");
|
|
32
|
+
when(config.appVersion).thenReturn("1.0.0");
|
|
33
|
+
when(config.environmentName).thenReturn("env");
|
|
34
|
+
when(config.clientId).thenReturn("client-id");
|
|
35
|
+
configMock = instance(config);
|
|
36
|
+
|
|
37
|
+
client = {
|
|
38
|
+
id: "id",
|
|
39
|
+
version: "0.0.1",
|
|
40
|
+
name: "client",
|
|
41
|
+
variant: "variant",
|
|
42
|
+
};
|
|
43
|
+
messageContext = { category: "default", id: "none" };
|
|
44
|
+
const i18nStub: I18nApiStub = {
|
|
45
|
+
getCatalog: () => ({ "en-US": {} }),
|
|
46
|
+
};
|
|
47
|
+
i18nData = (i18nStub as unknown) as i18nAPI;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const buildBuilder = () =>
|
|
51
|
+
new ContextBuilder(
|
|
52
|
+
loggerMock,
|
|
53
|
+
configMock,
|
|
54
|
+
client,
|
|
55
|
+
messageContext,
|
|
56
|
+
i18nData
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
it("returns now override when provided", () => {
|
|
60
|
+
const builder = buildBuilder();
|
|
61
|
+
|
|
62
|
+
const before = Date.now();
|
|
63
|
+
const initialNow = builder.now().getTime();
|
|
64
|
+
expect(initialNow).toBeGreaterThanOrEqual(before);
|
|
65
|
+
|
|
66
|
+
const override = new Date("2024-02-02T12:00:00Z");
|
|
67
|
+
builder.setNow(override);
|
|
68
|
+
|
|
69
|
+
expect(builder.now()).toBe(override);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("clones state with build including agent", () => {
|
|
73
|
+
const builder = buildBuilder();
|
|
74
|
+
builder.client.agent = "ua/1.0";
|
|
75
|
+
|
|
76
|
+
const clone = builder.build();
|
|
77
|
+
|
|
78
|
+
expect(clone).not.toBe(builder);
|
|
79
|
+
expect(clone.client.agent).toBe("ua/1.0");
|
|
80
|
+
expect(clone.now()).toBeInstanceOf(Date);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sets client fields including agent from headers", () => {
|
|
84
|
+
const builder = buildBuilder();
|
|
85
|
+
const headers: ApiMetaHeadersInterface & {
|
|
86
|
+
"x-forwarded-uri": string;
|
|
87
|
+
"user-agent": string;
|
|
88
|
+
"x-timezone": string;
|
|
89
|
+
} = {
|
|
90
|
+
"x-correlation-id": "corr-1",
|
|
91
|
+
"accept-language": "en-US",
|
|
92
|
+
"x-started": "2025-01-01T10:00:00Z",
|
|
93
|
+
"x-created": "2025-01-01T10:00:00Z",
|
|
94
|
+
"x-context-category": "cat",
|
|
95
|
+
"x-context-id": "ctx-id",
|
|
96
|
+
"x-client-id": "client-1",
|
|
97
|
+
"x-client-name": "client-name",
|
|
98
|
+
"x-client-version": "1.2.3",
|
|
99
|
+
"x-client-variant": "variant-x",
|
|
100
|
+
"x-forwarded-uri": "/some/path",
|
|
101
|
+
"x-now": "2025-01-01T11:00:00Z",
|
|
102
|
+
"x-timezone": "UTC",
|
|
103
|
+
"user-agent": "UnitTestAgent/1.0",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const resultBuilder = builder.setMetaFromHeaders(headers);
|
|
107
|
+
const context = resultBuilder.getResult();
|
|
108
|
+
|
|
109
|
+
expect(context.client.agent).toBe("UnitTestAgent/1.0");
|
|
110
|
+
expect(context.client.id).toBe("client-1");
|
|
111
|
+
expect(context.client.name).toBe("client-name");
|
|
112
|
+
expect(context.client.version).toBe("1.2.3");
|
|
113
|
+
expect(context.client.variant).toBe("variant-x");
|
|
114
|
+
expect(context.path).toBe("/some/path");
|
|
115
|
+
expect(context.now().toISOString()).toBe("2025-01-01T11:00:00.000Z");
|
|
116
|
+
expect(context.timezone).toBe("UTC");
|
|
117
|
+
expect(resultBuilder).toBe(builder);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("allows overriding i18n data", () => {
|
|
121
|
+
const builder = buildBuilder();
|
|
122
|
+
const customI18n: I18nApiStub = {
|
|
123
|
+
getCatalog: () => ({ "en-US": { greeting: "hi" } }),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const returned = builder.setI18nData((customI18n as unknown) as i18nAPI);
|
|
127
|
+
|
|
128
|
+
expect(returned).toBe(builder);
|
|
129
|
+
expect(() => builder.getResult()).not.toThrow();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("leaves client fields unchanged when optional headers missing", () => {
|
|
133
|
+
const builder = buildBuilder();
|
|
134
|
+
const minimalHeaders: ApiMetaHeadersInterface = {
|
|
135
|
+
"x-correlation-id": "corr-2",
|
|
136
|
+
"accept-language": "en-US",
|
|
137
|
+
"x-started": "2024-03-01T10:00:00Z",
|
|
138
|
+
"x-created": "2024-03-01T10:00:00Z",
|
|
139
|
+
"x-context-category": "default",
|
|
140
|
+
"x-context-id": "none",
|
|
141
|
+
"x-client-id": "id",
|
|
142
|
+
"x-client-name": "client",
|
|
143
|
+
"x-client-version": "0.0.1",
|
|
144
|
+
"x-client-variant": "variant",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
builder.setMetaFromHeaders(minimalHeaders);
|
|
148
|
+
const context = builder.getResult();
|
|
149
|
+
|
|
150
|
+
expect(context.client.agent).toBeUndefined();
|
|
151
|
+
expect(context.path).toBeUndefined();
|
|
152
|
+
expect(context.timezone).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("sets values from headers for new messages", () => {
|
|
156
|
+
const builder = buildBuilder();
|
|
157
|
+
const headers: ApiMetaHeadersInterface & { "x-forwarded-uri": string } = {
|
|
158
|
+
"x-correlation-id": "corr-3",
|
|
159
|
+
"accept-language": "en-US",
|
|
160
|
+
"x-started": "2024-04-01T10:00:00Z",
|
|
161
|
+
"x-created": "2024-04-01T10:00:00Z",
|
|
162
|
+
"x-context-category": "custom",
|
|
163
|
+
"x-context-id": "ctx-3",
|
|
164
|
+
"x-client-id": "id",
|
|
165
|
+
"x-client-name": "client",
|
|
166
|
+
"x-client-version": "0.0.1",
|
|
167
|
+
"x-client-variant": "variant",
|
|
168
|
+
"x-forwarded-uri": "/another/path",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
builder.setMetaFromHeadersForNewMessage(headers);
|
|
172
|
+
const context = builder.getResult();
|
|
173
|
+
|
|
174
|
+
expect(context.correlationId).toBe("corr-3");
|
|
175
|
+
expect(context.messageContext.category).toBe("custom");
|
|
176
|
+
expect(context.messageContext.id).toBe("ctx-3");
|
|
177
|
+
expect(context.path).toBe("/another/path");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("copies values from existing context", () => {
|
|
181
|
+
const baseBuilder = buildBuilder();
|
|
182
|
+
baseBuilder
|
|
183
|
+
.setCorrelationId("corr-4")
|
|
184
|
+
.setStarted("2024-05-01T10:00:00Z")
|
|
185
|
+
.setLocale(new Locale("en", "US"));
|
|
186
|
+
baseBuilder.client.agent = "CopiedAgent";
|
|
187
|
+
const sourceContext = baseBuilder.getResult();
|
|
188
|
+
|
|
189
|
+
const targetBuilder = buildBuilder().setMetaFromContext(sourceContext);
|
|
190
|
+
const result = targetBuilder.getResult();
|
|
191
|
+
|
|
192
|
+
expect(result.correlationId).toBe("corr-4");
|
|
193
|
+
expect(result.messageContext.id).toBe(sourceContext.messageContext.id);
|
|
194
|
+
expect(result.client.agent).toBe("CopiedAgent");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("sets values from message meta", () => {
|
|
198
|
+
const builder = buildBuilder();
|
|
199
|
+
const meta: MessageMetaInterface = {
|
|
200
|
+
correlationId: "corr-5",
|
|
201
|
+
time: {
|
|
202
|
+
started: "2024-06-01T10:00:00Z",
|
|
203
|
+
created: "2024-06-01T10:00:00Z",
|
|
204
|
+
},
|
|
205
|
+
context: { category: "meta", id: "meta-id" },
|
|
206
|
+
client,
|
|
207
|
+
type: "type",
|
|
208
|
+
schemaVersion: "0.1.0",
|
|
209
|
+
locale: new Locale("fr", "CA") as LocaleI18nInterface,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
builder.setMeta(meta);
|
|
213
|
+
const context = builder.getResult();
|
|
214
|
+
|
|
215
|
+
expect(context.correlationId).toBe("corr-5");
|
|
216
|
+
expect(context.locale.language).toBe("fr");
|
|
217
|
+
expect(context.started.toISOString()).toBe("2024-06-01T10:00:00.000Z");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("applies defaults when values are missing", () => {
|
|
221
|
+
const builder = buildBuilder();
|
|
222
|
+
builder.setNow(new Date("2024-07-01T11:00:00Z"));
|
|
223
|
+
|
|
224
|
+
const context = builder.getResult();
|
|
225
|
+
|
|
226
|
+
expect(context.correlationId).toHaveLength(36);
|
|
227
|
+
expect(context.locale.i18n).toBe("en-US");
|
|
228
|
+
expect(context.started).toBeInstanceOf(Date);
|
|
229
|
+
expect(context.now().toISOString()).toBe("2024-07-01T11:00:00.000Z");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -51,6 +51,7 @@ export class ContextBuilder {
|
|
|
51
51
|
version: this.client.version,
|
|
52
52
|
id: this.client.id,
|
|
53
53
|
variant: this.client.variant,
|
|
54
|
+
agent: this.client.agent,
|
|
54
55
|
},
|
|
55
56
|
{
|
|
56
57
|
category: this.messageContext.category,
|
|
@@ -81,6 +82,9 @@ export class ContextBuilder {
|
|
|
81
82
|
if (headers["x-client-variant"]) {
|
|
82
83
|
this.client.variant = headers["x-client-variant"];
|
|
83
84
|
}
|
|
85
|
+
if (headers["user-agent"]) {
|
|
86
|
+
this.client.agent = headers["user-agent"];
|
|
87
|
+
}
|
|
84
88
|
if (headers["x-forwarded-uri"]) {
|
|
85
89
|
this._path = headers["x-forwarded-uri"];
|
|
86
90
|
}
|
|
@@ -112,6 +116,15 @@ export class ContextBuilder {
|
|
|
112
116
|
return this;
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
public setMetaFromContext(context: Context) {
|
|
120
|
+
this.setCorrelationId(context.correlationId);
|
|
121
|
+
this.setLocale(context.locale);
|
|
122
|
+
this.setStarted(context.started);
|
|
123
|
+
this.messageContext.category = context.messageContext.category;
|
|
124
|
+
this.messageContext.id = context.messageContext.id;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
public setMeta(meta: MessageMetaInterface) {
|
|
116
129
|
this.setCorrelationId(meta.correlationId);
|
|
117
130
|
this.setLocale(new Locale(meta.locale.language, meta.locale.country));
|
|
@@ -23,6 +23,9 @@ import { LocaleUtil } from "../util";
|
|
|
23
23
|
@Catch()
|
|
24
24
|
@Injectable()
|
|
25
25
|
export class AppHttpExceptionFilter extends BaseExceptionFilter {
|
|
26
|
+
private _lastErrorLogTime: number = 0;
|
|
27
|
+
private _suppressedErrorCount: number = 0;
|
|
28
|
+
|
|
26
29
|
constructor(
|
|
27
30
|
private readonly fallbackLocale: Locale,
|
|
28
31
|
@Inject("LOGGER") private readonly fallbackLogger: LoggerService,
|
|
@@ -32,12 +35,29 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
|
|
|
32
35
|
super(applicationRef);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
private
|
|
38
|
+
private _isDebugLevel(): boolean {
|
|
36
39
|
return this.config.logLevels.some(
|
|
37
40
|
(level) => level.toLowerCase() === "debug"
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
private _shouldLogError(): boolean {
|
|
45
|
+
if (this._isDebugLevel()) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const timeSinceLastLog = now - this._lastErrorLogTime;
|
|
51
|
+
|
|
52
|
+
if (timeSinceLastLog >= this.config.errorLogIntervalMs) {
|
|
53
|
+
this._lastErrorLogTime = now;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._suppressedErrorCount++;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
catch(exception: any, host: ArgumentsHost) {
|
|
42
62
|
const ctx = host.switchToHttp();
|
|
43
63
|
const request = ctx.getRequest<Request>();
|
|
@@ -97,43 +117,54 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
|
|
|
97
117
|
logger.error(`No stack available for error of type ${typeof exception}`);
|
|
98
118
|
}
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
const shouldLogError = this._shouldLogError();
|
|
121
|
+
|
|
122
|
+
if (shouldLogError) {
|
|
123
|
+
if (developerText) {
|
|
124
|
+
if (this.config.loggerType === "pino") {
|
|
125
|
+
logger.error(developerText, { body: request.body, stack });
|
|
126
|
+
} else {
|
|
127
|
+
logger.error(developerText, {
|
|
128
|
+
request: {
|
|
129
|
+
headers: request.headers,
|
|
130
|
+
body: request.body,
|
|
131
|
+
url: request.url,
|
|
132
|
+
method: request.method,
|
|
133
|
+
stack,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
103
137
|
} else {
|
|
104
|
-
logger.error(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
logger.error(
|
|
139
|
+
[
|
|
140
|
+
"Error not explained",
|
|
141
|
+
"This probably means someone wrote code that throws an error without an error message",
|
|
142
|
+
"This should never happen!",
|
|
143
|
+
].join(". "),
|
|
144
|
+
{
|
|
110
145
|
stack,
|
|
111
|
-
}
|
|
112
|
-
|
|
146
|
+
}
|
|
147
|
+
);
|
|
113
148
|
}
|
|
114
|
-
} else {
|
|
115
|
-
logger.error(
|
|
116
|
-
[
|
|
117
|
-
"Error not explained",
|
|
118
|
-
"This probably means someone wrote code that throws an error without an error message",
|
|
119
|
-
"This should never happen!",
|
|
120
|
-
].join(". "),
|
|
121
|
-
{
|
|
122
|
-
stack,
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
// If this is a normal error it means something random occurred and we want the stacktrace
|
|
151
|
+
if (
|
|
152
|
+
!(
|
|
153
|
+
exception instanceof HttpException ||
|
|
154
|
+
exception instanceof FriendlyHttpException ||
|
|
155
|
+
exception instanceof ContextualHttpException
|
|
156
|
+
) &&
|
|
157
|
+
exception.stack
|
|
158
|
+
) {
|
|
159
|
+
logger.error(exception.stack);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this._suppressedErrorCount > 0) {
|
|
163
|
+
logger.error(
|
|
164
|
+
`Suppressed ${this._suppressedErrorCount} error(s) in the last minute due to rate limiting`
|
|
165
|
+
);
|
|
166
|
+
this._suppressedErrorCount = 0;
|
|
167
|
+
}
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
let status: number;
|
|
@@ -174,7 +205,7 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
|
|
|
174
205
|
request.url || (request.headers["x-forwarded-uri"] as string);
|
|
175
206
|
|
|
176
207
|
const canShowDeveloperText =
|
|
177
|
-
this.
|
|
208
|
+
this._isDebugLevel() || (status >= 400 && status < 500);
|
|
178
209
|
|
|
179
210
|
const errorHttpResponse = new ErrorHttpResponse(
|
|
180
211
|
status,
|
|
@@ -189,7 +220,7 @@ export class AppHttpExceptionFilter extends BaseExceptionFilter {
|
|
|
189
220
|
: "Error unavailable in this context",
|
|
190
221
|
userMessage
|
|
191
222
|
),
|
|
192
|
-
this.
|
|
223
|
+
this._isDebugLevel() ? exception.stack || null : null,
|
|
193
224
|
this.config,
|
|
194
225
|
correlationId,
|
|
195
226
|
started,
|