@treatwell/moleculer-essentials 1.0.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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/context-factory-BWO3xPWE.d.cts +520 -0
- package/dist/context-factory-BWO3xPWE.d.mts +520 -0
- package/dist/index-82e1CXJX.cjs +11 -0
- package/dist/index-BV1ZqQrU.mjs +351 -0
- package/dist/index-DNJWwcZu.mjs +8 -0
- package/dist/index-rZl77S1z.cjs +375 -0
- package/dist/index.cjs +1570 -0
- package/dist/index.d.cts +373 -0
- package/dist/index.d.mts +373 -0
- package/dist/index.mjs +1535 -0
- package/dist/mixins/database.mixin.cjs +1673 -0
- package/dist/mixins/database.mixin.d.cts +958 -0
- package/dist/mixins/database.mixin.d.mts +958 -0
- package/dist/mixins/database.mixin.mjs +1645 -0
- package/dist/mixins/encryptor.mixin.cjs +84 -0
- package/dist/mixins/encryptor.mixin.d.cts +31 -0
- package/dist/mixins/encryptor.mixin.d.mts +31 -0
- package/dist/mixins/encryptor.mixin.mjs +81 -0
- package/dist/mixins/global-store.mixin.cjs +56 -0
- package/dist/mixins/global-store.mixin.d.cts +39 -0
- package/dist/mixins/global-store.mixin.d.mts +39 -0
- package/dist/mixins/global-store.mixin.mjs +54 -0
- package/dist/mixins/jwt.mixin.cjs +118 -0
- package/dist/mixins/jwt.mixin.d.cts +43 -0
- package/dist/mixins/jwt.mixin.d.mts +43 -0
- package/dist/mixins/jwt.mixin.mjs +115 -0
- package/dist/mixins/queue.mixin.cjs +420 -0
- package/dist/mixins/queue.mixin.d.cts +150 -0
- package/dist/mixins/queue.mixin.d.mts +150 -0
- package/dist/mixins/queue.mixin.mjs +414 -0
- package/dist/mixins/redis.mixin.cjs +50 -0
- package/dist/mixins/redis.mixin.d.cts +27 -0
- package/dist/mixins/redis.mixin.d.mts +27 -0
- package/dist/mixins/redis.mixin.mjs +48 -0
- package/dist/mixins/redlock.mixin.cjs +76 -0
- package/dist/mixins/redlock.mixin.d.cts +30 -0
- package/dist/mixins/redlock.mixin.d.mts +30 -0
- package/dist/mixins/redlock.mixin.mjs +74 -0
- package/package.json +181 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
import { S as SCHEMA_REF_NAME, z as zodToOpenAPISchema, o as omitFields, G as GetOpenApiParamsSchema, V as ValidationErrorSchema, F as FileTooBigSchema, a as FileNotExistSchema, U as UnauthorizedErrorSchema, b as ServerErrorSchema, C as COERCE_ARRAY_ATTRIBUTE } from './index-BV1ZqQrU.mjs';
|
|
2
|
+
export { D as DATE_TYPE, E as EMPTY_OBJECT_SCHEMA, O as OBJECTID_TYPE, c as addFieldsToSchema, f as composeSchemas, j as createOpenAPIResponses, e as optionalExceptFields, d as optionalFields, p as pickFields, t as toPartialSchema, i as zodCoerceArray, g as zodDate, h as zodObjectId } from './index-BV1ZqQrU.mjs';
|
|
3
|
+
import { ZodType, ZodObject, ZodOptional, z } from 'zod/v4';
|
|
4
|
+
import { Ajv2019 } from 'ajv/dist/2019.js';
|
|
5
|
+
import { Validators, Errors, Context, ServiceBroker, Service, TracerExporters, MetricReporters } from 'moleculer';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import addKeywords from 'ajv-keywords';
|
|
8
|
+
import { omit, isEqual, merge, isArray, isString, isNaN, isPlainObject, constant, defaultsDeep, isObject, isFunction } from 'lodash';
|
|
9
|
+
import { parseISO } from 'date-fns';
|
|
10
|
+
import { ObjectId } from 'bson';
|
|
11
|
+
import { w as wrapMixin } from './index-DNJWwcZu.mjs';
|
|
12
|
+
export { a as wrapService } from './index-DNJWwcZu.mjs';
|
|
13
|
+
import http, { STATUS_CODES } from 'http';
|
|
14
|
+
import { pino } from 'pino';
|
|
15
|
+
import { hostname } from 'node:os';
|
|
16
|
+
import 'zod';
|
|
17
|
+
|
|
18
|
+
function getSchemaFromMoleculer(schema) {
|
|
19
|
+
if (!schema) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
if (typeof schema === "object" && !Array.isArray(schema)) {
|
|
23
|
+
return schema;
|
|
24
|
+
}
|
|
25
|
+
if (typeof schema === "function") {
|
|
26
|
+
const res = schema();
|
|
27
|
+
if (typeof res === "object" && !Array.isArray(res)) {
|
|
28
|
+
return res;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class RefExtractor {
|
|
35
|
+
refs = /* @__PURE__ */ new Map();
|
|
36
|
+
meta = {};
|
|
37
|
+
onError(message, other = {}) {
|
|
38
|
+
console.log("Error while extracting refs", message, {
|
|
39
|
+
...this.meta,
|
|
40
|
+
...other
|
|
41
|
+
});
|
|
42
|
+
throw new Error(message);
|
|
43
|
+
}
|
|
44
|
+
setMeta(meta) {
|
|
45
|
+
this.meta = meta;
|
|
46
|
+
}
|
|
47
|
+
extract(schemaToExtract, initialRef = void 0, skipRoot = false) {
|
|
48
|
+
const walk = (curr, currentRef, root = false) => {
|
|
49
|
+
if (curr.$ref) {
|
|
50
|
+
if (curr.$ref === "#") {
|
|
51
|
+
if (!currentRef) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Recursive refs MUST have at least one parent with a SCHEMA_REF_NAME defined"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { $ref: currentRef };
|
|
57
|
+
}
|
|
58
|
+
return curr;
|
|
59
|
+
}
|
|
60
|
+
const schemaRefName = curr[SCHEMA_REF_NAME];
|
|
61
|
+
if (schemaRefName) {
|
|
62
|
+
const ref = this.refReplacer(schemaRefName);
|
|
63
|
+
if (!this.refs.has(schemaRefName)) {
|
|
64
|
+
this.refs.set(schemaRefName, null);
|
|
65
|
+
}
|
|
66
|
+
const newSchema2 = walk(omit(curr, [SCHEMA_REF_NAME, "nullable"]), ref);
|
|
67
|
+
const registeredSchema = this.refs.get(schemaRefName);
|
|
68
|
+
if (registeredSchema === null) {
|
|
69
|
+
this.refs.set(schemaRefName, newSchema2);
|
|
70
|
+
this.onNewRef(schemaRefName, newSchema2);
|
|
71
|
+
} else if (!isEqual(newSchema2, registeredSchema)) {
|
|
72
|
+
this.onError(
|
|
73
|
+
`Schema ${schemaRefName} is already defined with different content`,
|
|
74
|
+
{ registeredSchema, newSchema: newSchema2 }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (skipRoot && root) {
|
|
78
|
+
return this.refs.get(schemaRefName);
|
|
79
|
+
}
|
|
80
|
+
if (curr.nullable) {
|
|
81
|
+
return {
|
|
82
|
+
oneOf: [{ $ref: ref }, { type: "null" }]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { $ref: ref };
|
|
86
|
+
}
|
|
87
|
+
if (["integer", "number", "string", "boolean"].includes(curr.type)) {
|
|
88
|
+
return curr;
|
|
89
|
+
}
|
|
90
|
+
const newSchema = { ...curr };
|
|
91
|
+
if (newSchema.type === "array" && newSchema.items) {
|
|
92
|
+
newSchema.items = walk(newSchema.items, currentRef);
|
|
93
|
+
} else if (newSchema.type === "object") {
|
|
94
|
+
if (newSchema.properties) {
|
|
95
|
+
newSchema.properties = { ...newSchema.properties };
|
|
96
|
+
Object.entries(newSchema.properties).forEach(([key, val]) => {
|
|
97
|
+
newSchema.properties[key] = walk(val, currentRef);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (newSchema.additionalProperties && newSchema.additionalProperties !== true) {
|
|
101
|
+
newSchema.additionalProperties = walk(
|
|
102
|
+
newSchema.additionalProperties,
|
|
103
|
+
currentRef
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(newSchema.oneOf)) {
|
|
108
|
+
newSchema.oneOf = newSchema.oneOf.map((s) => walk(s, currentRef));
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(newSchema.allOf)) {
|
|
111
|
+
newSchema.allOf = newSchema.allOf.map((s) => walk(s, currentRef));
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(newSchema.anyOf)) {
|
|
114
|
+
newSchema.anyOf = newSchema.anyOf.map((s) => walk(s, currentRef));
|
|
115
|
+
}
|
|
116
|
+
return newSchema;
|
|
117
|
+
};
|
|
118
|
+
return walk(schemaToExtract, initialRef, true);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class OpenAPIExtractor extends RefExtractor {
|
|
123
|
+
doc;
|
|
124
|
+
constructor(doc) {
|
|
125
|
+
super();
|
|
126
|
+
this.doc = doc;
|
|
127
|
+
}
|
|
128
|
+
refReplacer(ref) {
|
|
129
|
+
return `#/components/schemas/${ref}`;
|
|
130
|
+
}
|
|
131
|
+
onNewRef(originalRef, schema) {
|
|
132
|
+
if (!this.doc.components) {
|
|
133
|
+
this.doc.components = {};
|
|
134
|
+
}
|
|
135
|
+
if (!this.doc.components.schemas) {
|
|
136
|
+
this.doc.components.schemas = {};
|
|
137
|
+
}
|
|
138
|
+
this.doc.components.schemas[originalRef] = schema;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateOpenAPISpec({
|
|
143
|
+
doc,
|
|
144
|
+
aliases,
|
|
145
|
+
services,
|
|
146
|
+
getSecuritySchemes
|
|
147
|
+
}) {
|
|
148
|
+
services.forEach((svc) => merge(doc, svc.settings?.openapi));
|
|
149
|
+
const extractor = new OpenAPIExtractor(doc);
|
|
150
|
+
if (doc.components?.schemas) {
|
|
151
|
+
const { schemas } = doc.components;
|
|
152
|
+
doc.components.schemas = {};
|
|
153
|
+
for (const [key, val] of Object.entries(schemas)) {
|
|
154
|
+
doc.components.schemas[key] = extractor.extract(
|
|
155
|
+
val,
|
|
156
|
+
`#/components/schemas/${key}`,
|
|
157
|
+
true
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const alias of aliases) {
|
|
162
|
+
const url = alias.fullPath.replace(/:(\w+)/g, "{$1}");
|
|
163
|
+
const p = doc.paths[url] || {};
|
|
164
|
+
doc.paths[url] = p;
|
|
165
|
+
const op = createOperationFromAlias(alias, extractor, url);
|
|
166
|
+
p[alias.methods.toLowerCase()] = op;
|
|
167
|
+
if (op.responses) {
|
|
168
|
+
op.responses["401"] = {
|
|
169
|
+
$ref: "#/components/responses/UnauthorizedError"
|
|
170
|
+
};
|
|
171
|
+
op.responses["422"] = { $ref: "#/components/responses/ValidationError" };
|
|
172
|
+
op.responses["5XX"] = { $ref: "#/components/responses/ServerError" };
|
|
173
|
+
}
|
|
174
|
+
const authModes = getSecuritySchemes?.(alias);
|
|
175
|
+
if (authModes) {
|
|
176
|
+
op.security = authModes.map((authMode) => ({ [authMode]: [] }));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (doc.components?.schemas) {
|
|
180
|
+
const sortedEntries = Object.entries(doc.components.schemas).sort(
|
|
181
|
+
(a, b) => a[0] > b[0] ? 1 : -1
|
|
182
|
+
);
|
|
183
|
+
doc.components.schemas = Object.fromEntries(sortedEntries);
|
|
184
|
+
}
|
|
185
|
+
return doc;
|
|
186
|
+
}
|
|
187
|
+
function createOperationFromAlias(alias, extractor, url) {
|
|
188
|
+
let parameters;
|
|
189
|
+
let requestBody;
|
|
190
|
+
extractor.setMeta({ url, alias });
|
|
191
|
+
const pathParams = url.match(/{(\w+)}/g) || [];
|
|
192
|
+
const params = getSchemaFromMoleculer(alias.action.params);
|
|
193
|
+
if (params instanceof ZodType) {
|
|
194
|
+
({ parameters, requestBody } = createOperationFromZodParams(
|
|
195
|
+
params,
|
|
196
|
+
pathParams,
|
|
197
|
+
alias,
|
|
198
|
+
extractor
|
|
199
|
+
));
|
|
200
|
+
} else {
|
|
201
|
+
({ parameters, requestBody } = createOperationFromAjvParams(
|
|
202
|
+
params,
|
|
203
|
+
pathParams,
|
|
204
|
+
alias,
|
|
205
|
+
extractor
|
|
206
|
+
));
|
|
207
|
+
}
|
|
208
|
+
const openapi = alias.action.openapi || {};
|
|
209
|
+
if (openapi.parameters) {
|
|
210
|
+
if (!parameters) {
|
|
211
|
+
parameters = [];
|
|
212
|
+
}
|
|
213
|
+
for (const param of openapi.parameters) {
|
|
214
|
+
const newP = param;
|
|
215
|
+
const idx = parameters.findIndex((p) => p.name === newP.name);
|
|
216
|
+
if (idx === -1) {
|
|
217
|
+
parameters.push(newP);
|
|
218
|
+
} else {
|
|
219
|
+
parameters[idx] = newP;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const responses = {};
|
|
224
|
+
if (openapi.responses) {
|
|
225
|
+
for (const [key, val] of Object.entries(openapi.responses)) {
|
|
226
|
+
responses[key] = val;
|
|
227
|
+
const content = val.content?.["application/json"];
|
|
228
|
+
if (content && "zodInstance" in content && typeof content.zodInstance === "function") {
|
|
229
|
+
content.schema = zodToOpenAPISchema(content.zodInstance(), extractor);
|
|
230
|
+
} else if (content?.schema) {
|
|
231
|
+
content.schema = extractor.extract(content.schema);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
requestBody,
|
|
237
|
+
// Let openapi override requestBody
|
|
238
|
+
operationId: alias.actionName,
|
|
239
|
+
...openapi,
|
|
240
|
+
responses,
|
|
241
|
+
parameters
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function createOperationFromAjvParams(params, pathParams, alias, extractor) {
|
|
245
|
+
const parameters = [];
|
|
246
|
+
let requestBody;
|
|
247
|
+
if (pathParams.length) {
|
|
248
|
+
parameters.push(
|
|
249
|
+
...pathParams.map((p) => {
|
|
250
|
+
const name = p.slice(1, -1);
|
|
251
|
+
return {
|
|
252
|
+
in: "path",
|
|
253
|
+
name,
|
|
254
|
+
required: true,
|
|
255
|
+
schema: extractor.extract(
|
|
256
|
+
params?.properties?.[name] || { type: "string" }
|
|
257
|
+
)
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (["get", "delete"].includes(alias.methods.toLowerCase())) {
|
|
263
|
+
const schemas = [
|
|
264
|
+
...params?.allOf || [],
|
|
265
|
+
...params?.oneOf || [],
|
|
266
|
+
...params?.anyOf || [],
|
|
267
|
+
...params && "properties" in params ? [params] : []
|
|
268
|
+
];
|
|
269
|
+
for (const schema of schemas) {
|
|
270
|
+
parameters.push(
|
|
271
|
+
...Object.entries(schema.properties).filter(([key]) => !parameters.find((p) => p.name === key)).map(([key, val]) => ({
|
|
272
|
+
in: "query",
|
|
273
|
+
name: key,
|
|
274
|
+
schema: extractor.extract(val || { type: "string" }),
|
|
275
|
+
required: schema.required?.includes(key)
|
|
276
|
+
}))
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} else if (params) {
|
|
280
|
+
requestBody = {
|
|
281
|
+
required: true,
|
|
282
|
+
content: {
|
|
283
|
+
"application/json": {
|
|
284
|
+
schema: extractor.extract(
|
|
285
|
+
omitFields(
|
|
286
|
+
params,
|
|
287
|
+
parameters.map((p) => p.name),
|
|
288
|
+
alias.action.bodySchemaRefName
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return { requestBody, parameters };
|
|
296
|
+
}
|
|
297
|
+
function createOperationFromZodParams(params, pathParams, alias, extractor) {
|
|
298
|
+
const parameters = [];
|
|
299
|
+
let requestBody;
|
|
300
|
+
if (!(params instanceof ZodObject)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Expected params to be an ZodObject in ${alias.actionName}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (pathParams.length) {
|
|
306
|
+
parameters.push(
|
|
307
|
+
...pathParams.map((p) => {
|
|
308
|
+
const name = p.slice(1, -1);
|
|
309
|
+
return {
|
|
310
|
+
in: "path",
|
|
311
|
+
name,
|
|
312
|
+
required: true,
|
|
313
|
+
schema: zodToOpenAPISchema(params.shape[name], extractor)
|
|
314
|
+
};
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (["get", "delete"].includes(alias.methods.toLowerCase())) {
|
|
319
|
+
parameters.push(
|
|
320
|
+
...Object.entries(params.shape).filter(([key]) => !parameters.find((p) => p.name === key)).map(([key, val]) => ({
|
|
321
|
+
in: "query",
|
|
322
|
+
name: key,
|
|
323
|
+
schema: zodToOpenAPISchema(val, extractor),
|
|
324
|
+
required: !(val instanceof ZodOptional)
|
|
325
|
+
}))
|
|
326
|
+
);
|
|
327
|
+
} else if (params) {
|
|
328
|
+
let zodBody = params;
|
|
329
|
+
if (parameters.length > 0) {
|
|
330
|
+
zodBody = params.omit(
|
|
331
|
+
Object.fromEntries(parameters.map((p) => [p.name, true]))
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (alias.action.bodySchemaRefName) {
|
|
335
|
+
zodBody = zodBody.meta({ id: alias.action.bodySchemaRefName });
|
|
336
|
+
}
|
|
337
|
+
requestBody = {
|
|
338
|
+
required: true,
|
|
339
|
+
content: {
|
|
340
|
+
"application/json": { schema: zodToOpenAPISchema(zodBody, extractor) }
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return { requestBody, parameters };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function OpenAPIMixin(options) {
|
|
348
|
+
const { swaggerUI, tags, defaultKinds, actionOptions, getSecuritySchemes } = options;
|
|
349
|
+
return wrapMixin({
|
|
350
|
+
settings: {
|
|
351
|
+
openapi: {
|
|
352
|
+
openapi: "3.0.3",
|
|
353
|
+
info: {
|
|
354
|
+
description: "Default Moleculer essentials spec",
|
|
355
|
+
version: "0.0.0",
|
|
356
|
+
title: "Sample API"
|
|
357
|
+
},
|
|
358
|
+
paths: {},
|
|
359
|
+
components: {
|
|
360
|
+
responses: {
|
|
361
|
+
// Standard moleculer responses
|
|
362
|
+
ServerError: {
|
|
363
|
+
description: "Server errors: 500, 501, 400, 404 and etc...",
|
|
364
|
+
content: { "application/json": { schema: ServerErrorSchema } }
|
|
365
|
+
},
|
|
366
|
+
UnauthorizedError: {
|
|
367
|
+
description: "Need auth",
|
|
368
|
+
content: {
|
|
369
|
+
"application/json": { schema: UnauthorizedErrorSchema }
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
FileNotExist: {
|
|
373
|
+
description: "File not exist",
|
|
374
|
+
content: { "application/json": { schema: FileNotExistSchema } }
|
|
375
|
+
},
|
|
376
|
+
FileTooBig: {
|
|
377
|
+
description: "File too big",
|
|
378
|
+
content: { "application/json": { schema: FileTooBigSchema } }
|
|
379
|
+
},
|
|
380
|
+
ValidationError: {
|
|
381
|
+
description: "Fields invalid",
|
|
382
|
+
content: {
|
|
383
|
+
"application/json": { schema: ValidationErrorSchema }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
schemas: {
|
|
388
|
+
OpenAPIModel: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
openapi: {
|
|
392
|
+
example: "3.0.3",
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "OpenAPI version"
|
|
395
|
+
},
|
|
396
|
+
info: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: { description: { type: "string" } }
|
|
399
|
+
},
|
|
400
|
+
tags: { type: "array", items: { type: "string" } }
|
|
401
|
+
},
|
|
402
|
+
required: ["openapi"]
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
methods: {
|
|
409
|
+
matchesKind(openAPINames, kind) {
|
|
410
|
+
if (openAPINames === null) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
if (!kind) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return (openAPINames || defaultKinds || []).includes(kind);
|
|
417
|
+
},
|
|
418
|
+
/**
|
|
419
|
+
* Generate the OpenAPI schema for the specified kind.
|
|
420
|
+
*/
|
|
421
|
+
async generateSchema(ctx, kind) {
|
|
422
|
+
const doc = JSON.parse(JSON.stringify(this.settings.openapi));
|
|
423
|
+
const [aliases, services] = await Promise.all([
|
|
424
|
+
ctx.call("api.listAliases", {
|
|
425
|
+
withActionSchema: true
|
|
426
|
+
}),
|
|
427
|
+
ctx.call("$node.services", { onlyAvailable: true, skipInternal: true })
|
|
428
|
+
]);
|
|
429
|
+
return generateOpenAPISpec({
|
|
430
|
+
doc,
|
|
431
|
+
getSecuritySchemes,
|
|
432
|
+
services: services.filter(
|
|
433
|
+
(svc) => this.matchesKind(svc.settings?.openAPINames, kind)
|
|
434
|
+
),
|
|
435
|
+
aliases: aliases.filter((alias) => this.matchesKind(alias.action.openAPINames, kind)).sort((a, b) => a.actionName > b.actionName ? 1 : -1)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
actions: {
|
|
440
|
+
generateDocs: {
|
|
441
|
+
rest: "GET /openapi.json",
|
|
442
|
+
openapi: {
|
|
443
|
+
summary: "OpenAPI schema url",
|
|
444
|
+
responses: {
|
|
445
|
+
"200": {
|
|
446
|
+
description: "",
|
|
447
|
+
content: {
|
|
448
|
+
"application/json": {
|
|
449
|
+
schema: { $ref: "#/components/schemas/OpenAPIModel" }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
tags
|
|
455
|
+
},
|
|
456
|
+
...actionOptions,
|
|
457
|
+
params: GetOpenApiParamsSchema,
|
|
458
|
+
handler(ctx) {
|
|
459
|
+
return this.generateSchema(ctx, ctx.params.kind);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
ui: {
|
|
463
|
+
rest: "GET /ui",
|
|
464
|
+
openapi: {
|
|
465
|
+
summary: "OpenAPI ui",
|
|
466
|
+
description: "You can provide any schema file in query param",
|
|
467
|
+
tags
|
|
468
|
+
},
|
|
469
|
+
...actionOptions,
|
|
470
|
+
handler(ctx) {
|
|
471
|
+
ctx.meta.$responseType = "text/html";
|
|
472
|
+
ctx.meta.$responseHeaders = {
|
|
473
|
+
"Content-Security-Policy": `default-src 'self' unpkg.com; img-src 'self' data:; script-src-elem 'self' 'unsafe-inline' unpkg.com`
|
|
474
|
+
};
|
|
475
|
+
const version = swaggerUI?.version || "latest";
|
|
476
|
+
return `
|
|
477
|
+
<!DOCTYPE html>
|
|
478
|
+
<html lang="en">
|
|
479
|
+
<head>
|
|
480
|
+
<meta charset="utf-8" />
|
|
481
|
+
<title>OpenAPI UI</title>
|
|
482
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@${version}/swagger-ui.css"/>
|
|
483
|
+
<link rel="stylesheet" href="https://unpkg.com/normalize.css@8.0.1/normalize.css"/>
|
|
484
|
+
</head>
|
|
485
|
+
<body>
|
|
486
|
+
<div id="swagger-ui">
|
|
487
|
+
<p>Loading...</p>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<script src="https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js" crossorigin><\/script>
|
|
491
|
+
<script src="https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-standalone-preset.js" crossorigin><\/script>
|
|
492
|
+
<script>
|
|
493
|
+
window.onload = function() {
|
|
494
|
+
window.ui = SwaggerUIBundle({
|
|
495
|
+
url: 'openapi.json',
|
|
496
|
+
dom_id: '#swagger-ui',
|
|
497
|
+
deepLinking: true,
|
|
498
|
+
presets: [
|
|
499
|
+
SwaggerUIBundle.presets.apis,
|
|
500
|
+
SwaggerUIStandalonePreset,
|
|
501
|
+
],
|
|
502
|
+
plugins: [
|
|
503
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
504
|
+
],
|
|
505
|
+
layout: "StandaloneLayout",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
<\/script>
|
|
509
|
+
</body>
|
|
510
|
+
</html>`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
class LeafTransformerBase {
|
|
518
|
+
transformMap = /* @__PURE__ */ new WeakMap();
|
|
519
|
+
findTransformsInternal(schema, currentField) {
|
|
520
|
+
if (this.isTransformableLeaf(schema)) {
|
|
521
|
+
currentField.push({ type: "this" });
|
|
522
|
+
return [currentField];
|
|
523
|
+
}
|
|
524
|
+
if (schema.type === "object" && schema.properties) {
|
|
525
|
+
return Object.entries(schema.properties).flatMap(([key, subSchema]) => {
|
|
526
|
+
const currentFieldCopy = [...currentField];
|
|
527
|
+
currentFieldCopy.push({ type: "access", key });
|
|
528
|
+
return this.findTransformsInternal(
|
|
529
|
+
subSchema,
|
|
530
|
+
currentFieldCopy
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (schema.type === "array" && schema.items) {
|
|
535
|
+
currentField.push({ type: "loop" });
|
|
536
|
+
return this.findTransformsInternal(
|
|
537
|
+
schema.items,
|
|
538
|
+
currentField
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (schema.oneOf || schema.anyOf || schema.allOf) {
|
|
542
|
+
const arr = schema.oneOf || schema.anyOf || schema.allOf;
|
|
543
|
+
const subTransforms = arr.flatMap(
|
|
544
|
+
(subSchema) => this.findTransformsInternal(subSchema, [])
|
|
545
|
+
);
|
|
546
|
+
currentField.push({ type: "select", subTransforms });
|
|
547
|
+
return [currentField];
|
|
548
|
+
}
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
findTransforms(schema) {
|
|
552
|
+
return this.findTransformsInternal(schema, []);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
class CoerceArrayTransformer extends LeafTransformerBase {
|
|
557
|
+
beforeTransformer = (val) => {
|
|
558
|
+
if (isArray(val) || val === null) {
|
|
559
|
+
return val;
|
|
560
|
+
}
|
|
561
|
+
return [val];
|
|
562
|
+
};
|
|
563
|
+
afterTransformer = (val) => val;
|
|
564
|
+
isTransformableLeaf = (schema) => schema.type === "array" && schema[COERCE_ARRAY_ATTRIBUTE];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
class DateTransformer extends LeafTransformerBase {
|
|
568
|
+
transformFormats = ["date-time"];
|
|
569
|
+
beforeTransformer = (val) => {
|
|
570
|
+
if (val instanceof Date) {
|
|
571
|
+
return val.toISOString();
|
|
572
|
+
}
|
|
573
|
+
return val;
|
|
574
|
+
};
|
|
575
|
+
afterTransformer = (val) => {
|
|
576
|
+
if (!isString(val)) {
|
|
577
|
+
return val;
|
|
578
|
+
}
|
|
579
|
+
const date = parseISO(val);
|
|
580
|
+
if (isNaN(date.getTime())) {
|
|
581
|
+
return val;
|
|
582
|
+
}
|
|
583
|
+
return date;
|
|
584
|
+
};
|
|
585
|
+
isTransformableLeaf(schema) {
|
|
586
|
+
return schema.type === "string" && this.transformFormats.includes(schema.format);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
class ObjectIdTransformer extends LeafTransformerBase {
|
|
591
|
+
beforeTransformer = (val) => {
|
|
592
|
+
if (val?._bsontype?.toLowerCase() === "objectid" && ObjectId.isValid(val)) {
|
|
593
|
+
return val.toString();
|
|
594
|
+
}
|
|
595
|
+
return val;
|
|
596
|
+
};
|
|
597
|
+
afterTransformer = (val) => {
|
|
598
|
+
if (isString(val) && /^[a-f\d]{24}$/i.test(val)) {
|
|
599
|
+
return new ObjectId(val);
|
|
600
|
+
}
|
|
601
|
+
return val;
|
|
602
|
+
};
|
|
603
|
+
isTransformableLeaf = (schema) => schema.type === "string" && schema.format === "object-id";
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const transformers = [
|
|
607
|
+
new DateTransformer(),
|
|
608
|
+
new ObjectIdTransformer(),
|
|
609
|
+
new CoerceArrayTransformer()
|
|
610
|
+
];
|
|
611
|
+
function applyTransform(parent, dataKey, transformer, transformField, index) {
|
|
612
|
+
const transformLevel = transformField[index];
|
|
613
|
+
switch (transformLevel.type) {
|
|
614
|
+
case "this": {
|
|
615
|
+
if (!isPlainObject(parent) && !isArray(parent)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (parent.hasOwnProperty(dataKey)) {
|
|
619
|
+
parent[dataKey] = transformer(parent[dataKey]);
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "access":
|
|
624
|
+
if (!isPlainObject(parent[dataKey])) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
applyTransform(
|
|
628
|
+
parent[dataKey],
|
|
629
|
+
transformLevel.key,
|
|
630
|
+
transformer,
|
|
631
|
+
transformField,
|
|
632
|
+
index + 1
|
|
633
|
+
);
|
|
634
|
+
break;
|
|
635
|
+
case "select":
|
|
636
|
+
for (const localTransformField of transformLevel.subTransforms) {
|
|
637
|
+
applyTransform(parent, dataKey, transformer, localTransformField, 0);
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
case "loop": {
|
|
641
|
+
if (!isArray(parent[dataKey])) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const cast = parent[dataKey];
|
|
645
|
+
for (let i = 0; i < cast.length; i += 1) {
|
|
646
|
+
applyTransform(
|
|
647
|
+
cast,
|
|
648
|
+
i,
|
|
649
|
+
transformer,
|
|
650
|
+
transformField,
|
|
651
|
+
index + 1
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
default:
|
|
657
|
+
throw new Error(
|
|
658
|
+
`Unknown transformLevel type '${transformLevel.type}'`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function applyTransforms(schema, data, transformMap, transformer) {
|
|
663
|
+
if (transformMap.has(schema)) {
|
|
664
|
+
const transformFields = transformMap.get(schema);
|
|
665
|
+
transformFields.forEach((transformField) => {
|
|
666
|
+
applyTransform({ data }, "data", transformer, transformField, 0);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function applyBeforeTransforms(schema, data) {
|
|
671
|
+
transformers.forEach((transformer) => {
|
|
672
|
+
applyTransforms(
|
|
673
|
+
schema,
|
|
674
|
+
data,
|
|
675
|
+
transformer.transformMap,
|
|
676
|
+
transformer.beforeTransformer
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function applyAfterTransforms(schema, data) {
|
|
681
|
+
transformers.forEach((transformer) => {
|
|
682
|
+
applyTransforms(
|
|
683
|
+
schema,
|
|
684
|
+
data,
|
|
685
|
+
transformer.transformMap,
|
|
686
|
+
transformer.afterTransformer
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function loadTransforms(schema) {
|
|
691
|
+
transformers.forEach((transformer) => {
|
|
692
|
+
if (!transformer.transformMap.has(schema)) {
|
|
693
|
+
const transforms = transformer.findTransforms(schema);
|
|
694
|
+
if (transforms.some((t) => t.length === 0)) {
|
|
695
|
+
const strSchema = JSON.stringify(schema, void 0, 2);
|
|
696
|
+
throw new Error(`Unsupported transforms found for Schema ${strSchema}`);
|
|
697
|
+
}
|
|
698
|
+
transformer.transformMap.set(schema, transforms);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
class AjvExtractor extends RefExtractor {
|
|
704
|
+
ajv;
|
|
705
|
+
constructor(ajv) {
|
|
706
|
+
super();
|
|
707
|
+
this.ajv = ajv;
|
|
708
|
+
}
|
|
709
|
+
refReplacer(ref) {
|
|
710
|
+
return `https://schemas.wavy.fr/${ref}`;
|
|
711
|
+
}
|
|
712
|
+
onNewRef(originalRef, schema) {
|
|
713
|
+
this.ajv.addSchema(schema, this.refReplacer(originalRef));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
class AjvValidator extends Validators.Base {
|
|
718
|
+
modes;
|
|
719
|
+
defaultMode;
|
|
720
|
+
// broker is a property of the base class
|
|
721
|
+
broker;
|
|
722
|
+
zodValidator;
|
|
723
|
+
/**
|
|
724
|
+
* Cache of compiled validation functions.
|
|
725
|
+
* This is because compiling a schema is quite expensive for Ajv.
|
|
726
|
+
*/
|
|
727
|
+
compiledFns = /* @__PURE__ */ new WeakMap();
|
|
728
|
+
constructor(opts, defaultMode, zodValidator) {
|
|
729
|
+
super();
|
|
730
|
+
this.defaultMode = defaultMode;
|
|
731
|
+
this.zodValidator = zodValidator;
|
|
732
|
+
this.modes = /* @__PURE__ */ new Map();
|
|
733
|
+
for (const [mode, ajvOpts] of Object.entries(opts)) {
|
|
734
|
+
const validator = new Ajv2019(ajvOpts);
|
|
735
|
+
this.modes.set(mode, {
|
|
736
|
+
validator,
|
|
737
|
+
extractor: new AjvExtractor(validator)
|
|
738
|
+
});
|
|
739
|
+
addFormats(validator);
|
|
740
|
+
addKeywords(validator);
|
|
741
|
+
validator.addKeyword({
|
|
742
|
+
keyword: COERCE_ARRAY_ATTRIBUTE,
|
|
743
|
+
valid: true
|
|
744
|
+
});
|
|
745
|
+
validator.addFormat("object-id", {
|
|
746
|
+
type: "string",
|
|
747
|
+
validate: constant(true)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
validate(params, schema) {
|
|
752
|
+
if (schema instanceof ZodType) {
|
|
753
|
+
if (!this.zodValidator) {
|
|
754
|
+
throw new Error("No validator to handle zod schemas");
|
|
755
|
+
}
|
|
756
|
+
return this.zodValidator.validate(params, schema);
|
|
757
|
+
}
|
|
758
|
+
return this.compile(schema)(params);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* This method compiles a schema into a validation function.
|
|
762
|
+
*
|
|
763
|
+
* Compiling a schema is quite expensive, so it will only be done once per schema.
|
|
764
|
+
*/
|
|
765
|
+
compile(schema, mode = this.defaultMode) {
|
|
766
|
+
if (this.compiledFns.has(schema)) {
|
|
767
|
+
return this.compiledFns.get(schema);
|
|
768
|
+
}
|
|
769
|
+
const validatorMode = this.modes.get(mode);
|
|
770
|
+
if (!validatorMode) {
|
|
771
|
+
throw new Error(`Validator mode ${mode} doesn't exist.`);
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
loadTransforms(schema);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
const strSchema = JSON.stringify(schema, void 0, 2);
|
|
777
|
+
this.broker.logger.error(
|
|
778
|
+
"Error loading transforms for schema",
|
|
779
|
+
strSchema
|
|
780
|
+
);
|
|
781
|
+
throw err;
|
|
782
|
+
}
|
|
783
|
+
const { validator, extractor } = validatorMode;
|
|
784
|
+
const refSchema = extractor.extract(schema);
|
|
785
|
+
const fn = (params, ctx) => {
|
|
786
|
+
if (!ctx?.action?.disableTransforms) {
|
|
787
|
+
applyBeforeTransforms(schema, params);
|
|
788
|
+
}
|
|
789
|
+
const validate = validator.compile(refSchema);
|
|
790
|
+
const res = validate(params);
|
|
791
|
+
if (res !== true) {
|
|
792
|
+
const name = ctx?.action?.name || ctx?.eventName || "unknown";
|
|
793
|
+
this.logError(name, schema, validate.errors);
|
|
794
|
+
throw new Errors.ValidationError(
|
|
795
|
+
`Parameters validation error on ${name}: ${validator.errorsText(
|
|
796
|
+
validate.errors
|
|
797
|
+
)}`,
|
|
798
|
+
"VALIDATION_ERROR",
|
|
799
|
+
validate.errors || []
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!ctx?.action?.disableTransforms) {
|
|
803
|
+
applyAfterTransforms(schema, params);
|
|
804
|
+
}
|
|
805
|
+
return true;
|
|
806
|
+
};
|
|
807
|
+
this.compiledFns.set(schema, fn);
|
|
808
|
+
return fn;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* For debugging purposes, log the validation errors.
|
|
812
|
+
* This is only enabled if the DETAILED_AJV_VALIDATION_ERRORS env var is set to 'yes'.
|
|
813
|
+
*/
|
|
814
|
+
logError(name, schema, errors) {
|
|
815
|
+
if (process.env.DETAILED_AJV_VALIDATION_ERRORS !== "yes") {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const errorLogMessage = `Validation of parameters given to ${name} against schema ${schema[SCHEMA_REF_NAME]} gave the following errors:
|
|
819
|
+
${(errors || []).map(
|
|
820
|
+
({ message, params: errorParameters, instancePath, schemaPath }) => `- ${message} ${JSON.stringify(
|
|
821
|
+
errorParameters
|
|
822
|
+
)} @${instancePath} in ${schemaPath}`
|
|
823
|
+
).join("\n")}`;
|
|
824
|
+
this.broker.logger.error(errorLogMessage);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* We wrap the localAction and localEvent methods to add validation to the actions and events handlers.
|
|
828
|
+
* Note that we lazy compile schemas in order to avoid a performance hit on startup.
|
|
829
|
+
*/
|
|
830
|
+
middleware() {
|
|
831
|
+
let zodMiddleware = this.zodValidator?.middleware();
|
|
832
|
+
if (!zodMiddleware) {
|
|
833
|
+
zodMiddleware = {
|
|
834
|
+
name: "Validator",
|
|
835
|
+
localAction() {
|
|
836
|
+
throw new Error("No validator to handle zod schemas");
|
|
837
|
+
},
|
|
838
|
+
localEvent() {
|
|
839
|
+
throw new Error("No validator to handle zod schemas");
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
name: "Validator",
|
|
845
|
+
localAction: (handler, action) => {
|
|
846
|
+
const schema = getSchemaFromMoleculer(action.params);
|
|
847
|
+
if (!schema) {
|
|
848
|
+
return handler;
|
|
849
|
+
}
|
|
850
|
+
if (schema instanceof ZodType) {
|
|
851
|
+
return zodMiddleware.localAction(
|
|
852
|
+
handler,
|
|
853
|
+
action
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const validate = this.compile(schema, action.validatorMode);
|
|
857
|
+
return async (ctx) => {
|
|
858
|
+
validate(ctx.params != null ? ctx.params : {}, ctx);
|
|
859
|
+
return handler(ctx);
|
|
860
|
+
};
|
|
861
|
+
},
|
|
862
|
+
localEvent: (handler, event) => {
|
|
863
|
+
const schema = getSchemaFromMoleculer(event.params);
|
|
864
|
+
if (!schema) {
|
|
865
|
+
return handler;
|
|
866
|
+
}
|
|
867
|
+
if (schema instanceof ZodType) {
|
|
868
|
+
return zodMiddleware.localEvent(
|
|
869
|
+
handler,
|
|
870
|
+
event
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const validate = this.compile(schema, event.validatorMode);
|
|
874
|
+
return async (ctx) => {
|
|
875
|
+
validate(ctx.params != null ? ctx.params : {}, ctx);
|
|
876
|
+
return handler(ctx);
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
class ZodValidator extends Validators.Base {
|
|
884
|
+
compile() {
|
|
885
|
+
throw new Error("compile should not be used, use validate instead");
|
|
886
|
+
}
|
|
887
|
+
validate(params, schema, ctx) {
|
|
888
|
+
const res = schema.safeParse(params);
|
|
889
|
+
if (res.success) {
|
|
890
|
+
return res.data;
|
|
891
|
+
}
|
|
892
|
+
const name = ctx?.action?.name || ctx?.eventName || "unknown";
|
|
893
|
+
throw new Errors.ValidationError(
|
|
894
|
+
`Parameters validation error on ${name}:
|
|
895
|
+
${z.prettifyError(res.error)}`,
|
|
896
|
+
"VALIDATION_ERROR",
|
|
897
|
+
res.error.issues
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Override BaseValidator middleware to handle our custom compile function.
|
|
902
|
+
*/
|
|
903
|
+
middleware() {
|
|
904
|
+
return {
|
|
905
|
+
name: "Validator",
|
|
906
|
+
localAction: (handler, action) => {
|
|
907
|
+
const schema = getSchemaFromMoleculer(action.params);
|
|
908
|
+
if (!schema) {
|
|
909
|
+
return handler;
|
|
910
|
+
}
|
|
911
|
+
return async (ctx) => {
|
|
912
|
+
ctx.params = this.validate(ctx.params, schema, ctx);
|
|
913
|
+
return handler(ctx);
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
localEvent: (handler, event) => {
|
|
917
|
+
const schema = getSchemaFromMoleculer(event.params);
|
|
918
|
+
if (!schema) {
|
|
919
|
+
return handler;
|
|
920
|
+
}
|
|
921
|
+
return async (ctx) => {
|
|
922
|
+
ctx.params = this.validate(ctx.params, schema, ctx);
|
|
923
|
+
return handler(ctx);
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
class ContextFactory extends Context {
|
|
931
|
+
constructor(broker, endpoint) {
|
|
932
|
+
super(broker, endpoint);
|
|
933
|
+
this.setLogger();
|
|
934
|
+
}
|
|
935
|
+
startSpan(name, opts) {
|
|
936
|
+
const span = super.startSpan(name, opts);
|
|
937
|
+
this.setLogger();
|
|
938
|
+
return span;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Get a logger for this specific span of the context.
|
|
942
|
+
*/
|
|
943
|
+
setLogger() {
|
|
944
|
+
const bindings = {
|
|
945
|
+
traceID: this.requestID,
|
|
946
|
+
spanID: this.span ? this.span.id : void 0
|
|
947
|
+
};
|
|
948
|
+
if (this.service) {
|
|
949
|
+
Object.assign(bindings, {
|
|
950
|
+
svc: this.service.name,
|
|
951
|
+
ver: this.service.version
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
this.logger = this.broker.getLogger(
|
|
955
|
+
this.service ? this.service.fullName : "context",
|
|
956
|
+
bindings
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const NOOP_CACHE = {
|
|
962
|
+
get: () => void 0,
|
|
963
|
+
set: () => void 0
|
|
964
|
+
};
|
|
965
|
+
class CustomServiceBroker extends ServiceBroker {
|
|
966
|
+
/**
|
|
967
|
+
* By replacing the cache property of the loggerFactory, we can disable the cache.
|
|
968
|
+
* This is because caching loggers can lead to OOM because of
|
|
969
|
+
* the large number of child loggers created (one per span).
|
|
970
|
+
*/
|
|
971
|
+
getLogger(module, props) {
|
|
972
|
+
this.loggerFactory.cache = NOOP_CACHE;
|
|
973
|
+
return super.getLogger(module, props);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function lockAllParams(schema) {
|
|
978
|
+
if (schema?.mixins) {
|
|
979
|
+
for (const mixin of schema.mixins) {
|
|
980
|
+
lockAllParams(mixin);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (schema?.actions) {
|
|
984
|
+
for (const action of Object.values(schema.actions)) {
|
|
985
|
+
if (action && typeof action === "object" && "params" in action && typeof action.params === "object") {
|
|
986
|
+
const rawParams = action.params;
|
|
987
|
+
action.params = () => rawParams;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (schema?.events) {
|
|
992
|
+
for (const event of Object.values(schema.events)) {
|
|
993
|
+
if (event && typeof event === "object" && "params" in event && typeof event.params === "object") {
|
|
994
|
+
const rawParams = event.params;
|
|
995
|
+
event.params = () => rawParams;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
class ServiceFactory extends Service {
|
|
1001
|
+
constructor(broker, schema) {
|
|
1002
|
+
lockAllParams(schema);
|
|
1003
|
+
super(broker, schema);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function createServiceBroker(opts = {}) {
|
|
1008
|
+
return new CustomServiceBroker({
|
|
1009
|
+
logger: process.env.NODE_ENV === "test" ? false : void 0,
|
|
1010
|
+
// TODO Add default logger
|
|
1011
|
+
validator: new AjvValidator(
|
|
1012
|
+
{
|
|
1013
|
+
default: {
|
|
1014
|
+
useDefaults: true,
|
|
1015
|
+
coerceTypes: true,
|
|
1016
|
+
allErrors: true,
|
|
1017
|
+
removeAdditional: false
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
"default",
|
|
1021
|
+
new ZodValidator()
|
|
1022
|
+
),
|
|
1023
|
+
ServiceFactory,
|
|
1024
|
+
ContextFactory,
|
|
1025
|
+
internalServices: false,
|
|
1026
|
+
...opts
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function isServiceSelected(metadata, selector) {
|
|
1031
|
+
const s = {
|
|
1032
|
+
...selector,
|
|
1033
|
+
matchExpressions: selector.matchExpressions?.slice() || []
|
|
1034
|
+
};
|
|
1035
|
+
if (s.matchLabels) {
|
|
1036
|
+
s.matchExpressions.push(
|
|
1037
|
+
...Object.entries(s.matchLabels).map(([key, value]) => ({
|
|
1038
|
+
key,
|
|
1039
|
+
operator: "in",
|
|
1040
|
+
values: [value]
|
|
1041
|
+
}))
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
return s.matchExpressions.every((rule) => isServiceMatchRule(metadata, rule));
|
|
1045
|
+
}
|
|
1046
|
+
function isServiceMatchRule(metadata, rule) {
|
|
1047
|
+
const meta = metadata || {};
|
|
1048
|
+
switch (rule.operator) {
|
|
1049
|
+
case "in":
|
|
1050
|
+
return rule.values.includes(meta[rule.key]?.toString());
|
|
1051
|
+
case "notin":
|
|
1052
|
+
return !rule.values.includes(meta[rule.key]?.toString());
|
|
1053
|
+
case "exists":
|
|
1054
|
+
return rule.key in meta;
|
|
1055
|
+
case "doesnotexists":
|
|
1056
|
+
return !(rule.key in meta);
|
|
1057
|
+
default:
|
|
1058
|
+
throw new Error("Unknown operator");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function getMetadataFromService(svc) {
|
|
1062
|
+
const res = {
|
|
1063
|
+
name: svc.name || "unknown"
|
|
1064
|
+
};
|
|
1065
|
+
defaultsDeep(res, svc.metadata);
|
|
1066
|
+
svc.mixins?.forEach((mixin) => {
|
|
1067
|
+
defaultsDeep(res, mixin.metadata);
|
|
1068
|
+
mixin.mixins?.forEach((m) => {
|
|
1069
|
+
defaultsDeep(res, getMetadataFromService(m));
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
return res;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const DEFAULT_OPTIONS = {
|
|
1076
|
+
port: 3001,
|
|
1077
|
+
startDebounce: 500,
|
|
1078
|
+
readiness: { path: "/ready" },
|
|
1079
|
+
liveness: { path: "/live" }
|
|
1080
|
+
};
|
|
1081
|
+
function HealthCheckMiddleware(_opts) {
|
|
1082
|
+
const opts = defaultsDeep(_opts, DEFAULT_OPTIONS);
|
|
1083
|
+
let state = "down";
|
|
1084
|
+
let server;
|
|
1085
|
+
function handler(req, res) {
|
|
1086
|
+
if (req.url === opts.readiness.path || req.url === opts.liveness.path) {
|
|
1087
|
+
const resHeader = {
|
|
1088
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
1089
|
+
};
|
|
1090
|
+
const content = {
|
|
1091
|
+
state,
|
|
1092
|
+
uptime: process.uptime(),
|
|
1093
|
+
timestamp: Date.now()
|
|
1094
|
+
};
|
|
1095
|
+
if (req.url === opts.readiness.path) {
|
|
1096
|
+
res.writeHead(state === "up" ? 200 : 503, resHeader);
|
|
1097
|
+
} else {
|
|
1098
|
+
res.writeHead(state !== "down" ? 200 : 503, resHeader);
|
|
1099
|
+
}
|
|
1100
|
+
res.end(JSON.stringify(content, null, 2));
|
|
1101
|
+
} else {
|
|
1102
|
+
res.writeHead(404, STATUS_CODES[404], {});
|
|
1103
|
+
res.end();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
created(broker) {
|
|
1108
|
+
state = "starting";
|
|
1109
|
+
server = http.createServer(handler);
|
|
1110
|
+
server.listen(opts.port, (err) => {
|
|
1111
|
+
if (err) {
|
|
1112
|
+
broker.logger.error("Unable to start health-check server", err);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
broker.logger.info(
|
|
1116
|
+
`Health-check server listening on port ${opts.port}`,
|
|
1117
|
+
{ readiness: opts.readiness.path, liveness: opts.liveness.path }
|
|
1118
|
+
);
|
|
1119
|
+
});
|
|
1120
|
+
},
|
|
1121
|
+
// After broker started
|
|
1122
|
+
started() {
|
|
1123
|
+
const timeout = setTimeout(() => {
|
|
1124
|
+
state = "up";
|
|
1125
|
+
}, opts.startDebounce);
|
|
1126
|
+
timeout.unref();
|
|
1127
|
+
},
|
|
1128
|
+
// Before broker stopping
|
|
1129
|
+
stopping() {
|
|
1130
|
+
state = "stopping";
|
|
1131
|
+
},
|
|
1132
|
+
// After broker stopped
|
|
1133
|
+
stopped() {
|
|
1134
|
+
state = "down";
|
|
1135
|
+
server.close();
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const FILTER_SERVICE_LOGS_REGEX = /('[^']*' service is registered\.)|(Service '[^']*' started\.)|('[^']*' finished starting\.)/;
|
|
1141
|
+
let transport;
|
|
1142
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1143
|
+
transport = {
|
|
1144
|
+
target: "./pino-pretty-transport.cjs",
|
|
1145
|
+
options: {
|
|
1146
|
+
colorize: true,
|
|
1147
|
+
singleLine: true,
|
|
1148
|
+
ignore: [
|
|
1149
|
+
"hostname",
|
|
1150
|
+
// Hide req and res in logs as it will be included in the pretty message
|
|
1151
|
+
// or is not useful in development
|
|
1152
|
+
"req",
|
|
1153
|
+
"res",
|
|
1154
|
+
"responseTime",
|
|
1155
|
+
"span\\.id"
|
|
1156
|
+
].join(",")
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const logger = pino({
|
|
1161
|
+
base: {
|
|
1162
|
+
hostname: hostname(),
|
|
1163
|
+
// Need to set this to allow logs in context of traces
|
|
1164
|
+
// Note that this will not work with dotenv loaded environment variables.
|
|
1165
|
+
"service.name": process.env.NEWRELIC_APP_NAME
|
|
1166
|
+
},
|
|
1167
|
+
transport,
|
|
1168
|
+
hooks: {
|
|
1169
|
+
/**
|
|
1170
|
+
* This function allow logs like `logger.info('str 1', {a: 1}, 'str 2', ...)` to
|
|
1171
|
+
* be formatted correctly (every string are concatenated into one and objects are merged in the log).
|
|
1172
|
+
*/
|
|
1173
|
+
logMethod(args, method) {
|
|
1174
|
+
const mergingObject = {};
|
|
1175
|
+
let msg = "";
|
|
1176
|
+
for (const arg of args) {
|
|
1177
|
+
if (arg instanceof Error) {
|
|
1178
|
+
mergingObject.err = arg;
|
|
1179
|
+
} else if (typeof arg === "string") {
|
|
1180
|
+
msg += (msg ? " " : "") + arg;
|
|
1181
|
+
} else if (arg && typeof arg.msg === "string") {
|
|
1182
|
+
msg += (msg ? " " : "") + arg.msg;
|
|
1183
|
+
Object.assign(mergingObject, arg);
|
|
1184
|
+
} else {
|
|
1185
|
+
Object.assign(mergingObject, arg);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (process.env.FILTER_SERVICE_LOGS === "yes" && FILTER_SERVICE_LOGS_REGEX.test(msg)) {
|
|
1189
|
+
return void 0;
|
|
1190
|
+
}
|
|
1191
|
+
mergingObject.msg = msg;
|
|
1192
|
+
return method.apply(this, [mergingObject]);
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
redact: {
|
|
1196
|
+
paths: [
|
|
1197
|
+
// Request headers
|
|
1198
|
+
"req.headers.authorization",
|
|
1199
|
+
'req.headers["device-useragent"]',
|
|
1200
|
+
"req.headers.connection",
|
|
1201
|
+
'req.headers["content-type"]',
|
|
1202
|
+
'req.headers["accept"]',
|
|
1203
|
+
'req.headers["keep-alive"]',
|
|
1204
|
+
'req.headers["dnt"]',
|
|
1205
|
+
'req.headers["accept-encoding"]',
|
|
1206
|
+
'req.headers["accept-language"]',
|
|
1207
|
+
'req.headers["sec-fetch-site"]',
|
|
1208
|
+
'req.headers["sec-fetch-mode"]',
|
|
1209
|
+
'req.headers["sec-fetch-dest"]',
|
|
1210
|
+
'req.headers["sec-fetch-user"]',
|
|
1211
|
+
'req.headers["sec-ch-ua"]',
|
|
1212
|
+
'req.headers["sec-ch-ua-mobile"]',
|
|
1213
|
+
'req.headers["upgrade-insecure-requests"]',
|
|
1214
|
+
'req.headers["if-none-match"]',
|
|
1215
|
+
'req.headers["cookie"]',
|
|
1216
|
+
"req.headers.referer",
|
|
1217
|
+
// Response headers
|
|
1218
|
+
"res.headers.allow",
|
|
1219
|
+
"res.headers.vary",
|
|
1220
|
+
'res.headers["x-powered-by"]',
|
|
1221
|
+
'res.headers["access-control-allow-origin"]',
|
|
1222
|
+
'res.headers["content-type"]',
|
|
1223
|
+
'res.headers["content-encoding"]'
|
|
1224
|
+
],
|
|
1225
|
+
remove: true
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
const createLogger = (bindings) => logger.child(bindings);
|
|
1229
|
+
function createLoggerConfig() {
|
|
1230
|
+
return {
|
|
1231
|
+
type: "Pino",
|
|
1232
|
+
options: {
|
|
1233
|
+
createLogger: (level, bindings) => createLogger({
|
|
1234
|
+
label: bindings.mod,
|
|
1235
|
+
"trace.id": bindings.traceID,
|
|
1236
|
+
"span.id": bindings.spanID
|
|
1237
|
+
})
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const defaultLogger = createLogger({ label: "default" });
|
|
1242
|
+
|
|
1243
|
+
function flattenTags(obj, convertToString = false, path = "") {
|
|
1244
|
+
if (!obj) return null;
|
|
1245
|
+
return Object.keys(obj).reduce((res, k) => {
|
|
1246
|
+
const o = obj[k];
|
|
1247
|
+
const pp = (path ? `${path}.` : "") + k;
|
|
1248
|
+
if (isObject(o)) {
|
|
1249
|
+
if ("toHexString" in o) {
|
|
1250
|
+
res[pp] = o.toString();
|
|
1251
|
+
} else {
|
|
1252
|
+
Object.assign(res, flattenTags(o, convertToString, pp));
|
|
1253
|
+
}
|
|
1254
|
+
} else if (o !== void 0 && o !== null) {
|
|
1255
|
+
res[pp] = convertToString ? String(o) : o;
|
|
1256
|
+
}
|
|
1257
|
+
return res;
|
|
1258
|
+
}, {});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
class NewrelicTraceExporter extends TracerExporters.Base {
|
|
1262
|
+
queue;
|
|
1263
|
+
timer = null;
|
|
1264
|
+
defaultTags = {};
|
|
1265
|
+
constructor(opts) {
|
|
1266
|
+
super(opts);
|
|
1267
|
+
this.opts = defaultsDeep(this.opts, {
|
|
1268
|
+
baseURL: process.env.NEW_RELIC_TRACE_API_URL || "https://trace-api.newrelic.com",
|
|
1269
|
+
batchSize: 1e3,
|
|
1270
|
+
insertKey: "",
|
|
1271
|
+
interval: 5,
|
|
1272
|
+
defaultTags: null
|
|
1273
|
+
});
|
|
1274
|
+
this.queue = [];
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Initialize Trace Exporter.
|
|
1278
|
+
*/
|
|
1279
|
+
init(tracer) {
|
|
1280
|
+
super.init(tracer);
|
|
1281
|
+
if (this.opts.interval > 0) {
|
|
1282
|
+
this.timer = setInterval(() => this.flush(), this.opts.interval * 1e3);
|
|
1283
|
+
this.timer.unref();
|
|
1284
|
+
}
|
|
1285
|
+
this.defaultTags = isFunction(this.opts.defaultTags) ? this.opts.defaultTags.call(this, tracer) : this.opts.defaultTags;
|
|
1286
|
+
if (this.defaultTags) {
|
|
1287
|
+
this.defaultTags = flattenTags(this.defaultTags, true);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Stop Trace exporter
|
|
1292
|
+
*/
|
|
1293
|
+
async stop() {
|
|
1294
|
+
if (this.timer) {
|
|
1295
|
+
clearInterval(this.timer);
|
|
1296
|
+
this.timer = null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Span is finished.
|
|
1301
|
+
*/
|
|
1302
|
+
spanFinished(span) {
|
|
1303
|
+
this.queue.push(span);
|
|
1304
|
+
if (this.queue.length >= this.opts.batchSize) {
|
|
1305
|
+
this.flush();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Flush tracing data to NewRelic Zipkin api endpoint
|
|
1310
|
+
*/
|
|
1311
|
+
async flush() {
|
|
1312
|
+
if (!this.queue.length) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const data = this.generateTracingData();
|
|
1316
|
+
this.queue.length = 0;
|
|
1317
|
+
try {
|
|
1318
|
+
const res = await fetch(`${this.opts.baseURL}/trace/v1`, {
|
|
1319
|
+
method: "post",
|
|
1320
|
+
body: JSON.stringify(data),
|
|
1321
|
+
headers: {
|
|
1322
|
+
"Content-Type": "application/json",
|
|
1323
|
+
"Api-Key": this.opts.insertKey,
|
|
1324
|
+
"Data-Format": "newrelic",
|
|
1325
|
+
"Data-Format-Version": "1"
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
if (res.status >= 400) {
|
|
1329
|
+
this.logger.warn(
|
|
1330
|
+
`Unable to upload tracing spans to NewRelic. Status: ${res.status} ${res.statusText}`
|
|
1331
|
+
);
|
|
1332
|
+
} else {
|
|
1333
|
+
this.logger.debug(
|
|
1334
|
+
`Tracing spans (${data.length} spans) uploaded to NewRelic. Status: ${res.statusText}`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
this.logger.warn(
|
|
1339
|
+
`Unable to upload tracing spans to NewRelic. Error:${err.message}`,
|
|
1340
|
+
err
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Generate tracing data for NewRelic
|
|
1346
|
+
*/
|
|
1347
|
+
generateTracingData() {
|
|
1348
|
+
return [
|
|
1349
|
+
{
|
|
1350
|
+
common: { attributes: this.defaultTags || {} },
|
|
1351
|
+
spans: this.queue.map((span) => this.makePayload(span))
|
|
1352
|
+
}
|
|
1353
|
+
];
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Create NewRelic v1 payload
|
|
1357
|
+
*/
|
|
1358
|
+
makePayload(span) {
|
|
1359
|
+
return {
|
|
1360
|
+
// Trace & span IDs
|
|
1361
|
+
"trace.id": span.traceID,
|
|
1362
|
+
id: span.id,
|
|
1363
|
+
timestamp: span.startTime,
|
|
1364
|
+
attributes: {
|
|
1365
|
+
"duration.ms": span.duration,
|
|
1366
|
+
name: span.name,
|
|
1367
|
+
"parent.id": span.parentID,
|
|
1368
|
+
// @ts-expect-error fullName isn't declared on span yet
|
|
1369
|
+
"service.name": span.service?.fullName || null,
|
|
1370
|
+
...flattenTags(span.tags, true),
|
|
1371
|
+
...flattenTags(this.errorToObject(span.error), true, "error") || {}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
class NewrelicMetricsReporter extends MetricReporters.Base {
|
|
1378
|
+
// Those fields are not declared on the Base class.
|
|
1379
|
+
registry;
|
|
1380
|
+
logger;
|
|
1381
|
+
timer = null;
|
|
1382
|
+
defaultTags = {};
|
|
1383
|
+
constructor(opts) {
|
|
1384
|
+
super(opts);
|
|
1385
|
+
this.opts = defaultsDeep(this.opts, {
|
|
1386
|
+
baseURL: process.env.NEW_RELIC_METRICS_API_URL || "https://metric-api.newrelic.com",
|
|
1387
|
+
insertKey: "",
|
|
1388
|
+
interval: 10
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Initialize Trace Exporter.
|
|
1393
|
+
*/
|
|
1394
|
+
init(registry) {
|
|
1395
|
+
super.init(registry);
|
|
1396
|
+
if (this.opts.interval > 0) {
|
|
1397
|
+
this.timer = setInterval(() => this.flush(), this.opts.interval * 1e3);
|
|
1398
|
+
this.timer.unref();
|
|
1399
|
+
}
|
|
1400
|
+
const defaultTags = isFunction(this.opts.defaultTags) ? this.opts.defaultTags.call(this, registry) : this.opts.defaultTags;
|
|
1401
|
+
if (defaultTags) {
|
|
1402
|
+
this.defaultTags = flattenTags(defaultTags, true);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Stop Trace exporter
|
|
1407
|
+
*/
|
|
1408
|
+
async stop() {
|
|
1409
|
+
if (this.timer) {
|
|
1410
|
+
clearInterval(this.timer);
|
|
1411
|
+
this.timer = null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Flush tracing data to NewRelic Zipkin api endpoint
|
|
1416
|
+
*/
|
|
1417
|
+
async flush() {
|
|
1418
|
+
const data = this.generateMetricsPayload();
|
|
1419
|
+
if (!this.opts.insertKey) {
|
|
1420
|
+
this.logger.warn(
|
|
1421
|
+
`Unable to upload metrics to NewRelic. No insertKey provided.`
|
|
1422
|
+
);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
const res = await fetch(`${this.opts.baseURL}/metric/v1`, {
|
|
1427
|
+
method: "post",
|
|
1428
|
+
body: JSON.stringify(data),
|
|
1429
|
+
headers: {
|
|
1430
|
+
"Content-Type": "application/json",
|
|
1431
|
+
"Api-Key": this.opts.insertKey
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
if (res.status >= 400) {
|
|
1435
|
+
this.logger.warn(
|
|
1436
|
+
`Unable to upload metrics to NewRelic. Status: ${res.status} ${res.statusText}`
|
|
1437
|
+
);
|
|
1438
|
+
} else {
|
|
1439
|
+
this.logger.debug(
|
|
1440
|
+
`Tracing metrics uploaded to NewRelic. Status: ${res.statusText}`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
this.logger.warn(
|
|
1445
|
+
`Unable to upload metrics to NewRelic. Error:${err.message}`,
|
|
1446
|
+
err
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Generate metrics data for NewRelic
|
|
1452
|
+
*/
|
|
1453
|
+
generateMetricsPayload() {
|
|
1454
|
+
const metrics = [];
|
|
1455
|
+
this.registry.store.forEach((metric) => {
|
|
1456
|
+
if (!this.matchMetricName(metric.name)) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (metric.name.startsWith("os.datetime")) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const snapshot = metric.snapshot();
|
|
1463
|
+
if (!snapshot.length) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
switch (metric.type) {
|
|
1467
|
+
case "counter":
|
|
1468
|
+
case "gauge":
|
|
1469
|
+
snapshot.forEach((item) => {
|
|
1470
|
+
const { value, timestamp, labels } = item;
|
|
1471
|
+
metrics.push({
|
|
1472
|
+
name: metric.name,
|
|
1473
|
+
attributes: labels,
|
|
1474
|
+
type: "gauge",
|
|
1475
|
+
timestamp,
|
|
1476
|
+
value
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
break;
|
|
1480
|
+
case "histogram":
|
|
1481
|
+
snapshot.forEach((item) => {
|
|
1482
|
+
const {
|
|
1483
|
+
timestamp,
|
|
1484
|
+
labels,
|
|
1485
|
+
count,
|
|
1486
|
+
min,
|
|
1487
|
+
max,
|
|
1488
|
+
sum,
|
|
1489
|
+
buckets,
|
|
1490
|
+
quantiles
|
|
1491
|
+
} = item;
|
|
1492
|
+
metrics.push({
|
|
1493
|
+
name: metric.name,
|
|
1494
|
+
type: "summary",
|
|
1495
|
+
attributes: labels,
|
|
1496
|
+
timestamp,
|
|
1497
|
+
value: { count, sum, min: min || 0, max: max || 0 },
|
|
1498
|
+
"interval.ms": this.opts.interval * 1e3
|
|
1499
|
+
});
|
|
1500
|
+
if (buckets) {
|
|
1501
|
+
Object.entries(buckets).forEach(([key, val]) => {
|
|
1502
|
+
metrics.push({
|
|
1503
|
+
name: `${metric.name}.${key}`,
|
|
1504
|
+
type: "gauge",
|
|
1505
|
+
attributes: labels,
|
|
1506
|
+
timestamp,
|
|
1507
|
+
value: val
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (quantiles) {
|
|
1512
|
+
Object.entries(quantiles).forEach(([key, val]) => {
|
|
1513
|
+
metrics.push({
|
|
1514
|
+
name: `${metric.name}.q.${key}`,
|
|
1515
|
+
type: "gauge",
|
|
1516
|
+
attributes: labels,
|
|
1517
|
+
timestamp,
|
|
1518
|
+
value: val
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
return [
|
|
1527
|
+
{
|
|
1528
|
+
common: { attributes: this.defaultTags || {} },
|
|
1529
|
+
metrics
|
|
1530
|
+
}
|
|
1531
|
+
];
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export { AjvExtractor, AjvValidator, COERCE_ARRAY_ATTRIBUTE, ContextFactory, HealthCheckMiddleware, NewrelicMetricsReporter, NewrelicTraceExporter, OpenAPIExtractor, OpenAPIMixin, RefExtractor, SCHEMA_REF_NAME, ServiceFactory, ZodValidator, createLogger, createLoggerConfig, createServiceBroker, defaultLogger, getMetadataFromService, isServiceSelected, omitFields, wrapMixin, zodToOpenAPISchema };
|