@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.5.2 → 2.7.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 (36) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +39 -14
  3. package/config.d.ts +20 -42
  4. package/dist/constants/aggregationKPIs.cjs.js +22 -0
  5. package/dist/constants/aggregationKPIs.cjs.js.map +1 -1
  6. package/dist/plugin.cjs.js +12 -3
  7. package/dist/plugin.cjs.js.map +1 -1
  8. package/dist/providers/MetricProvidersRegistry.cjs.js +74 -48
  9. package/dist/providers/MetricProvidersRegistry.cjs.js.map +1 -1
  10. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +88 -1
  11. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
  12. package/dist/service/CatalogMetricService.cjs.js +6 -74
  13. package/dist/service/CatalogMetricService.cjs.js.map +1 -1
  14. package/dist/service/aggregations/AggregatedMetricLoader.cjs.js +23 -0
  15. package/dist/service/aggregations/AggregatedMetricLoader.cjs.js.map +1 -0
  16. package/dist/service/aggregations/AggregationService.cjs.js +54 -0
  17. package/dist/service/aggregations/AggregationService.cjs.js.map +1 -0
  18. package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js +110 -0
  19. package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js.map +1 -0
  20. package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js +34 -0
  21. package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js.map +1 -0
  22. package/dist/service/aggregations/strategies/registerStrategies.cjs.js +18 -0
  23. package/dist/service/aggregations/strategies/registerStrategies.cjs.js.map +1 -0
  24. package/dist/service/mappers.cjs.js +2 -10
  25. package/dist/service/mappers.cjs.js.map +1 -1
  26. package/dist/service/router.cjs.js +18 -29
  27. package/dist/service/router.cjs.js.map +1 -1
  28. package/dist/utils/buildAggregationConfig.cjs.js +31 -1
  29. package/dist/utils/buildAggregationConfig.cjs.js.map +1 -1
  30. package/dist/utils/mergeEntityAndProviderThresholds.cjs.js +1 -1
  31. package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +1 -1
  32. package/dist/validation/schemas/aggregationConfigSchemas.cjs.js +40 -0
  33. package/dist/validation/schemas/aggregationConfigSchemas.cjs.js.map +1 -0
  34. package/dist/validation/validateAggregationConfig.cjs.js +7 -10
  35. package/dist/validation/validateAggregationConfig.cjs.js.map +1 -1
  36. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 2.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bf72ffc: Adds `**average**` as an aggregation KPI type alongside `**statusGrouped**`, with configurable `**options.statusScores**` and optional `**options.thresholds**` (same shape as metric thresholds) for homepage donut coloring against `**averageScore × 100**`, with built-in defaults when omitted.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [bf72ffc]
12
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.0
13
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.0
14
+
15
+ ## 2.6.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 4ecaacd: Add support for batch metric providers, allowing a single provider to handle multiple metrics efficiently. Introduce a new backend module for configurable file existence checks (filecheck.\*) that verify whether required files (like README, LICENSE, or CODEOWNERS) are present in a repository.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [4ecaacd]
24
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.6.0
25
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.6.0
26
+
3
27
  ## 2.5.2
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -88,12 +88,13 @@ For more information about schedule configuration options, see the [Metric Colle
88
88
 
89
89
  The following metric providers are available:
90
90
 
91
- | Provider | Metric ID | Title | Description | Type |
92
- | -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ |
93
- | **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
94
- | **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
95
- | **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
96
- | **Dependabot** | `dependabot.*` | Dependabot Alerts | Critical, High, Medium and Low CVE Alerts | number |
91
+ | Provider | Metric ID | Title | Description | Type |
92
+ | -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------- |
93
+ | **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
94
+ | **Filecheck** | `filecheck.*` | File Checks | Checks whether specific files (e.g., `README.md`, `LICENSE`, `CODEOWNERS`) exist in a repository. | boolean |
95
+ | **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
96
+ | **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
97
+ | **Dependabot** | `dependabot.*` | Dependabot Alerts | Critical, High, Medium and Low CVE Alerts | number |
97
98
 
98
99
  To use these providers, install the corresponding backend modules:
99
100
 
@@ -101,6 +102,7 @@ To use these providers, install the corresponding backend modules:
101
102
  - Jira: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira`](../scorecard-backend-module-jira/README.md)
102
103
  - OpenSSF: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf`](../scorecard-backend-module-openssf/README.md)
