@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/context-factory-BWO3xPWE.d.cts +520 -0
  4. package/dist/context-factory-BWO3xPWE.d.mts +520 -0
  5. package/dist/index-82e1CXJX.cjs +11 -0
  6. package/dist/index-BV1ZqQrU.mjs +351 -0
  7. package/dist/index-DNJWwcZu.mjs +8 -0
  8. package/dist/index-rZl77S1z.cjs +375 -0
  9. package/dist/index.cjs +1570 -0
  10. package/dist/index.d.cts +373 -0
  11. package/dist/index.d.mts +373 -0
  12. package/dist/index.mjs +1535 -0
  13. package/dist/mixins/database.mixin.cjs +1673 -0
  14. package/dist/mixins/database.mixin.d.cts +958 -0
  15. package/dist/mixins/database.mixin.d.mts +958 -0
  16. package/dist/mixins/database.mixin.mjs +1645 -0
  17. package/dist/mixins/encryptor.mixin.cjs +84 -0
  18. package/dist/mixins/encryptor.mixin.d.cts +31 -0
  19. package/dist/mixins/encryptor.mixin.d.mts +31 -0
  20. package/dist/mixins/encryptor.mixin.mjs +81 -0
  21. package/dist/mixins/global-store.mixin.cjs +56 -0
  22. package/dist/mixins/global-store.mixin.d.cts +39 -0
  23. package/dist/mixins/global-store.mixin.d.mts +39 -0
  24. package/dist/mixins/global-store.mixin.mjs +54 -0
  25. package/dist/mixins/jwt.mixin.cjs +118 -0
  26. package/dist/mixins/jwt.mixin.d.cts +43 -0
  27. package/dist/mixins/jwt.mixin.d.mts +43 -0
  28. package/dist/mixins/jwt.mixin.mjs +115 -0
  29. package/dist/mixins/queue.mixin.cjs +420 -0
  30. package/dist/mixins/queue.mixin.d.cts +150 -0
  31. package/dist/mixins/queue.mixin.d.mts +150 -0
  32. package/dist/mixins/queue.mixin.mjs +414 -0
  33. package/dist/mixins/redis.mixin.cjs +50 -0
  34. package/dist/mixins/redis.mixin.d.cts +27 -0
  35. package/dist/mixins/redis.mixin.d.mts +27 -0
  36. package/dist/mixins/redis.mixin.mjs +48 -0
  37. package/dist/mixins/redlock.mixin.cjs +76 -0
  38. package/dist/mixins/redlock.mixin.d.cts +30 -0
  39. package/dist/mixins/redlock.mixin.d.mts +30 -0
  40. package/dist/mixins/redlock.mixin.mjs +74 -0
  41. 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 };