@backstage-community/plugin-code-coverage-backend 0.2.32

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.
@@ -0,0 +1,650 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var express = require('express');
6
+ var Router = require('express-promise-router');
7
+ var BodyParser = require('body-parser');
8
+ var bodyParserXml = require('body-parser-xml');
9
+ var catalogClient = require('@backstage/catalog-client');
10
+ var backendCommon = require('@backstage/backend-common');
11
+ var errors = require('@backstage/errors');
12
+ var integration = require('@backstage/integration');
13
+ var catalogModel = require('@backstage/catalog-model');
14
+ var uuid = require('uuid');
15
+ var backendPluginApi = require('@backstage/backend-plugin-api');
16
+
17
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
18
+
19
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
20
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
21
+ var BodyParser__default = /*#__PURE__*/_interopDefaultCompat(BodyParser);
22
+ var bodyParserXml__default = /*#__PURE__*/_interopDefaultCompat(bodyParserXml);
23
+
24
+ const calculatePercentage = (available, covered) => {
25
+ if (available === 0) {
26
+ return 0;
27
+ }
28
+ return parseFloat((covered / available * 100).toFixed(2));
29
+ };
30
+ const aggregateCoverage = (c) => {
31
+ let availableLine = 0;
32
+ let coveredLine = 0;
33
+ let availableBranch = 0;
34
+ let coveredBranch = 0;
35
+ c.files.forEach((f) => {
36
+ availableLine += Object.keys(f.lineHits).length;
37
+ coveredLine += Object.values(f.lineHits).filter((l) => l > 0).length;
38
+ availableBranch += Object.keys(f.branchHits).map((b) => parseInt(b, 10)).map((b) => f.branchHits[b].available).filter(Boolean).reduce((acc, curr) => acc + curr, 0);
39
+ coveredBranch += Object.keys(f.branchHits).map((b) => parseInt(b, 10)).map((b) => f.branchHits[b].covered).filter(Boolean).reduce((acc, curr) => acc + curr, 0);
40
+ });
41
+ return {
42
+ timestamp: c.metadata.generationTime,
43
+ branch: {
44
+ available: availableBranch,
45
+ covered: coveredBranch,
46
+ missed: availableBranch - coveredBranch,
47
+ percentage: calculatePercentage(availableBranch, coveredBranch)
48
+ },
49
+ line: {
50
+ available: availableLine,
51
+ covered: coveredLine,
52
+ missed: availableLine - coveredLine,
53
+ percentage: calculatePercentage(availableLine, coveredLine)
54
+ }
55
+ };
56
+ };
57
+ class CoverageUtils {
58
+ constructor(scm, urlReader) {
59
+ this.scm = scm;
60
+ this.urlReader = urlReader;
61
+ }
62
+ async processCoveragePayload(entity, req) {
63
+ var _a, _b, _c, _d, _e;
64
+ const enforceScmFiles = ((_a = entity.metadata.annotations) == null ? void 0 : _a["backstage.io/code-coverage"]) === "scm-only" || false;
65
+ let sourceLocation = void 0;
66
+ let vcs = void 0;
67
+ let scmFiles = [];
68
+ if (enforceScmFiles) {
69
+ try {
70
+ const sl = catalogModel.getEntitySourceLocation(entity);
71
+ sourceLocation = sl.target;
72
+ } catch (e) {
73
+ }
74
+ if (!sourceLocation) {
75
+ throw new errors.InputError(
76
+ `No "backstage.io/source-location" annotation on entity ${catalogModel.stringifyEntityRef(
77
+ entity
78
+ )}`
79
+ );
80
+ }
81
+ vcs = (_c = (_b = this.scm).byUrl) == null ? void 0 : _c.call(_b, sourceLocation);
82
+ if (!vcs) {
83
+ throw new errors.InputError(`Unable to determine SCM from ${sourceLocation}`);
84
+ }
85
+ const scmTree = await ((_e = (_d = this.urlReader).readTree) == null ? void 0 : _e.call(_d, sourceLocation));
86
+ if (!scmTree) {
87
+ throw new errors.NotFoundError(`Unable to read tree from ${sourceLocation}`);
88
+ }
89
+ scmFiles = (await scmTree.files()).map((f) => f.path);
90
+ }
91
+ const body = this.validateRequestBody(req);
92
+ if (Object.keys(body).length === 0) {
93
+ throw new errors.InputError("Unable to parse body");
94
+ }
95
+ return {
96
+ sourceLocation,
97
+ vcs,
98
+ scmFiles,
99
+ body
100
+ };
101
+ }
102
+ async buildCoverage(entity, sourceLocation, vcs, files) {
103
+ return {
104
+ metadata: {
105
+ vcs: {
106
+ type: (vcs == null ? void 0 : vcs.type) || "unknown",
107
+ location: sourceLocation || "unknown"
108
+ },
109
+ generationTime: Date.now()
110
+ },
111
+ entity: {
112
+ name: entity.metadata.name,
113
+ namespace: entity.metadata.namespace || "default",
114
+ kind: entity.kind
115
+ },
116
+ files
117
+ };
118
+ }
119
+ validateRequestBody(req) {
120
+ const contentType = req.headers["content-type"];
121
+ if (!contentType) {
122
+ throw new errors.InputError("Content-Type header missing");
123
+ } else if (!contentType.match(/^text\/xml|plain($|;)/)) {
124
+ throw new errors.InputError(
125
+ `Content-Type header "${contentType}" not supported, expected "text/xml" or "text/plain" possibly followed by a charset`
126
+ );
127
+ }
128
+ const body = req.body;
129
+ if (!body) {
130
+ throw new errors.InputError("Missing request body");
131
+ }
132
+ return body;
133
+ }
134
+ }
135
+
136
+ const migrationsDir = backendCommon.resolvePackagePath(
137
+ "@backstage-community/plugin-code-coverage-backend",
138
+ "migrations"
139
+ );
140
+ class CodeCoverageDatabase {
141
+ constructor(db) {
142
+ this.db = db;
143
+ }
144
+ static async create(database) {
145
+ var _a;
146
+ const knex = await database.getClient();
147
+ if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
148
+ await knex.migrate.latest({
149
+ directory: migrationsDir
150
+ });
151
+ }
152
+ return new CodeCoverageDatabase(knex);
153
+ }
154
+ async insertCodeCoverage(coverage) {
155
+ const codeCoverageId = uuid.v4();
156
+ const entity = catalogModel.stringifyEntityRef({
157
+ kind: coverage.entity.kind,
158
+ namespace: coverage.entity.namespace,
159
+ name: coverage.entity.name
160
+ });
161
+ await this.db("code_coverage").insert({
162
+ id: codeCoverageId,
163
+ entity,
164
+ coverage: JSON.stringify(coverage)
165
+ });
166
+ return { codeCoverageId };
167
+ }
168
+ async getCodeCoverage(entity) {
169
+ const [result] = await this.db("code_coverage").where({ entity }).orderBy("index", "desc").limit(1).select();
170
+ if (!result) {
171
+ throw new errors.NotFoundError(
172
+ `No coverage for entity '${JSON.stringify(entity)}' found`
173
+ );
174
+ }
175
+ try {
176
+ return JSON.parse(result.coverage);
177
+ } catch (error) {
178
+ throw new Error(`Failed to parse coverage for '${entity}', ${error}`);
179
+ }
180
+ }
181
+ async getHistory(entity, limit) {
182
+ const res = await this.db("code_coverage").where({ entity }).orderBy("index", "desc").limit(limit).select();
183
+ const history = res.map((r) => JSON.parse(r.coverage)).map((c) => aggregateCoverage(c));
184
+ const entityName = catalogModel.parseEntityRef(entity);
185
+ return {
186
+ entity: {
187
+ name: entityName.name,
188
+ kind: entityName.kind,
189
+ namespace: entityName.namespace
190
+ },
191
+ history
192
+ };
193
+ }
194
+ }
195
+
196
+ class Cobertura {
197
+ constructor(logger) {
198
+ this.logger = logger;
199
+ this.logger = logger;
200
+ }
201
+ /**
202
+ * convert cobertura into shared json coverage format
203
+ *
204
+ * @param xml - cobertura xml object
205
+ * @param scmFiles - list of files that are committed to SCM
206
+ */
207
+ convert(xml, scmFiles) {
208
+ var _a, _b;
209
+ const ppc = (_a = xml.coverage.packages) == null ? void 0 : _a.flatMap((p) => p.package).filter(Boolean).flatMap((p) => p.classes);
210
+ const pc = (_b = xml.coverage.package) == null ? void 0 : _b.filter(Boolean).flatMap((p) => p.classes);
211
+ const classes = [ppc, pc].flat().filter(Boolean).flatMap((c) => c.class).filter(Boolean);
212
+ const jscov = [];
213
+ classes.forEach((c) => {
214
+ const packageAndFilename = c.$.filename;
215
+ const lines = this.extractLines(c);
216
+ const lineHits = {};
217
+ const branchHits = {};
218
+ lines.forEach((l) => {
219
+ if (!lineHits[l.number]) {
220
+ lineHits[l.number] = 0;
221
+ }
222
+ lineHits[l.number] += l.hits;
223
+ if (l.branch && l["condition-coverage"]) {
224
+ const bh = this.parseBranch(l["condition-coverage"]);
225
+ if (bh) {
226
+ branchHits[l.number] = bh;
227
+ }
228
+ }
229
+ });
230
+ const currentFile = scmFiles.map((f) => f.trimEnd()).find((f) => f.endsWith(packageAndFilename));
231
+ this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
232
+ if (scmFiles.length === 0 || Object.keys(lineHits).length > 0 && currentFile) {
233
+ jscov.push({
234
+ filename: currentFile || packageAndFilename,
235
+ branchHits,
236
+ lineHits
237
+ });
238
+ }
239
+ });
240
+ return jscov;
241
+ }
242
+ /**
243
+ * Parses branch coverage information from condition-coverage
244
+ *
245
+ * @param condition - condition-coverage value from line coverage
246
+ */
247
+ parseBranch(condition) {
248
+ const pattern = /[0-9\.]+\%\s\(([0-9]+)\/([0-9]+)\)/;
249
+ const match = condition.match(pattern);
250
+ if (!match) {
251
+ return null;
252
+ }
253
+ const covered = parseInt(match[1], 10);
254
+ const available = parseInt(match[2], 10);
255
+ return {
256
+ covered,
257
+ missed: available - covered,
258
+ available
259
+ };
260
+ }
261
+ /**
262
+ * Extract line hits from a class coverage entry
263
+ *
264
+ * @param clz - class coverage information
265
+ */
266
+ extractLines(clz) {
267
+ var _a;
268
+ const classLines = clz.lines.flatMap((l) => l.line);
269
+ const methodLines = (_a = clz.methods) == null ? void 0 : _a.flatMap((m) => m.method).filter(Boolean).flatMap((m) => m.lines).filter(Boolean).flatMap((l) => l.line).filter(
270
+ ({ $: methodLine }) => classLines.some(
271
+ ({ $: classLine }) => methodLine.number === classLine.number
272
+ ) === false
273
+ );
274
+ const lines = [classLines, methodLines].flat().filter(Boolean);
275
+ const lineHits = lines.map((l) => {
276
+ return {
277
+ number: parseInt(l.$.number, 10),
278
+ hits: parseInt(l.$.hits, 10),
279
+ "condition-coverage": l.$["condition-coverage"],
280
+ branch: l.$.branch
281
+ };
282
+ });
283
+ return lineHits;
284
+ }
285
+ }
286
+
287
+ class Jacoco {
288
+ constructor(logger) {
289
+ this.logger = logger;
290
+ this.logger = logger;
291
+ }
292
+ /**
293
+ * Converts jacoco into shared json coverage format
294
+ *
295
+ * @param xml - jacoco xml object
296
+ * @param scmFiles - list of files that are committed to SCM
297
+ */
298
+ convert(xml, scmFiles) {
299
+ const jscov = [];
300
+ xml.report.package.forEach((r) => {
301
+ const packageName = r.$.name;
302
+ r.sourcefile.forEach((sf) => {
303
+ const fileName = sf.$.name;
304
+ const lines = this.extractLines(sf);
305
+ const lineHits = {};
306
+ const branchHits = {};
307
+ lines.forEach((l) => {
308
+ if (!lineHits[l.number]) {
309
+ lineHits[l.number] = 0;
310
+ }
311
+ lineHits[l.number] += l.covered_instructions;
312
+ const ab = l.covered_branches + l.missed_branches;
313
+ if (ab > 0) {
314
+ branchHits[l.number] = {
315
+ covered: l.covered_branches,
316
+ missed: l.missed_branches,
317
+ available: ab
318
+ };
319
+ }
320
+ });
321
+ const packageAndFilename = `${packageName}/${fileName}`;
322
+ const currentFile = scmFiles.map((f) => f.trimEnd()).find((f) => f.endsWith(packageAndFilename));
323
+ this.logger.debug(`matched ${packageAndFilename} to ${currentFile}`);
324
+ if (scmFiles.length === 0 || Object.keys(lineHits).length > 0 && currentFile) {
325
+ jscov.push({
326
+ filename: currentFile || packageAndFilename,
327
+ branchHits,
328
+ lineHits
329
+ });
330
+ }
331
+ });
332
+ });
333
+ return jscov;
334
+ }
335
+ extractLines(sourcefile) {
336
+ var _a;
337
+ const parsed = [];
338
+ (_a = sourcefile.line) == null ? void 0 : _a.forEach((l) => {
339
+ parsed.push({
340
+ number: parseInt(l.$.nr, 10),
341
+ missed_instructions: parseInt(l.$.mi, 10),
342
+ covered_instructions: parseInt(l.$.ci, 10),
343
+ missed_branches: parseInt(l.$.mb, 10),
344
+ covered_branches: parseInt(l.$.cb, 10)
345
+ });
346
+ });
347
+ return parsed;
348
+ }
349
+ }
350
+
351
+ class Lcov {
352
+ constructor(logger) {
353
+ this.logger = logger;
354
+ this.logger = logger;
355
+ }
356
+ /**
357
+ * convert lcov into shared json coverage format
358
+ *
359
+ * @param raw - raw lcov report
360
+ * @param scmFiles - list of files that are committed to SCM
361
+ */
362
+ convert(raw, scmFiles) {
363
+ const lines = raw.split(/\r?\n/);
364
+ const jscov = [];
365
+ let currentFile = null;
366
+ lines.forEach((line) => {
367
+ const [section, value] = line.split(":");
368
+ switch (section) {
369
+ case "SF":
370
+ currentFile = this.processNewFile(value, scmFiles);
371
+ break;
372
+ case "DA":
373
+ this.processLineHit(currentFile, value);
374
+ break;
375
+ case "BRDA":
376
+ this.processBranchHit(currentFile, value);
377
+ break;
378
+ case "end_of_record":
379
+ if (currentFile) {
380
+ jscov.push(currentFile);
381
+ currentFile = null;
382
+ }
383
+ break;
384
+ }
385
+ });
386
+ return jscov;
387
+ }
388
+ /**
389
+ * Parses a new file entry
390
+ *
391
+ * @param file - file name from coverage report
392
+ * @param scmFiles - list of files that are committed to SCM
393
+ */
394
+ processNewFile(file, scmFiles) {
395
+ const filename = scmFiles.map((f) => f.trimEnd()).find((f) => file.endsWith(f));
396
+ this.logger.debug(`matched ${file} to ${filename}`);
397
+ if (scmFiles.length === 0 || filename) {
398
+ return {
399
+ filename: filename || file,
400
+ lineHits: {},
401
+ branchHits: {}
402
+ };
403
+ }
404
+ return null;
405
+ }
406
+ /**
407
+ * Parses line coverage information
408
+ *
409
+ * @param currentFile - current file entry
410
+ * @param value - line coverage information
411
+ */
412
+ processLineHit(currentFile, value) {
413
+ if (!currentFile)
414
+ return;
415
+ const [lineNumber, hits] = value.split(",");
416
+ currentFile.lineHits[Number(lineNumber)] = Number(hits);
417
+ }
418
+ /**
419
+ * Parses branch coverage information
420
+ *
421
+ * @param currentFile - current file entry
422
+ * @param value - branch coverage information
423
+ */
424
+ processBranchHit(currentFile, value) {
425
+ if (!currentFile)
426
+ return;
427
+ const [lineNumber, , , hits] = value.split(",");
428
+ const lineNumberNum = Number(lineNumber);
429
+ const isHit = Number(hits) > 0;
430
+ const branch = currentFile.branchHits[lineNumberNum] || {
431
+ covered: 0,
432
+ available: 0,
433
+ missed: 0
434
+ };
435
+ branch.available++;
436
+ branch.covered += isHit ? 1 : 0;
437
+ branch.missed += isHit ? 0 : 1;
438
+ currentFile.branchHits[lineNumberNum] = branch;
439
+ }
440
+ }
441
+
442
+ const makeRouter = async (options) => {
443
+ var _a, _b;
444
+ const { config, logger, discovery, database, urlReader } = options;
445
+ const codeCoverageDatabase = await CodeCoverageDatabase.create(database);
446
+ const codecovUrl = await discovery.getExternalBaseUrl("code-coverage");
447
+ const catalogApi = (_a = options.catalogApi) != null ? _a : new catalogClient.CatalogClient({ discoveryApi: discovery });
448
+ const scm = integration.ScmIntegrations.fromConfig(config);
449
+ const { auth, httpAuth } = backendCommon.createLegacyAuthAdapters(options);
450
+ const bodySizeLimit = (_b = config.getOptionalString("codeCoverage.bodySizeLimit")) != null ? _b : "100kb";
451
+ bodyParserXml__default.default(BodyParser__default.default);
452
+ const router = Router__default.default();
453
+ router.use(
454
+ BodyParser__default.default.xml({
455
+ limit: bodySizeLimit
456
+ })
457
+ );
458
+ router.use(
459
+ BodyParser__default.default.text({
460
+ limit: bodySizeLimit
461
+ })
462
+ );
463
+ router.use(express__default.default.json());
464
+ const utils = new CoverageUtils(scm, urlReader);
465
+ router.get("/health", async (_req, res) => {
466
+ res.status(200).json({ status: "ok" });
467
+ });
468
+ router.get("/report", async (req, res) => {
469
+ const { entity } = req.query;
470
+ const entityLookup = await catalogApi.getEntityByRef(
471
+ entity,
472
+ await auth.getPluginRequestToken({
473
+ onBehalfOf: await httpAuth.credentials(req),
474
+ targetPluginId: "catalog"
475
+ })
476
+ );
477
+ if (!entityLookup) {
478
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
479
+ }
480
+ const stored = await codeCoverageDatabase.getCodeCoverage(entity);
481
+ const aggregate = aggregateCoverage(stored);
482
+ res.status(200).json({
483
+ ...stored,
484
+ aggregate: {
485
+ line: aggregate.line,
486
+ branch: aggregate.branch
487
+ }
488
+ });
489
+ });
490
+ router.get("/history", async (req, res) => {
491
+ const { entity } = req.query;
492
+ const entityLookup = await catalogApi.getEntityByRef(
493
+ entity,
494
+ await auth.getPluginRequestToken({
495
+ onBehalfOf: await httpAuth.credentials(req),
496
+ targetPluginId: "catalog"
497
+ })
498
+ );
499
+ if (!entityLookup) {
500
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
501
+ }
502
+ const { limit } = req.query;
503
+ const history = await codeCoverageDatabase.getHistory(
504
+ entity,
505
+ parseInt((limit == null ? void 0 : limit.toString()) || "10", 10)
506
+ );
507
+ res.status(200).json(history);
508
+ });
509
+ router.get("/file-content", async (req, res) => {
510
+ const { entity, path } = req.query;
511
+ const entityLookup = await catalogApi.getEntityByRef(
512
+ entity,
513
+ await auth.getPluginRequestToken({
514
+ onBehalfOf: await httpAuth.credentials(req),
515
+ targetPluginId: "catalog"
516
+ })
517
+ );
518
+ if (!entityLookup) {
519
+ throw new errors.NotFoundError(`No entity found matching ${entity}`);
520
+ }
521
+ if (!path) {
522
+ throw new errors.InputError("Need path query parameter");
523
+ }
524
+ const sourceLocation = catalogModel.getEntitySourceLocation(entityLookup);
525
+ if (!sourceLocation) {
526
+ throw new errors.InputError(
527
+ `No "backstage.io/source-location" annotation on entity ${entity}`
528
+ );
529
+ }
530
+ const vcs = scm.byUrl(sourceLocation.target);
531
+ if (!vcs) {
532
+ throw new errors.InputError(`Unable to determine SCM from ${sourceLocation}`);
533
+ }
534
+ const scmTree = await urlReader.readTree(sourceLocation.target);
535
+ const scmFile = (await scmTree.files()).find((f) => f.path === path);
536
+ if (!scmFile) {
537
+ res.status(404).json({
538
+ message: "Couldn't find file in SCM",
539
+ file: path,
540
+ scm: vcs.title
541
+ });
542
+ return;
543
+ }
544
+ const content = await (scmFile == null ? void 0 : scmFile.content());
545
+ if (!content) {
546
+ res.status(400).json({
547
+ message: "Couldn't process content of file in SCM",
548
+ file: path,
549
+ scm: vcs.title
550
+ });
551
+ return;
552
+ }
553
+ const data = content.toString();
554
+ res.status(200).contentType("text/plain").send(data);
555
+ });
556
+ router.post("/report", async (req, res) => {
557
+ const { entity: entityRef, coverageType } = req.query;
558
+ const entity = await catalogApi.getEntityByRef(
559
+ entityRef,
560
+ await auth.getPluginRequestToken({
561
+ onBehalfOf: await httpAuth.credentials(req),
562
+ targetPluginId: "catalog"
563
+ })
564
+ );
565
+ if (!entity) {
566
+ throw new errors.NotFoundError(`No entity found matching ${entityRef}`);
567
+ }
568
+ let converter;
569
+ if (!coverageType) {
570
+ throw new errors.InputError("Need coverageType query parameter");
571
+ } else if (coverageType === "jacoco") {
572
+ converter = new Jacoco(logger);
573
+ } else if (coverageType === "cobertura") {
574
+ converter = new Cobertura(logger);
575
+ } else if (coverageType === "lcov") {
576
+ converter = new Lcov(logger);
577
+ } else {
578
+ throw new errors.InputError(`Unsupported coverage type '${coverageType}`);
579
+ }
580
+ const { sourceLocation, vcs, scmFiles, body } = await utils.processCoveragePayload(entity, req);
581
+ const files = converter.convert(body, scmFiles);
582
+ if (!files || files.length === 0) {
583
+ throw new errors.InputError(`Unable to parse body as ${coverageType}`);
584
+ }
585
+ const coverage = await utils.buildCoverage(
586
+ entity,
587
+ sourceLocation,
588
+ vcs,
589
+ files
590
+ );
591
+ await codeCoverageDatabase.insertCodeCoverage(coverage);
592
+ res.status(201).json({
593
+ links: [
594
+ {
595
+ rel: "coverage",
596
+ href: `${codecovUrl}/report?entity=${entityRef}`
597
+ }
598
+ ]
599
+ });
600
+ });
601
+ router.use(backendCommon.errorHandler());
602
+ return router;
603
+ };
604
+ async function createRouter(options) {
605
+ const logger = options.logger;
606
+ logger.info("Initializing Code Coverage backend");
607
+ return makeRouter(options);
608
+ }
609
+
610
+ const codeCoveragePlugin = backendPluginApi.createBackendPlugin({
611
+ pluginId: "codeCoverage",
612
+ register(env) {
613
+ env.registerInit({
614
+ deps: {
615
+ config: backendPluginApi.coreServices.rootConfig,
616
+ logger: backendPluginApi.coreServices.logger,
617
+ urlReader: backendPluginApi.coreServices.urlReader,
618
+ httpRouter: backendPluginApi.coreServices.httpRouter,
619
+ discovery: backendPluginApi.coreServices.discovery,
620
+ database: backendPluginApi.coreServices.database
621
+ },
622
+ async init({
623
+ config,
624
+ logger,
625
+ urlReader,
626
+ httpRouter,
627
+ discovery,
628
+ database
629
+ }) {
630
+ httpRouter.use(
631
+ await createRouter({
632
+ config,
633
+ logger,
634
+ urlReader,
635
+ discovery,
636
+ database
637
+ })
638
+ );
639
+ httpRouter.addAuthPolicy({
640
+ path: "/health",
641
+ allow: "unauthenticated"
642
+ });
643
+ }
644
+ });
645
+ }
646
+ });
647
+
648
+ exports.createRouter = createRouter;
649
+ exports.default = codeCoveragePlugin;
650
+ //# sourceMappingURL=index.cjs.js.map