103
104
  - Dependabot: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot`](../scorecard-backend-module-dependabot/README.md)
105
+ - Filecheck: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-filecheck`](../scorecard-backend-module-filecheck/README.md)
104
106
 
105
107
  ### Disabling Metrics
106
108
 
@@ -116,7 +118,7 @@ Thresholds define conditions to assign metric values to specific visual categori
116
118
 
117
119
  Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`).
118
120
 
119
- For comprehensive threshold configuration guide, examples, and best practices, see [thresholds.md](./docs/thresholds.md).
121
+ For comprehensive threshold configuration guide, examples, best practices, and **aggregation KPI result thresholds** for **`type: average`**, see [thresholds.md](./docs/thresholds.md).
120
122
 
121
123
  ## Aggregation KPIs (homepage and `GET /aggregations`)
122
124
 
@@ -135,18 +137,41 @@ scorecard:
135
137
  description: 'Open pull requests grouped by status.'
136
138
  type: statusGrouped
137
139
  metricId: github.open_prs
140
+ openPrsWeightedKpi:
141
+ title: 'GitHub open PRs (weighted health)'
142
+ description: 'Weighted health from status counts using configurable scores.'
143
+ type: average
144
+ metricId: github.open_prs
145
+ options:
146
+ statusScores:
147
+ success: 100
148
+ warning: 50
149
+ error: 0
150
+ # Optional: colors for the average-score donut (expressions apply to percentage 0–100)
151
+ thresholds:
152
+ rules:
153
+ - key: success
154
+ expression: '>=75'
155
+ color: success.main
156
+ - key: warning
157
+ expression: '10-74'
158
+ color: warning.main
159
+ - key: error
160
+ expression: '<10'
161
+ color: error.main
138
162
  ```
139
163
 
140
- | Field | Description |
141
- | ------------- | -------------------------------------------------------------- |
142
- | `title` | Display title for this aggregation (returned in API metadata). |
143
- | `description` | Display description for this aggregation. |
144
- | `type` | Aggregation algorithm; currently `statusGrouped`. |
145
- | `metricId` | Metric provider id used to load thresholds and compute counts. |
164
+ | Field | Description |
165
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
+ | `title` | Display title for this aggregation (returned in API metadata). |
167
+ | `description` | Display description for this aggregation. |
168
+ | `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `average` (normalized weighted score). |
169
+ | `metricId` | Metric provider id used to load thresholds and compute counts. |
170
+ | `options` | Optional for `statusGrouped`. **Required** for `average`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-average-type)); evaluated on **`averageScore × 100`**; first match sets **`aggregationChartDisplayColor`**. The API includes **`averageScore`** (ratio **0–1**, **one decimal**), **`averageWeightedSum`**, **`averageMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). |
146
171
 
147
172
  - **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
148
173
  - If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
149
- - Invalid KPI entries may be logged and skipped at runtime; prefer fixing config per backend validation.
174
+ - **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`average`** KPIs without **`options.statusScores`**, bad expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation).
150
175
 
151
176
  **Homepage cards** are configured in the app (for example Dynamic Home Page mount points). They should pass **`aggregationId`** matching a key in `aggregationKPIs` or the metric id for the default case. See the [Scorecard frontend plugin README](../scorecard/README.md#homepage-scorecard-cards).
152
177
 
package/config.d.ts CHANGED
@@ -15,7 +15,11 @@
15
15
  */
16
16
 
17
17
  import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
18
- import { AggregationType } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
18
+ import {
19
+ AggregationType,
20
+ ThresholdConfig,
21
+ AggregationThresholdRule,
22
+ } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
19
23
 
20
24
  export interface Config {
21
25
  /** Configuration for scorecard plugin */
@@ -32,6 +36,20 @@ export interface Config {
32
36
  type: AggregationType;
33
37
  /** Metric ID for which the aggregation is calculated */
34
38
  metricId: string;
39
+ /** Type-specific settings */
40
+ options?: {
41
+ /** Required under `options` when `type` is `average` */
42
+ statusScores?: {
43
+ [thresholdRuleKey: string]: number;
44
+ };
45
+ /**
46
+ * Optional: threshold rules for coloring the KPI headline value from the aggregation result
47
+ * (e.g. average percentage 0–100 for `average` KPIs).
48
+ */
49
+ thresholds?: {
50
+ rules: AggregationThresholdRule[];
51
+ };
52
+ };
35
53
  };
36
54
  };
37
55
  /** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
@@ -57,47 +75,7 @@ export interface Config {
57
75
  */
58
76
  [metricName: string]: {
59
77
  /** Threshold configuration for the metric */
60
- thresholds?: {
61
- /**
62
- * Rules describe how metric values are categorized and how that category is presented in the UI.
63
- * They are evaluated in order and the first matching rule is applied.
64
- */
65
- rules?: Array<{
66
- /**
67
- * Threshold category key that a metric value is assigned to when this rule
68
- * matches (for example `success`, `warning`, `error`, or a custom key).
69
- */
70
- key: string;
71
- /**
72
- * Threshold expression that determines whether a metric value matches this
73
- * rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
74
- *
75
- * @example `<= 10` - Metric value must be less than or equal to 10.
76
- * @example `10-60` - Metric value must be between 10 and 60 (inclusive).
77
- */
78
- expression: string;
79
- /**
80
- * Color configuration - supports multiple formats:
81
- * - theme palette reference (`success.main` / `warning.main` / `error.main`)
82
- * - HEX code (e.g. '#FFA500')
83
- * - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
84
- *
85
- * Threshold rules 'success', 'warning' and 'error' have default colors.
86
- */
87
- color?: string;
88
- /**
89
- * Icon configuration - supports multiple formats:
90
- * - Backstage system icons: 'kind:component', 'kind:api', etc.
91
- * - Material Design icons: 'settings', 'home', 'build', etc.
92
- * - SVG strings: '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
93
- * - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
94
- * - Data URIs: 'data:image/svg+xml;base64,...'
95
- *
96
- * Threshold rules 'success', 'warning' and 'error' have default icons.
97
- */
98
- icon?: string;
99
- }>;
100
- };
78
+ thresholds?: ThresholdConfig;
101
79
  /**
102
80
  * Schedule for collecting this metric. If not set, the default hourly schedule is used.
103
81
  *
@@ -1,6 +1,28 @@
1
1
  'use strict';
2
2
 
3
+ var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
4
+
3
5
  const AGGREGATION_KPIS_CONFIG_PATH = "scorecard.aggregationKPIs";
6
+ const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS = {
7
+ rules: [
8
+ {
9
+ key: "success",
10
+ expression: ">=80",
11
+ color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.SUCCESS
12
+ },
13
+ {
14
+ key: "warning",
15
+ expression: "30-79",
16
+ color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.WARNING
17
+ },
18
+ {
19
+ key: "error",
20
+ expression: "<30",
21
+ color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.ERROR
22
+ }
23
+ ]
24
+ };
4
25
 
5
26
  exports.AGGREGATION_KPIS_CONFIG_PATH = AGGREGATION_KPIS_CONFIG_PATH;
27
+ exports.DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS = DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;
6
28
  //# sourceMappingURL=aggregationKPIs.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n"],"names":[],"mappings":";;AAgBO,MAAM,4BACX,GAAA;;;;"}
1
+ {"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ThresholdConfig,\n ScorecardThresholdRuleColors,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n\n/**\n * Default applied by `AverageAggregationStrategy` when `options.thresholds` is omitted\n * from app-config. Higher headline percentage (0–100) = better. Evaluated in order; first match wins.\n */\nexport const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS: ThresholdConfig = {\n rules: [\n {\n key: 'success',\n expression: '>=80',\n color: ScorecardThresholdRuleColors.SUCCESS,\n },\n {\n key: 'warning',\n expression: '30-79',\n color: ScorecardThresholdRuleColors.WARNING,\n },\n {\n key: 'error',\n expression: '<30',\n color: ScorecardThresholdRuleColors.ERROR,\n },\n ],\n};\n"],"names":["ScorecardThresholdRuleColors"],"mappings":";;;;AAqBO,MAAM,4BACX,GAAA;AAMK,MAAM,qCAAyD,GAAA;AAAA,EACpE,KAAO,EAAA;AAAA,IACL;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,MAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,OAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,OAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA;AACtC;AAEJ;;;;;"}
@@ -13,6 +13,7 @@ var migration = require('./database/migration.cjs.js');
13
13
  var DatabaseMetricValues = require('./database/DatabaseMetricValues.cjs.js');
14
14
  var index = require('./scheduler/index.cjs.js');
15
15
  var validateAggregationConfig = require('./validation/validateAggregationConfig.cjs.js');
16
+ var AggregationService = require('./service/aggregations/AggregationService.cjs.js');
16
17
 
17
18
  const scorecardPlugin = backendPluginApi.createBackendPlugin({
18
19
  pluginId: "scorecard",
@@ -66,8 +67,12 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
66
67
  auth,
67
68
  registry: metricProvidersRegistry,
68
69
  database: dbMetricValues,
69
- logger,
70
- config
70
+ logger
71
+ });
72
+ const aggregationsService = new AggregationService.AggregationsService({
73
+ config,
74
+ database: dbMetricValues,
75
+ logger
71
76
  });
72
77
  validateAggregationConfig.validateAggregationConfig({
73
78
  rootConfig: config,
@@ -83,10 +88,14 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
83
88
  metricProvidersRegistry,
84
89
  thresholdEvaluator: new ThresholdEvaluator.ThresholdEvaluator()
85
90
  }).start();
91
+ const service = {
92
+ aggregationsService,
93
+ catalogMetricService
94
+ };
86
95
  httpRouter.use(
87
96
  await router.createRouter({
88
97
  metricProvidersRegistry,
89
- catalogMetricService,
98
+ service,
90
99
  catalog,
91
100
  httpAuth,
92
101
  permissions,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\nimport { migrate } from './database/migration';\nimport { DatabaseMetricValues } from './database/DatabaseMetricValues';\nimport { Scheduler } from './scheduler';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n config: coreServices.rootConfig,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n scheduler: coreServices.scheduler,\n },\n async init({\n auth,\n catalog,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n // Run database migrations\n await migrate(database);\n\n const client = await database.getClient();\n const dbMetricValues = new DatabaseMetricValues(client);\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n config,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n }).start();\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n logger,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardMetricsExtensionPoint","coreServices","catalogServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","migrate","DatabaseMetricValues","CatalogMetricService","validateAggregationConfig","Scheduler","ThresholdEvaluator","createRouter"],"mappings":";;;;;;;;;;;;;;;;AA2CO,MAAM,kBAAkBA,oCAAoB,CAAA;AAAA,EACjD,QAAU,EAAA,WAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,uBAAA,GAA0B,IAAIC,+CAAwB,EAAA;AAE5D,IAAA,GAAA,CAAI,uBAAuBC,2DAAgC,EAAA;AAAA,MACzD,qBAAqB,kBAAsC,EAAA;AACzD,QAAA,kBAAA,CAAmB,QAAQ,CAAkB,cAAA,KAAA;AAC3C,UAAA,uBAAA,CAAwB,SAAS,cAAc,CAAA;AAAA,SAChD,CAAA;AAAA;AACH,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,MAAMC,6BAAa,CAAA,IAAA;AAAA,QACnB,OAAS,EAAAC,mCAAA;AAAA,QACT,QAAQD,6BAAa,CAAA,UAAA;AAAA,QACrB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,QAAQA,6BAAa,CAAA,MAAA;AAAA,QACrB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,qBAAqBA,6BAAa,CAAA,mBAAA;AAAA,QAClC,WAAWA,6BAAa,CAAA;AAAA,OAC1B;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,IAAA;AAAA,QACA,OAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,MAAA;AAAA,QACA,WAAA;AAAA,QACA,mBAAA;AAAA,QACA;AAAA,OACC,EAAA;AACD,QAAA,mBAAA,CAAoB,eAAgB,CAAA;AAAA,UAClC,WAAa,EAAAE,0CAAA;AAAA,UACb,YAAA,EAAc,OAAO,YAA2B,KAAA;AAC9C,YAAO,OAAA,uBAAA,CAAwB,YAAY,YAAY,CAAA;AAAA,WACzD;AAAA,UACA,WAAa,EAAAC,mDAAA;AAAA,UACb,KAAA,EAAO,MAAO,CAAA,MAAA,CAAOC,WAAc;AAAA,SACpC,CAAA;AAGD,QAAA,MAAMC,kBAAQ,QAAQ,CAAA;AAEtB,QAAM,MAAA,MAAA,GAAS,MAAM,QAAA,CAAS,SAAU,EAAA;AACxC,QAAM,MAAA,cAAA,GAAiB,IAAIC,yCAAA,CAAqB,MAAM,CAAA;AAEtD,QAAM,MAAA,oBAAA,GAAuB,IAAIC,yCAAqB,CAAA;AAAA,UACpD,OAAA;AAAA,UACA,IAAA;AAAA,UACA,QAAU,EAAA,uBAAA;AAAA,UACV,QAAU,EAAA,cAAA;AAAA,UACV,MAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAA0BC,mDAAA,CAAA;AAAA,UACxB,UAAY,EAAA,MAAA;AAAA,UACZ,QAAU,EAAA;AAAA,SACX,CAAA;AAED,QAAAC,eAAA,CAAU,MAAO,CAAA;AAAA,UACf,IAAA;AAAA,UACA,OAAA;AAAA,UACA,MAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,QAAU,EAAA,cAAA;AAAA,UACV,uBAAA;AAAA,UACA,kBAAA,EAAoB,IAAIC,qCAAmB;AAAA,SAC5C,EAAE,KAAM,EAAA;AAET,QAAW,UAAA,CAAA,GAAA;AAAA,UACT,MAAMC,mBAAa,CAAA;AAAA,YACjB,uBAAA;AAAA,YACA,oBAAA;AAAA,YACA,OAAA;AAAA,YACA,QAAA;AAAA,YACA,WAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAAA;AACF,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\nimport { migrate } from './database/migration';\nimport { DatabaseMetricValues } from './database/DatabaseMetricValues';\nimport { Scheduler } from './scheduler';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\nimport { AggregationsService } from './service/aggregations/AggregationService';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n config: coreServices.rootConfig,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n scheduler: coreServices.scheduler,\n },\n async init({\n auth,\n catalog,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n // Run database migrations\n await migrate(database);\n\n const client = await database.getClient();\n const dbMetricValues = new DatabaseMetricValues(client);\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n });\n\n const aggregationsService = new AggregationsService({\n config,\n database: dbMetricValues,\n logger: logger,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n }).start();\n\n const service = {\n aggregationsService: aggregationsService,\n catalogMetricService: catalogMetricService,\n };\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n service,\n catalog,\n httpAuth,\n permissions,\n logger,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardMetricsExtensionPoint","coreServices","catalogServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","migrate","DatabaseMetricValues","CatalogMetricService","AggregationsService","validateAggregationConfig","Scheduler","ThresholdEvaluator","createRouter"],"mappings":"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"}
@@ -6,44 +6,51 @@ class MetricProvidersRegistry {
6
6
  metricProviders = /* @__PURE__ */ new Map();
7
7
  datasourceIndex = /* @__PURE__ */ new Map();
8
8
  register(metricProvider) {
9
- const providerId = metricProvider.getProviderId();
10
9
  const providerDatasource = metricProvider.getProviderDatasourceId();
11
- const metric = metricProvider.getMetric();
12
10
  const metricType = metricProvider.getMetricType();
13
- if (providerId !== metric.id) {
14
- throw new Error(
15
- `Invalid metric provider with ID ${providerId}, provider ID must match metric ID '${metric.id}'`
16
- );
17
- }
18
- if (metricType !== metric.type) {
19
- throw new Error(
20
- `Invalid metric provider with ID ${providerId}, getMetricType() must match getMetric().type. Expected '${metricType}', but got '${metric.type}'`
21
- );
22
- }
23
- const expectedPrefix = `${providerDatasource}.`;
24
- if (!providerId.startsWith(expectedPrefix) || providerId === expectedPrefix) {
25
- throw new Error(
26
- `Invalid metric provider with ID ${providerId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`
27
- );
28
- }
29
- if (this.metricProviders.has(providerId)) {
30
- throw new errors.ConflictError(
31
- `Metric provider with ID '${providerId}' has already been registered`
32
- );
33
- }
34
- this.metricProviders.set(providerId, metricProvider);
35
- let datasourceProviders = this.datasourceIndex.get(providerDatasource);
36
- if (!datasourceProviders) {
37
- datasourceProviders = /* @__PURE__ */ new Set();
38
- this.datasourceIndex.set(providerDatasource, datasourceProviders);
11
+ const metricIds = metricProvider.getMetricIds?.() ?? [
12
+ metricProvider.getProviderId()
13
+ ];
14
+ const metrics = metricProvider.getMetrics?.() ?? [
15
+ metricProvider.getMetric()
16
+ ];
17
+ for (const metricId of metricIds) {
18
+ const metric = metrics.find((m) => m.id === metricId);
19
+ if (!metric) {
20
+ throw new Error(
21
+ `Invalid metric provider: metric ID '${metricId}' returned by getMetricIds() does not have a corresponding metric in getMetrics()`
22
+ );
23
+ }
24
+ if (metricType !== metric.type) {
25
+ throw new Error(
26
+ `Invalid metric provider with ID ${metricId}, getMetricType() must match getMetric().type. Expected '${metricType}', but got '${metric.type}'`
27
+ );
28
+ }
29
+ const expectedPrefix = `${providerDatasource}.`;
30
+ if (!metricId.startsWith(expectedPrefix) || metricId === expectedPrefix) {
31
+ throw new Error(
32
+ `Invalid metric provider with ID ${metricId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`
33
+ );
34
+ }
35
+ if (this.metricProviders.has(metricId)) {
36
+ throw new errors.ConflictError(
37
+ `Metric provider with ID '${metricId}' has already been registered`
38
+ );
39
+ }
40
+ this.metricProviders.set(metricId, metricProvider);
41
+ let datasourceProviders = this.datasourceIndex.get(providerDatasource);
42
+ if (!datasourceProviders) {
43
+ datasourceProviders = /* @__PURE__ */ new Set();
44
+ this.datasourceIndex.set(providerDatasource, datasourceProviders);
45
+ }
46
+ datasourceProviders.add(metricId);
39
47
  }
40
- datasourceProviders.add(providerId);
41
48
  }
42
- getProvider(providerId) {
43
- const metricProvider = this.metricProviders.get(providerId);
49
+ getProvider(metricId) {
50
+ const metricProvider = this.metricProviders.get(metricId);
44
51
  if (!metricProvider) {
45
52
  throw new errors.NotFoundError(
46
- `Metric provider with ID '${providerId}' is not registered.`
53
+ `No metric provider registered for metric ID '${metricId}'.`
47
54
  );
48
55
  }
49
56
  return metricProvider;
@@ -51,33 +58,49 @@ class MetricProvidersRegistry {
51
58
  hasProvider(providerId) {
52
59
  return this.metricProviders.has(providerId);
53
60
  }
54
- getMetric(providerId) {
55
- return this.getProvider(providerId).getMetric();
61
+ getMetric(metricId) {
62
+ const provider = this.getProvider(metricId);
63
+ if (provider.getMetrics) {
64
+ const metrics = provider.getMetrics();
65
+ const metric = metrics.find((m) => m.id === metricId);
66
+ if (metric) {
67
+ return metric;
68
+ }
69
+ }
70
+ return provider.getMetric();
56
71
  }
57
- async calculateMetric(providerId, entity) {
58
- return this.getProvider(providerId).calculateMetric(entity);
72
+ async calculateMetric(metricId, entity) {
73
+ return this.getProvider(metricId).calculateMetric(entity);
59
74
  }
60
- async calculateMetrics(providerIds, entity) {
75
+ async calculateMetrics(metricIds, entity) {
61
76
  const results = await Promise.allSettled(
62
- providerIds.map((providerId) => this.calculateMetric(providerId, entity))
77
+ metricIds.map((metricId) => this.calculateMetric(metricId, entity))
63
78
  );
64
79
  return results.map((result, index) => {
65
- const providerId = providerIds[index];
80
+ const metricId = metricIds[index];
66
81
  if (result.status === "fulfilled") {
67
- return { providerId, value: result.value };
82
+ return { metricId, value: result.value };
68
83
  }
69
- return { providerId, error: result.reason };
84
+ return { metricId, error: result.reason };
70
85
  });
71
86
  }
72
87
  listProviders() {
73
- return Array.from(this.metricProviders.values());
88
+ return [...new Set(this.metricProviders.values())];
74
89
  }
75
- listMetrics(providerIds) {
76
- if (providerIds && providerIds.length !== 0) {
77
- return providerIds.map((providerId) => this.metricProviders.get(providerId)?.getMetric()).filter((m) => m !== void 0);
90
+ listMetrics(metricIds) {
91
+ if (metricIds && metricIds.length !== 0) {
92
+ return metricIds.map((metricId) => {
93
+ const provider = this.metricProviders.get(metricId);
94
+ if (!provider) return void 0;
95
+ if (provider.getMetrics) {
96
+ const metrics = provider.getMetrics();
97
+ return metrics.find((m) => m.id === metricId);
98
+ }
99
+ return provider.getMetric();
100
+ }).filter((m) => m !== void 0);
78
101
  }
79
- return [...this.metricProviders.values()].map(
80
- (provider) => provider.getMetric()
102
+ return this.listProviders().flatMap(
103
+ (provider) => provider.getMetrics?.() ?? [provider.getMetric()]
81
104
  );
82
105
  }
83
106
  listMetricsByDatasource(datasourceId) {
@@ -85,7 +108,10 @@ class MetricProvidersRegistry {
85
108
  if (!providerIdsOfDatasource) {
86
109
  return [];
87
110
  }
88
- return Array.from(providerIdsOfDatasource).map((providerId) => this.metricProviders.get(providerId)).filter((provider) => provider !== void 0).map((provider) => provider.getMetric());
111
+ const providers = [...providerIdsOfDatasource].map((id) => this.metricProviders.get(id)).filter((p) => p !== void 0);
112
+ return [...new Set(providers)].flatMap(
113
+ (provider) => provider.getMetrics?.() ?? [provider.getMetric()]
114
+ );
89
115
  }
90
116
  }
91
117
 
@@ -1 +1 @@
1
- {"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerId = metricProvider.getProviderId();\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metric = metricProvider.getMetric();\n const metricType = metricProvider.getMetricType();\n\n if (providerId !== metric.id) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, provider ID must match metric ID '${metric.id}'`,\n );\n }\n\n if (metricType !== metric.type) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, getMetricType() must match getMetric().type. Expected '${metricType}', but got '${metric.type}'`,\n );\n }\n\n const expectedPrefix = `${providerDatasource}.`;\n if (\n !providerId.startsWith(expectedPrefix) ||\n providerId === expectedPrefix\n ) {\n throw new Error(\n `Invalid metric provider with ID ${providerId}, must have format '${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(providerId)) {\n throw new ConflictError(\n `Metric provider with ID '${providerId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(providerId, metricProvider);\n\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(providerId);\n }\n\n getProvider(providerId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(providerId);\n if (!metricProvider) {\n throw new NotFoundError(\n `Metric provider with ID '${providerId}' is not registered.`,\n );\n }\n return metricProvider;\n }\n\n hasProvider(providerId: string): boolean {\n return this.metricProviders.has(providerId);\n }\n\n getMetric(providerId: string): Metric {\n return this.getProvider(providerId).getMetric();\n }\n\n async calculateMetric(\n providerId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(providerId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n providerIds: string[],\n entity: Entity,\n ): Promise<{ providerId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n providerIds.map(providerId => this.calculateMetric(providerId, entity)),\n );\n\n return results.map((result, index) => {\n const providerId = providerIds[index];\n if (result.status === 'fulfilled') {\n return { providerId, value: result.value };\n }\n return { providerId, error: result.reason as Error };\n });\n }\n\n listProviders(): MetricProvider[] {\n return Array.from(this.metricProviders.values());\n }\n\n listMetrics(providerIds?: string[]): Metric[] {\n if (providerIds && providerIds.length !== 0) {\n return providerIds\n .map(providerId => this.metricProviders.get(providerId)?.getMetric())\n .filter((m): m is Metric => m !== undefined);\n }\n return [...this.metricProviders.values()].map(provider =>\n provider.getMetric(),\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n return Array.from(providerIdsOfDatasource)\n .map(providerId => this.metricProviders.get(providerId))\n .filter((provider): provider is MetricProvider => provider !== undefined)\n .map(provider => provider.getMetric());\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}
1
+ {"version":3,"file":"MetricProvidersRegistry.cjs.js","sources":["../../src/providers/MetricProvidersRegistry.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n private readonly metricProviders = new Map<string, MetricProvider>();\n private readonly datasourceIndex = new Map<string, Set<string>>();\n\n register(metricProvider: MetricProvider): void {\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const metricType = metricProvider.getMetricType();\n\n // Support both single and batch providers\n const metricIds = metricProvider.getMetricIds?.() ?? [\n metricProvider.getProviderId(),\n ];\n const metrics = metricProvider.getMetrics?.() ?? [\n metricProvider.getMetric(),\n ];\n\n // Validate: Each metric ID must have a corresponding metric definition\n for (const metricId of metricIds) {\n const metric = metrics.find(m => m.id === metricId);\n if (!metric) {\n throw new Error(\n `Invalid metric provider: metric ID '${metricId}' returned by getMetricIds() ` +\n `does not have a corresponding metric in getMetrics()`,\n );\n }\n\n if (metricType !== metric.type) {\n throw new Error(\n `Invalid metric provider with ID ${metricId}, getMetricType() must match ` +\n `getMetric().type. Expected '${metricType}', but got '${metric.type}'`,\n );\n }\n\n // Validate: Provider ID format (datasource.metric_name)\n const expectedPrefix = `${providerDatasource}.`;\n if (!metricId.startsWith(expectedPrefix) || metricId === expectedPrefix) {\n throw new Error(\n `Invalid metric provider with ID ${metricId}, must have format ` +\n `'${providerDatasource}.<metric_name>' where metric name is not empty`,\n );\n }\n\n if (this.metricProviders.has(metricId)) {\n throw new ConflictError(\n `Metric provider with ID '${metricId}' has already been registered`,\n );\n }\n\n this.metricProviders.set(metricId, metricProvider);\n\n // Index by datasource\n let datasourceProviders = this.datasourceIndex.get(providerDatasource);\n if (!datasourceProviders) {\n datasourceProviders = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceProviders);\n }\n datasourceProviders.add(metricId);\n }\n }\n\n getProvider(metricId: string): MetricProvider {\n const metricProvider = this.metricProviders.get(metricId);\n if (!metricProvider) {\n throw new NotFoundError(\n `No metric provider registered for metric ID '${metricId}'.`,\n );\n }\n return metricProvider;\n }\n\n hasProvider(providerId: string): boolean {\n return this.metricProviders.has(providerId);\n }\n\n getMetric(metricId: string): Metric {\n const provider = this.getProvider(metricId);\n\n // For batch providers, find the specific metric by ID\n if (provider.getMetrics) {\n const metrics = provider.getMetrics();\n const metric = metrics.find(m => m.id === metricId);\n if (metric) {\n return metric;\n }\n }\n\n return provider.getMetric();\n }\n\n async calculateMetric(\n metricId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n return this.getProvider(metricId).calculateMetric(entity);\n }\n\n async calculateMetrics(\n metricIds: string[],\n entity: Entity,\n ): Promise<{ metricId: string; value?: MetricValue; error?: Error }[]> {\n const results = await Promise.allSettled(\n metricIds.map(metricId => this.calculateMetric(metricId, entity)),\n );\n\n return results.map((result, index) => {\n const metricId = metricIds[index];\n if (result.status === 'fulfilled') {\n return { metricId, value: result.value };\n }\n return { metricId, error: result.reason as Error };\n });\n }\n\n listProviders(): MetricProvider[] {\n // Deduplicate providers since batch providers are stored under multiple metric IDs\n return [...new Set(this.metricProviders.values())];\n }\n\n listMetrics(metricIds?: string[]): Metric[] {\n if (metricIds && metricIds.length !== 0) {\n return metricIds\n .map(metricId => {\n const provider = this.metricProviders.get(metricId);\n if (!provider) return undefined;\n\n if (provider.getMetrics) {\n const metrics = provider.getMetrics();\n return metrics.find(m => m.id === metricId);\n }\n\n return provider.getMetric();\n })\n .filter((m): m is Metric => m !== undefined);\n }\n\n // List all metrics from all providers (deduplicate batch providers)\n return this.listProviders().flatMap(\n provider => provider.getMetrics?.() ?? [provider.getMetric()],\n );\n }\n\n listMetricsByDatasource(datasourceId: string): Metric[] {\n const providerIdsOfDatasource = this.datasourceIndex.get(datasourceId);\n\n if (!providerIdsOfDatasource) {\n return [];\n }\n\n // Get unique providers for this datasource, then get their metrics\n const providers = [...providerIdsOfDatasource]\n .map(id => this.metricProviders.get(id))\n .filter((p): p is MetricProvider => p !== undefined);\n\n return [...new Set(providers)].flatMap(\n provider => provider.getMetrics?.() ?? [provider.getMetric()],\n );\n }\n}\n"],"names":["ConflictError","NotFoundError"],"mappings":"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"}