@red-hat-developer-hub/backstage-plugin-scorecard-backend 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8c14679: **BREAKING**: Scorecard provider configuration now lives under top-level `scorecard.metricProviders` instead of `scorecard.plugins`. Provider IDs must be `<datasource>.<providerName>` (no longer equal to the datasource alone). Entity annotations for thresholds use now the full metric ID instead of provider ID.
8
+
9
+ Thresholds from configuration are determined by the most specific setting (**metric > provider**):
10
+
11
+ 1. `metricProviders.<datasource>.<providerName>.metrics.<metricName>.thresholds`
12
+ 2. `metricProviders.<datasource>.<providerName>.thresholds`
13
+
14
+ Config keys are local names (no datasource prefix). Entity annotations use the full metric ID:
15
+ `scorecard.io/<metricId>.thresholds.rules.<key>`.
16
+
17
+ Filecheck provider ID is now `filecheck.fileExistence`; files move under `options`:
18
+
19
+ ```diff
20
+ scorecard:
21
+ - plugins:
22
+ - filecheck:
23
+ - files:
24
+ - license: LICENSE
25
+ - codeowners: CODEOWNERS
26
+ - thresholds: ...
27
+ - schedule: ...
28
+ + metricProviders:
29
+ + filecheck:
30
+ + fileExistence:
31
+ + options:
32
+ + files:
33
+ + license: LICENSE
34
+ + codeowners: CODEOWNERS
35
+ + thresholds: ...
36
+ + schedule: ...
37
+ ```
38
+
39
+ Migration from the previous `scorecard.plugins` layout:
40
+
41
+ ```diff
42
+ scorecard:
43
+ - plugins:
44
+ + metricProviders:
45
+ github:
46
+ openPRs:
47
+ schedule: ...
48
+ thresholds: ...
49
+ ```
50
+
51
+ ### Patch Changes
52
+
53
+ - Updated dependencies [8c14679]
54
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@4.0.0
55
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@4.0.0
56
+
3
57
  ## 3.0.1
4
58
 
5
59
  ### Patch Changes
package/config.d.ts CHANGED
@@ -66,29 +66,36 @@ export interface Config {
66
66
  except?: string[];
67
67
  };
68
68
  };
69
- /** Configuration for scorecard metric providers */
70
- plugins?: {
71
- /** Configuration for datasource */
69
+ /** Metric providers calculate one or more metrics on a schedule. */
70
+ metricProviders?: {
71
+ /** Datasource ID, matches `getProviderDatasourceId()` of a provider (e.g., `jira`, `github`, `filecheck`). */
72
72
  [datasource: string]: {
73
- /** Configuration for metric providers within the datasource.
74
- * Each key corresponds to the metric name part of the provider ID (datasource.metricName).
73
+ /** Configuration for a specific metric provider.
74
+ * Use the local name without datasource prefix (e.g., `openPRs` instead of `github.openPRs`).
75
75
  */
76
- [metricName: string]: {
77
- /** Threshold configuration for the metric */
78
- thresholds?: ThresholdConfig;
76
+ [providerName: string]: {
77
+ /** How often metrics will be calculated for this provider. */
78
+ schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
79
79
  /**
80
- * Schedule for collecting this metric. If not set, the default hourly schedule is used.
81
- *
82
- * Default schedule:
83
- * ```ts
84
- * {
85
- * frequency: { hours: 1 },
86
- * timeout: { minutes: 15 },
87
- * initialDelay: { minutes: 1 },
88
- * }
89
- * ```
80
+ * How metric values are categorized for all metrics of this provider.
81
+ * Overridden by metric-level thresholds when set.
90
82
  */
91
- schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
83
+ thresholds?: ThresholdConfig;
84
+ /** Per-metric configuration. */
85
+ metrics?: {
86
+ /** Configuration for a specific metric.
87
+ * Use the local name without datasource prefix (e.g., 'openPRs' instead of 'github.openPRs').
88
+ */
89
+ [metricName: string]: {
90
+ /**
91
+ * How metric values are categorized for this metric.
92
+ * Overrides provider-level thresholds.
93
+ */
94
+ thresholds?: ThresholdConfig;
95
+ };
96
+ };
97
+ /** Provider-specific options (shape defined by each module). */
98
+ options?: unknown;
92
99
  };
93
100
  };
94
101
  };
@@ -2,26 +2,32 @@
2
2
 
3
3
  var errors = require('@backstage/errors');
4
4
  var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plugin-scorecard-node');
5
+ var validateMetricProviderIds = require('../validation/validateMetricProviderIds.cjs.js');
5
6
 
6
7
  class MetricProvidersRegistry {
8
+ /** metricId → provider (a multi-metric provider is stored under each of its metric IDs) */
7
9
  metricProviders = /* @__PURE__ */ new Map();
10
+ /** datasourceId → set of metricIds for that datasource */
8
11
  datasourceIndex = /* @__PURE__ */ new Map();
12
+ /** Registered provider IDs (unique; used for scheduler task / config keys) */
13
+ registeredProviderIds = /* @__PURE__ */ new Set();
9
14
  register(metricProvider) {
10
15
  const providerDatasource = metricProvider.getProviderDatasourceId();
11
16
  const providerId = metricProvider.getProviderId();
17
+ validateMetricProviderIds.validateProviderId(providerId, providerDatasource);
18
+ if (this.registeredProviderIds.has(providerId)) {
19
+ throw new errors.ConflictError(
20
+ `Metric provider with ID '${providerId}' has already been registered`
21
+ );
22
+ }
12
23
  const metrics = metricProvider.getMetrics();
13
24
  const metricIds = metrics.map((m) => m.id);
14
25
  for (const metric of metrics) {
15
26
  const metricId = metric.id;
16
- const expectedPrefix = `${providerDatasource}.`;
17
- if (!metricId.startsWith(expectedPrefix) || metricId === expectedPrefix) {
18
- throw new Error(
19
- `Invalid metric provider with ID ${metricId}, must have format '${providerDatasource}.<metricName>' where metric name is not empty`
20
- );
21
- }
27
+ validateMetricProviderIds.validateMetricId(metricId, providerDatasource);
22
28
  if (this.metricProviders.has(metricId)) {
23
29
  throw new errors.ConflictError(
24
- `Metric provider with ID '${metricId}' has already been registered`
30
+ `Metric with ID '${metricId}' has already been registered`
25
31
  );
26
32
  }
27
33
  try {
@@ -33,14 +39,15 @@ class MetricProvidersRegistry {
33
39
  );
34
40
  }
35
41
  }
42
+ this.registeredProviderIds.add(providerId);
36
43
  for (const metricId of metricIds) {
37
44
  this.metricProviders.set(metricId, metricProvider);
38
- let datasourceProviders = this.datasourceIndex.get(providerDatasource);
39
- if (!datasourceProviders) {
40
- datasourceProviders = /* @__PURE__ */ new Set();
41
- this.datasourceIndex.set(providerDatasource, datasourceProviders);
45
+ let datasourceMetricIds = this.datasourceIndex.get(providerDatasource);
46
+ if (!datasourceMetricIds) {
47
+ datasourceMetricIds = /* @__PURE__ */ new Set();
48
+ this.datasourceIndex.set(providerDatasource, datasourceMetricIds);
42
49
  }
43
- datasourceProviders.add(metricId);
50
+ datasourceMetricIds.add(metricId);
44
51
  }
45
52
  }
46
53
  getProvider(metricId) {
@@ -66,29 +73,6 @@ class MetricProvidersRegistry {
66
73
  `Metric '${metricId}' not found in provider '${provider.getProviderId()}'`
67
74
  );
68
75
  }
69
- async calculateMetric(metricId, entity) {
70
- const provider = this.getProvider(metricId);
71
- const results = await provider.calculateMetrics(entity);
72
- const value = results.get(metricId);
73
- if (value === void 0) {
74
- throw new Error(
75
- `Provider '${provider.getProviderId()}' did not return a value for metric '${metricId}'`
76
- );
77
- }
78
- return value;
79
- }
80
- async calculateMetrics(metricIds, entity) {
81
- const results = await Promise.allSettled(
82
- metricIds.map((metricId) => this.calculateMetric(metricId, entity))
83
- );
84
- return results.map((result, index) => {
85
- const metricId = metricIds[index];
86
- if (result.status === "fulfilled") {
87
- return { metricId, value: result.value };
88
- }
89
- return { metricId, error: result.reason };
90
- });
91
- }
92
76
  listProviders() {
93
77
  return [...new Set(this.metricProviders.values())];
94
78
  }
@@ -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 type { Entity } from '@backstage/catalog-model';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n MetricProvider,\n ThresholdConfigFormatError,\n validateThresholdsForMetric,\n} 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 providerId = metricProvider.getProviderId();\n\n const metrics = metricProvider.getMetrics();\n const metricIds = metrics.map(m => m.id);\n\n for (const metric of metrics) {\n const metricId = metric.id;\n\n // Validate: Provider ID format (datasource.metricName)\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}.<metricName>' 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 try {\n validateThresholdsForMetric(metric.thresholds, metric.type);\n } catch (error) {\n throw new ThresholdConfigFormatError(\n `Invalid default thresholds for metric provider '${providerId}', metric '${metricId}'`,\n error,\n );\n }\n }\n\n for (const metricId of metricIds) {\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 const metrics = provider.getMetrics();\n const metric = metrics.find(m => m.id === metricId);\n if (metric) {\n return metric;\n }\n\n throw new NotFoundError(\n `Metric '${metricId}' not found in provider '${provider.getProviderId()}'`,\n );\n }\n\n async calculateMetric(\n metricId: string,\n entity: Entity,\n ): Promise<MetricValue> {\n const provider = this.getProvider(metricId);\n const results = await provider.calculateMetrics(entity);\n const value = results.get(metricId);\n if (value === undefined) {\n throw new Error(\n `Provider '${provider.getProviderId()}' did not return a value for metric '${metricId}'`,\n );\n }\n return value;\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 const metrics = provider.getMetrics();\n return metrics.find(m => m.id === metricId);\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(provider => provider.getMetrics());\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(provider => provider.getMetrics());\n }\n}\n"],"names":["ConflictError","validateThresholdsForMetric","ThresholdConfigFormatError","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 { Metric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n MetricProvider,\n ThresholdConfigFormatError,\n validateThresholdsForMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport {\n validateMetricId,\n validateProviderId,\n} from '../validation/validateMetricProviderIds';\n\n/**\n * Registry of all registered metric providers.\n */\nexport class MetricProvidersRegistry {\n /** metricId → provider (a multi-metric provider is stored under each of its metric IDs) */\n private readonly metricProviders = new Map<string, MetricProvider>();\n /** datasourceId → set of metricIds for that datasource */\n private readonly datasourceIndex = new Map<string, Set<string>>();\n /** Registered provider IDs (unique; used for scheduler task / config keys) */\n private readonly registeredProviderIds = new Set<string>();\n\n register(metricProvider: MetricProvider): void {\n const providerDatasource = metricProvider.getProviderDatasourceId();\n const providerId = metricProvider.getProviderId();\n\n validateProviderId(providerId, providerDatasource);\n\n if (this.registeredProviderIds.has(providerId)) {\n throw new ConflictError(\n `Metric provider with ID '${providerId}' has already been registered`,\n );\n }\n\n const metrics = metricProvider.getMetrics();\n const metricIds = metrics.map(m => m.id);\n\n for (const metric of metrics) {\n const metricId = metric.id;\n\n validateMetricId(metricId, providerDatasource);\n\n if (this.metricProviders.has(metricId)) {\n throw new ConflictError(\n `Metric with ID '${metricId}' has already been registered`,\n );\n }\n\n try {\n validateThresholdsForMetric(metric.thresholds, metric.type);\n } catch (error) {\n throw new ThresholdConfigFormatError(\n `Invalid default thresholds for metric provider '${providerId}', metric '${metricId}'`,\n error,\n );\n }\n }\n\n this.registeredProviderIds.add(providerId);\n\n for (const metricId of metricIds) {\n this.metricProviders.set(metricId, metricProvider);\n\n // Index by datasource\n let datasourceMetricIds = this.datasourceIndex.get(providerDatasource);\n if (!datasourceMetricIds) {\n datasourceMetricIds = new Set();\n this.datasourceIndex.set(providerDatasource, datasourceMetricIds);\n }\n datasourceMetricIds.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 const metrics = provider.getMetrics();\n const metric = metrics.find(m => m.id === metricId);\n if (metric) {\n return metric;\n }\n\n throw new NotFoundError(\n `Metric '${metricId}' not found in provider '${provider.getProviderId()}'`,\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 const metrics = provider.getMetrics();\n return metrics.find(m => m.id === metricId);\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(provider => provider.getMetrics());\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(provider => provider.getMetrics());\n }\n}\n"],"names":["validateProviderId","ConflictError","validateMetricId","validateThresholdsForMetric","ThresholdConfigFormatError","NotFoundError"],"mappings":";;;;;;AA+BO,MAAM,uBAAA,CAAwB;AAAA;AAAA,EAElB,eAAA,uBAAsB,GAAA,EAA4B;AAAA;AAAA,EAElD,eAAA,uBAAsB,GAAA,EAAyB;AAAA;AAAA,EAE/C,qBAAA,uBAA4B,GAAA,EAAY;AAAA,EAEzD,SAAS,cAAA,EAAsC;AAC7C,IAAA,MAAM,kBAAA,GAAqB,eAAe,uBAAA,EAAwB;AAClE,IAAA,MAAM,UAAA,GAAa,eAAe,aAAA,EAAc;AAEhD,IAAAA,4CAAA,CAAmB,YAAY,kBAAkB,CAAA;AAEjD,IAAA,IAAI,IAAA,CAAK,qBAAA,CAAsB,GAAA,CAAI,UAAU,CAAA,EAAG;AAC9C,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,4BAA4B,UAAU,CAAA,6BAAA;AAAA,OACxC;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,eAAe,UAAA,EAAW;AAC1C,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,EAAE,CAAA;AAEvC,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,MAAM,WAAW,MAAA,CAAO,EAAA;AAExB,MAAAC,0CAAA,CAAiB,UAAU,kBAAkB,CAAA;AAE7C,MAAA,IAAI,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA,EAAG;AACtC,QAAA,MAAM,IAAID,oBAAA;AAAA,UACR,mBAAmB,QAAQ,CAAA,6BAAA;AAAA,SAC7B;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAAE,wDAAA,CAA4B,MAAA,CAAO,UAAA,EAAY,MAAA,CAAO,IAAI,CAAA;AAAA,MAC5D,SAAS,KAAA,EAAO;AACd,QAAA,MAAM,IAAIC,uDAAA;AAAA,UACR,CAAA,gDAAA,EAAmD,UAAU,CAAA,WAAA,EAAc,QAAQ,CAAA,CAAA,CAAA;AAAA,UACnF;AAAA,SACF;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,qBAAA,CAAsB,IAAI,UAAU,CAAA;AAEzC,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAA,EAAU,cAAc,CAAA;AAGjD,MAAA,IAAI,mBAAA,GAAsB,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,kBAAkB,CAAA;AACrE,MAAA,IAAI,CAAC,mBAAA,EAAqB;AACxB,QAAA,mBAAA,uBAA0B,GAAA,EAAI;AAC9B,QAAA,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,kBAAA,EAAoB,mBAAmB,CAAA;AAAA,MAClE;AACA,MAAA,mBAAA,CAAoB,IAAI,QAAQ,CAAA;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,YAAY,QAAA,EAAkC;AAC5C,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA;AACxD,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,gDAAgD,QAAQ,CAAA,EAAA;AAAA,OAC1D;AAAA,IACF;AACA,IAAA,OAAO,cAAA;AAAA,EACT;AAAA,EAEA,YAAY,UAAA,EAA6B;AACvC,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,UAAU,CAAA;AAAA,EAC5C;AAAA,EAEA,UAAU,QAAA,EAA0B;AAClC,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,WAAA,CAAY,QAAQ,CAAA;AAC1C,IAAA,MAAM,OAAA,GAAU,SAAS,UAAA,EAAW;AACpC,IAAA,MAAM,SAAS,OAAA,CAAQ,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,QAAQ,CAAA;AAClD,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,IAAIA,oBAAA;AAAA,MACR,CAAA,QAAA,EAAW,QAAQ,CAAA,yBAAA,EAA4B,QAAA,CAAS,eAAe,CAAA,CAAA;AAAA,KACzE;AAAA,EACF;AAAA,EAEA,aAAA,GAAkC;AAEhC,IAAA,OAAO,CAAC,GAAG,IAAI,GAAA,CAAI,KAAK,eAAA,CAAgB,MAAA,EAAQ,CAAC,CAAA;AAAA,EACnD;AAAA,EAEA,YAAY,SAAA,EAAgC;AAC1C,IAAA,IAAI,SAAA,IAAa,SAAA,CAAU,MAAA,KAAW,CAAA,EAAG;AACvC,MAAA,OAAO,SAAA,CACJ,IAAI,CAAA,QAAA,KAAY;AACf,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA;AAClD,QAAA,IAAI,CAAC,UAAU,OAAO,MAAA;AAEtB,QAAA,MAAM,OAAA,GAAU,SAAS,UAAA,EAAW;AACpC,QAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,QAAQ,CAAA;AAAA,MAC5C,CAAC,CAAA,CACA,MAAA,CAAO,CAAC,CAAA,KAAmB,MAAM,MAAS,CAAA;AAAA,IAC/C;AAGA,IAAA,OAAO,KAAK,aAAA,EAAc,CAAE,QAAQ,CAAA,QAAA,KAAY,QAAA,CAAS,YAAY,CAAA;AAAA,EACvE;AAAA,EAEA,wBAAwB,YAAA,EAAgC;AACtD,IAAA,MAAM,uBAAA,GAA0B,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,YAAY,CAAA;AAErE,IAAA,IAAI,CAAC,uBAAA,EAAyB;AAC5B,MAAA,OAAO,EAAC;AAAA,IACV;AAGA,IAAA,MAAM,YAAY,CAAC,GAAG,uBAAuB,CAAA,CAC1C,IAAI,CAAA,EAAA,KAAM,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,EAAE,CAAC,CAAA,CACtC,OAAO,CAAC,CAAA,KAA2B,MAAM,MAAS,CAAA;AAErD,IAAA,OAAO,CAAC,GAAG,IAAI,GAAA,CAAI,SAAS,CAAC,CAAA,CAAE,OAAA,CAAQ,CAAA,QAAA,KAAY,QAAA,CAAS,UAAA,EAAY,CAAA;AAAA,EAC1E;AACF;;;;"}
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var backendPluginApi = require('@backstage/backend-plugin-api');
4
3
  var metricUtils = require('../../utils/metricUtils.cjs.js');
5
4
  var node_crypto = require('node:crypto');
6
5
  var normalizeOwnerRef = require('../../utils/normalizeOwnerRef.cjs.js');
6
+ var metricProviderConfigKeys = require('../../utils/metricProviderConfigKeys.cjs.js');
7
7
  var catalogModel = require('@backstage/catalog-model');
8
8
 
9
9
  class PullMetricsByProviderTask {
@@ -36,8 +36,11 @@ class PullMetricsByProviderTask {
36
36
  this.thresholdResolver = options.thresholdResolver;
37
37
  }
38
38
  async start() {
39
- const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;
40
- const schedule = this.getScheduleFromConfig(scheduleConfigPath);
39
+ const schedule = metricProviderConfigKeys.resolveScheduleFromConfig(
40
+ this.config,
41
+ this.provider.getProviderDatasourceId(),
42
+ this.providerId
43
+ ) ?? PullMetricsByProviderTask.DEFAULT_SCHEDULE;
41
44
  const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);
42
45
  await taskRunner.run({
43
46
  id: this.providerId,
@@ -58,11 +61,6 @@ class PullMetricsByProviderTask {
58
61
  }
59
62
  });
60
63
  }
61
- getScheduleFromConfig(schedulePath) {
62
- return this.config.has(schedulePath) ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
63
- this.config.getConfig(schedulePath)
64
- ) : PullMetricsByProviderTask.DEFAULT_SCHEDULE;
65
- }
66
64
  async pullProviderMetrics(provider, logger) {
67
65
  logger.info(`Pulling metrics for ${this.providerId}`);
68
66
  let totalProcessed = 0;
@@ -113,8 +111,7 @@ class PullMetricsByProviderTask {
113
111
  try {
114
112
  const thresholds = this.thresholdResolver.resolveEntityThresholds(
115
113
  entity,
116
- metric,
117
- provider.getProviderId()
114
+ metric
118
115
  );
119
116
  const status = this.thresholdEvaluator.getFirstMatchingThreshold(
120
117
  value,
@@ -162,11 +159,7 @@ class PullMetricsByProviderTask {
162
159
  ).then(
163
160
  (promises) => promises.reduce((acc, curr) => {
164
161
  if (curr.status === "fulfilled" && curr.value !== void 0) {
165
- const result = curr.value;
166
- if (Array.isArray(result)) {
167
- return [...acc, ...result];
168
- }
169
- return [...acc, result];
162
+ return [...acc, ...curr.value];
170
163
  }
171
164
  return acc;
172
165
  }, [])
@@ -1 +1 @@
1
- {"version":3,"file":"PullMetricsByProviderTask.cjs.js","sources":["../../../src/scheduler/tasks/PullMetricsByProviderTask.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 { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport {\n AuthService,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isMetricIdDisabled } from '../../utils/metricUtils';\nimport { randomUUID } from 'node:crypto';\nimport { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { DbMetricValueCreate } from '../../database/types';\nimport { SchedulerOptions, SchedulerTask } from '../types';\nimport { ThresholdEvaluator } from '../../threshold/ThresholdEvaluator';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { ThresholdResolver } from '../../threshold/ThresholdResolver';\n\ntype Options = Pick<\n SchedulerOptions,\n | 'scheduler'\n | 'logger'\n | 'database'\n | 'config'\n | 'catalog'\n | 'auth'\n | 'thresholdEvaluator'\n | 'thresholdResolver'\n>;\n\nexport class PullMetricsByProviderTask implements SchedulerTask {\n private readonly config: Config;\n private readonly auth: AuthService;\n private readonly providerId: string;\n private readonly logger: LoggerService;\n private readonly catalog: CatalogService;\n private readonly provider: MetricProvider;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n private readonly thresholdResolver: ThresholdResolver;\n\n private static readonly CATALOG_BATCH_SIZE = 50;\n\n private static readonly DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { hours: 1 },\n timeout: { minutes: 15 },\n initialDelay: { minutes: 1 },\n };\n\n constructor(options: Options, provider: MetricProvider) {\n this.config = options.config;\n this.auth = options.auth;\n this.providerId = provider.getProviderId();\n this.logger = options.logger;\n this.catalog = options.catalog;\n this.provider = provider;\n this.scheduler = options.scheduler;\n this.database = options.database;\n this.thresholdEvaluator = options.thresholdEvaluator;\n this.thresholdResolver = options.thresholdResolver;\n }\n\n async start(): Promise<void> {\n const scheduleConfigPath = `scorecard.plugins.${this.providerId}.schedule`;\n const schedule = this.getScheduleFromConfig(scheduleConfigPath);\n\n const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);\n\n await taskRunner.run({\n id: this.providerId,\n fn: async () => {\n const logger = this.logger.child({\n class: this.constructor.name,\n taskId: this.providerId,\n taskInstanceId: randomUUID(),\n });\n\n try {\n await this.pullProviderMetrics(this.provider, logger);\n } catch (error) {\n logger.error(\n `${this.providerId} pulling metrics failed, ${error}`,\n error,\n );\n }\n },\n });\n }\n\n private getScheduleFromConfig(\n schedulePath: string,\n ): SchedulerServiceTaskScheduleDefinition {\n return this.config.has(schedulePath)\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n this.config.getConfig(schedulePath),\n )\n : PullMetricsByProviderTask.DEFAULT_SCHEDULE;\n }\n\n private async pullProviderMetrics(\n provider: MetricProvider,\n logger: LoggerService,\n ): Promise<void> {\n logger.info(`Pulling metrics for ${this.providerId}`);\n\n let totalProcessed = 0;\n let cursor: string | undefined = undefined;\n\n const metrics = provider.getMetrics();\n const metricsById = new Map<string, Metric>(metrics.map(m => [m.id, m]));\n const metricIds = metrics.map(m => m.id);\n\n try {\n do {\n const entitiesResponse = await this.catalog.queryEntities(\n {\n filter: provider.getCatalogFilter(),\n limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n cursor = entitiesResponse.pageInfo.nextCursor;\n\n const batchResults = await Promise.allSettled(\n entitiesResponse.items.map(async entity => {\n const entityRef = stringifyEntityRef(entity);\n const entityKind = normalizeField(entity.kind);\n const entityNamespace = normalizeField(entity.metadata.namespace);\n const entityOwner = normalizeOwnerRef(entity?.spec?.owner);\n\n const enabledMetricIds = metricIds.filter(\n metricId =>\n !isMetricIdDisabled(this.config, metricId, entity, logger),\n );\n\n if (enabledMetricIds.length === 0) {\n return undefined;\n }\n\n try {\n const resultsMap = await provider.calculateMetrics(entity);\n\n return enabledMetricIds.map(metricId => {\n if (!resultsMap.has(metricId)) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message: `calculateMetrics() did not return an entry for metric '${metricId}'`,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n\n const value = resultsMap.get(metricId) as MetricValue;\n const metric = metricsById.get(metricId)!;\n\n try {\n const thresholds =\n this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n provider.getProviderId(),\n );\n\n const status =\n this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metric.type,\n thresholds,\n );\n\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n status,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n } catch (error) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n });\n } catch (error) {\n return enabledMetricIds.map(\n metricId =>\n ({\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate),\n );\n }\n }),\n ).then(promises =>\n promises.reduce((acc, curr) => {\n if (curr.status === 'fulfilled' && curr.value !== undefined) {\n const result = curr.value;\n if (Array.isArray(result)) {\n return [...acc, ...result];\n }\n return [...acc, result];\n }\n return acc;\n }, [] as DbMetricValueCreate[]),\n );\n\n if (batchResults.length > 0) {\n const errorCount = batchResults.filter(r => r.error_message).length;\n logger.debug(\n `Storing ${batchResults.length} metric values (${errorCount} errors)`,\n );\n }\n\n await this.database.createMetricValues(batchResults);\n totalProcessed += entitiesResponse.items.length;\n } while (cursor !== undefined);\n\n logger.info(\n `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`,\n );\n } catch (error) {\n logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);\n\n throw error;\n }\n }\n}\n\nfunction normalizeField(field: unknown): string | undefined {\n if (typeof field !== 'string') return undefined;\n const normalized = field.trim().toLowerCase();\n if (!normalized) return undefined;\n\n // Prevent DB insertion failures (limits column length to 255 characters)\n return normalized.length <= 255 ? normalized : normalized.slice(0, 255);\n}\n"],"names":["randomUUID","readSchedulerServiceTaskScheduleDefinitionFromConfig","stringifyEntityRef","normalizeOwnerRef","isMetricIdDisabled"],"mappings":"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"}
1
+ {"version":3,"file":"PullMetricsByProviderTask.cjs.js","sources":["../../../src/scheduler/tasks/PullMetricsByProviderTask.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 { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport {\n AuthService,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Config } from '@backstage/config';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isMetricIdDisabled } from '../../utils/metricUtils';\nimport { randomUUID } from 'node:crypto';\nimport { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';\nimport { resolveScheduleFromConfig } from '../../utils/metricProviderConfigKeys';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { DbMetricValueCreate } from '../../database/types';\nimport { SchedulerOptions, SchedulerTask } from '../types';\nimport { ThresholdEvaluator } from '../../threshold/ThresholdEvaluator';\nimport {\n Metric,\n MetricValue,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { ThresholdResolver } from '../../threshold/ThresholdResolver';\n\ntype Options = Pick<\n SchedulerOptions,\n | 'scheduler'\n | 'logger'\n | 'database'\n | 'config'\n | 'catalog'\n | 'auth'\n | 'thresholdEvaluator'\n | 'thresholdResolver'\n>;\n\nexport class PullMetricsByProviderTask implements SchedulerTask {\n private readonly config: Config;\n private readonly auth: AuthService;\n private readonly providerId: string;\n private readonly logger: LoggerService;\n private readonly catalog: CatalogService;\n private readonly provider: MetricProvider;\n private readonly scheduler: SchedulerService;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdEvaluator: ThresholdEvaluator;\n private readonly thresholdResolver: ThresholdResolver;\n\n private static readonly CATALOG_BATCH_SIZE = 50;\n\n private static readonly DEFAULT_SCHEDULE: SchedulerServiceTaskScheduleDefinition =\n {\n frequency: { hours: 1 },\n timeout: { minutes: 15 },\n initialDelay: { minutes: 1 },\n };\n\n constructor(options: Options, provider: MetricProvider) {\n this.config = options.config;\n this.auth = options.auth;\n this.providerId = provider.getProviderId();\n this.logger = options.logger;\n this.catalog = options.catalog;\n this.provider = provider;\n this.scheduler = options.scheduler;\n this.database = options.database;\n this.thresholdEvaluator = options.thresholdEvaluator;\n this.thresholdResolver = options.thresholdResolver;\n }\n\n async start(): Promise<void> {\n const schedule =\n resolveScheduleFromConfig(\n this.config,\n this.provider.getProviderDatasourceId(),\n this.providerId,\n ) ?? PullMetricsByProviderTask.DEFAULT_SCHEDULE;\n\n const taskRunner = this.scheduler.createScheduledTaskRunner(schedule);\n\n await taskRunner.run({\n id: this.providerId,\n fn: async () => {\n const logger = this.logger.child({\n class: this.constructor.name,\n taskId: this.providerId,\n taskInstanceId: randomUUID(),\n });\n\n try {\n await this.pullProviderMetrics(this.provider, logger);\n } catch (error) {\n logger.error(\n `${this.providerId} pulling metrics failed, ${error}`,\n error,\n );\n }\n },\n });\n }\n\n private async pullProviderMetrics(\n provider: MetricProvider,\n logger: LoggerService,\n ): Promise<void> {\n logger.info(`Pulling metrics for ${this.providerId}`);\n\n let totalProcessed = 0;\n let cursor: string | undefined = undefined;\n\n const metrics = provider.getMetrics();\n const metricsById = new Map<string, Metric>(metrics.map(m => [m.id, m]));\n const metricIds = metrics.map(m => m.id);\n\n try {\n do {\n const entitiesResponse = await this.catalog.queryEntities(\n {\n filter: provider.getCatalogFilter(),\n limit: PullMetricsByProviderTask.CATALOG_BATCH_SIZE,\n ...(cursor ? { cursor } : {}),\n },\n { credentials: await this.auth.getOwnServiceCredentials() },\n );\n\n cursor = entitiesResponse.pageInfo.nextCursor;\n\n const batchResults = await Promise.allSettled(\n entitiesResponse.items.map(async entity => {\n const entityRef = stringifyEntityRef(entity);\n const entityKind = normalizeField(entity.kind);\n const entityNamespace = normalizeField(entity.metadata.namespace);\n const entityOwner = normalizeOwnerRef(entity?.spec?.owner);\n\n const enabledMetricIds = metricIds.filter(\n metricId =>\n !isMetricIdDisabled(this.config, metricId, entity, logger),\n );\n\n if (enabledMetricIds.length === 0) {\n return undefined;\n }\n\n try {\n const resultsMap = await provider.calculateMetrics(entity);\n\n return enabledMetricIds.map(metricId => {\n if (!resultsMap.has(metricId)) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message: `calculateMetrics() did not return an entry for metric '${metricId}'`,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n\n const value = resultsMap.get(metricId) as MetricValue;\n const metric = metricsById.get(metricId)!;\n\n try {\n const thresholds =\n this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n\n const status =\n this.thresholdEvaluator.getFirstMatchingThreshold(\n value,\n metric.type,\n thresholds,\n );\n\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n status,\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n } catch (error) {\n return {\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate;\n }\n });\n } catch (error) {\n return enabledMetricIds.map(\n metricId =>\n ({\n catalog_entity_ref: entityRef,\n metric_id: metricId,\n value: undefined,\n timestamp: new Date(),\n error_message:\n error instanceof Error ? error.message : String(error),\n entity_kind: entityKind,\n entity_namespace: entityNamespace,\n entity_owner: entityOwner,\n } as DbMetricValueCreate),\n );\n }\n }),\n ).then(promises =>\n promises.reduce((acc, curr) => {\n if (curr.status === 'fulfilled' && curr.value !== undefined) {\n return [...acc, ...curr.value];\n }\n return acc;\n }, [] as DbMetricValueCreate[]),\n );\n\n if (batchResults.length > 0) {\n const errorCount = batchResults.filter(r => r.error_message).length;\n logger.debug(\n `Storing ${batchResults.length} metric values (${errorCount} errors)`,\n );\n }\n\n await this.database.createMetricValues(batchResults);\n totalProcessed += entitiesResponse.items.length;\n } while (cursor !== undefined);\n\n logger.info(\n `Completed metric pull for ${this.providerId}: processed ${totalProcessed} entities`,\n );\n } catch (error) {\n logger.error(`Failed to pull metrics for ${this.providerId}: ${error}`);\n\n throw error;\n }\n }\n}\n\nfunction normalizeField(field: unknown): string | undefined {\n if (typeof field !== 'string') return undefined;\n const normalized = field.trim().toLowerCase();\n if (!normalized) return undefined;\n\n // Prevent DB insertion failures (limits column length to 255 characters)\n return normalized.length <= 255 ? normalized : normalized.slice(0, 255);\n}\n"],"names":["resolveScheduleFromConfig","randomUUID","stringifyEntityRef","normalizeOwnerRef","isMetricIdDisabled"],"mappings":"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"}
@@ -63,13 +63,11 @@ class CatalogMetricService {
63
63
  ({ metric_id, value, error_message, timestamp, status }) => {
64
64
  let thresholds;
65
65
  let thresholdError;
66
- const provider = this.registry.getProvider(metric_id);
67
66
  const metric = this.registry.getMetric(metric_id);
68
67
  try {
69
68
  thresholds = this.thresholdResolver.resolveEntityThresholds(
70
69
  entity,
71
- metric,
72
- provider.getProviderId()
70
+ metric
73
71
  );
74
72
  if (value === null) {
75
73
  thresholdError = "Unable to evaluate thresholds, metric value is missing";
@@ -1 +1 @@
1
- {"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.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 MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n ScorecardEntityHealthSummary,\n aggregationTypes,\n AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { isMetricCalculationError } from '../utils/metricCalculationError';\nimport { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n};\n\nexport class CatalogMetricService {\n private static entityHealthSummary(\n accessibleRows: DbMetricValue[],\n countsArePartial: boolean,\n ): ScorecardEntityHealthSummary {\n const calculationErrorCount = accessibleRows.filter(row =>\n isMetricCalculationError(row),\n ).length;\n return {\n totalEntities: accessibleRows.length,\n calculationErrorCount,\n countsArePartial,\n };\n }\n\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdResolver: ThresholdResolver;\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n this.thresholdResolver = options.thresholdResolver;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n metricIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(\n ({ metric_id, value, error_message, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const provider = this.registry.getProvider(metric_id);\n const metric = this.registry.getMetric(metric_id);\n\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n provider.getProviderId(),\n );\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (error_message) {\n thresholdError = error_message;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = isMetricCalculationError({\n value,\n error_message,\n });\n\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get an aggregated metric by status grouped for multiple entities and a single metric ID.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @returns Aggregated metric by status grouped results\n */\n async getStatusGroupedAggregatedMetrics(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n /**\n * Get an aggregated metric by aggregation type.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @param aggregationType - Aggregation type to use.\n * @returns Aggregated metric by aggregation type results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n aggregationType: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n if (aggregationType === aggregationTypes.statusGrouped) {\n return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);\n }\n throw new Error(`Unsupported aggregation type: ${aggregationType}`);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.openPRs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // Query database with all DB filters first\n // At the moment, this is going to be an O(MAX_FETCHABLE_ROWS) cost and is intentional to avoid leaking\n // pre-auth counts. MAX_FETCHABLE_ROWS and BATCH_SIZE are to be used as a method to of adjustment to\n // find the right amount of performance\n const rows = await this.database.readEntityMetricsWithFilters(metricId, {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: 0,\n },\n });\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with user\n // credentials. The catalog enforces auth natively: null = unauthorized or deleted.\n // We also cache the returned Entity objects so we can enrich the page rows without\n // a second catalog round-trip. Sequential processing preserves DB sort order.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n try {\n for (let i = 0; i < rows.length; i += CatalogMetricService.BATCH_SIZE) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalog_entity_ref),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n // Filter out the unauthorized entities\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n entityMap.set(batch[j].catalog_entity_ref, entity);\n accessibleRows.push(batch[j]);\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // True when DB results were capped; pagination.total may undercount the full dataset.\n const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n}\n"],"names":["isMetricCalculationError","NotFoundError","filterAuthorizedMetrics","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef"],"mappings":"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"}
1
+ {"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.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 MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n ScorecardEntityHealthSummary,\n aggregationTypes,\n AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { isMetricCalculationError } from '../utils/metricCalculationError';\nimport { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n};\n\nexport class CatalogMetricService {\n private static entityHealthSummary(\n accessibleRows: DbMetricValue[],\n countsArePartial: boolean,\n ): ScorecardEntityHealthSummary {\n const calculationErrorCount = accessibleRows.filter(row =>\n isMetricCalculationError(row),\n ).length;\n return {\n totalEntities: accessibleRows.length,\n calculationErrorCount,\n countsArePartial,\n };\n }\n\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n private readonly thresholdResolver: ThresholdResolver;\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n this.thresholdResolver = options.thresholdResolver;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n metricIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(\n ({ metric_id, value, error_message, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const metric = this.registry.getMetric(metric_id);\n\n try {\n thresholds = this.thresholdResolver.resolveEntityThresholds(\n entity,\n metric,\n );\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (error_message) {\n thresholdError = error_message;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = isMetricCalculationError({\n value,\n error_message,\n });\n\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get an aggregated metric by status grouped for multiple entities and a single metric ID.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @returns Aggregated metric by status grouped results\n */\n async getStatusGroupedAggregatedMetrics(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n /**\n * Get an aggregated metric by aggregation type.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @param aggregationType - Aggregation type to use.\n * @returns Aggregated metric by aggregation type results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n aggregationType: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n if (aggregationType === aggregationTypes.statusGrouped) {\n return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);\n }\n throw new Error(`Unsupported aggregation type: ${aggregationType}`);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.openPRs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // Query database with all DB filters first\n // At the moment, this is going to be an O(MAX_FETCHABLE_ROWS) cost and is intentional to avoid leaking\n // pre-auth counts. MAX_FETCHABLE_ROWS and BATCH_SIZE are to be used as a method to of adjustment to\n // find the right amount of performance\n const rows = await this.database.readEntityMetricsWithFilters(metricId, {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: 0,\n },\n });\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with user\n // credentials. The catalog enforces auth natively: null = unauthorized or deleted.\n // We also cache the returned Entity objects so we can enrich the page rows without\n // a second catalog round-trip. Sequential processing preserves DB sort order.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n try {\n for (let i = 0; i < rows.length; i += CatalogMetricService.BATCH_SIZE) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalog_entity_ref),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n // Filter out the unauthorized entities\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n entityMap.set(batch[j].catalog_entity_ref, entity);\n accessibleRows.push(batch[j]);\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // True when DB results were capped; pagination.total may undercount the full dataset.\n const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n}\n"],"names":["isMetricCalculationError","NotFoundError","filterAuthorizedMetrics","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef"],"mappings":"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"}
@@ -97,7 +97,6 @@ async function createRouter({
97
97
  permissions,
98
98
  backstagePluginScorecardCommon.scorecardMetricReadPermission
99
99
  );
100
- const provider = metricProvidersRegistry.getProvider(metricId);
101
100
  const metric = metricProvidersRegistry.getMetric(metricId);
102
101
  const authorizedMetrics = permissionUtils.filterAuthorizedMetrics([metric], conditions);
103
102
  if (authorizedMetrics.length === 0) {
@@ -117,10 +116,7 @@ async function createRouter({
117
116
  for (const entityRef of entitiesOwnedByAUser) {
118
117
  await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
119
118
  }
120
- const thresholds = thresholdResolver.resolveMetricThresholds(
121
- metric,
122
- provider.getProviderId()
123
- );
119
+ const thresholds = thresholdResolver.resolveMetricThresholds(metric);
124
120
  logger.warn(
125
121
  `Deprecated Scorecard API: GET /metrics/${metricId}/catalog/aggregations is deprecated; use GET /aggregations/:aggregationId (e.g. when the aggregation id matches the metric id, GET /aggregations/${metricId}).`
126
122
  );
@@ -198,9 +194,6 @@ async function createRouter({
198
194
  );
199
195
  const userEntityRef = await permissionUtils.getUserEntityRef(credentials);
200
196
  const aggregationConfig = aggregationsService.getAggregationConfig(aggregationId);
201
- const provider = metricProvidersRegistry.getProvider(
202
- aggregationConfig.metricId
203
- );
204
197
  const metric = metricProvidersRegistry.getMetric(
205
198
  aggregationConfig?.metricId ?? aggregationId
206
199
  );
@@ -217,10 +210,7 @@ async function createRouter({
217
210
  `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`
218
211
  );
219
212
  }
220
- const thresholds = thresholdResolver.resolveMetricThresholds(
221
- metric,
222
- provider.getProviderId()
223
- );
213
+ const thresholds = thresholdResolver.resolveMetricThresholds(metric);
224
214
  res.json(
225
215
  await aggregationsService.getAggregatedMetricByEntityRefs({
226
216
  metric,
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.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 AuthenticationError,\n InputError,\n NotAllowedError,\n} from '@backstage/errors';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n LoggerService,\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n authorizeConditional,\n getUserEntityRef,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateMetricIdsQueryParams } from '../middlewares/validateMetricIdsQueryParams';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\nimport { AggregationsService } from './aggregations/AggregationService';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\n\nexport type ScorecardRouterOptions = {\n service: {\n aggregationsService: AggregationsService;\n catalogMetricService: CatalogMetricService;\n };\n metricProvidersRegistry: MetricProvidersRegistry;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n service,\n catalog,\n httpAuth,\n permissions,\n logger,\n thresholdResolver,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const { aggregationsService, catalogMetricService } = service;\n\n router.get(\n '/metrics',\n validateMetricIdsQueryParams,\n validateDatasourceQueryParams,\n async (req, res) => {\n const { metricIds, datasource } = req.query;\n\n if (metricIds && datasource) {\n throw new InputError('Cannot filter by both metricIds and datasource');\n }\n\n if (metricIds) {\n return res.json({\n metrics: metricProvidersRegistry.listMetrics(\n parseCommaSeparatedString(metricIds as string),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(\n datasource as string,\n ),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n },\n );\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name',\n validateMetricIdsQueryParams,\n async (req, res) => {\n const { metricIds } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const entityRef = stringifyEntityRef({ kind, namespace, name });\n\n // Check if user has permission to read this specific catalog entity\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const metricIdArray = metricIds\n ? parseCommaSeparatedString(metricIds as string)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n },\n );\n\n // Deprecated (RFC 8594): use GET /aggregations/:aggregationId instead.\n router.get(\n '/metrics/:metricId/catalog/aggregations',\n (req, res, next) => {\n const { metricId } = req.params;\n const successorPath = `${req.baseUrl}/aggregations/${encodeURIComponent(\n metricId,\n )}`;\n res.setHeader('Deprecation', 'true');\n res.setHeader('Link', `<${successorPath}>; rel=\"alternate\"`);\n next();\n },\n async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const provider = metricProvidersRegistry.getProvider(metricId);\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(\n metric,\n provider.getProviderId(),\n );\n\n logger.warn(\n `Deprecated Scorecard API: GET /metrics/${metricId}/catalog/aggregations is deprecated; use GET /aggregations/:aggregationId (e.g. when the aggregation id matches the metric id, GET /aggregations/${metricId}).`,\n );\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(metricId);\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/metrics/:metricId/catalog/aggregations/entities',\n async (req, res) => {\n const { metricId } = req.params;\n\n const {\n page,\n pageSize,\n status,\n owner,\n kind,\n namespace,\n entityName,\n sortBy,\n sortOrder,\n } = validateDrillDownMetricsSchema(req.query, logger);\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entityMetrics = await catalogMetricService.getEntityMetricDetails(\n metricId,\n credentials,\n {\n status,\n owner,\n kind,\n entityName,\n namespace,\n sortBy,\n sortOrder,\n page,\n limit: pageSize,\n },\n );\n\n res.json(entityMetrics);\n },\n );\n\n router.get(\n '/aggregations/:aggregationId',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(aggregationId);\n\n const provider = metricProvidersRegistry.getProvider(\n aggregationConfig.metricId,\n );\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(\n metric,\n provider.getProviderId(),\n );\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/metadata',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(aggregationId);\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","NotAllowedError","AuthenticationError","getEntitiesOwnedByUser","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef","AggregatedMetricMapper"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.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 AuthenticationError,\n InputError,\n NotAllowedError,\n} from '@backstage/errors';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n LoggerService,\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n authorizeConditional,\n getUserEntityRef,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateMetricIdsQueryParams } from '../middlewares/validateMetricIdsQueryParams';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { AggregatedMetricMapper } from './mappers';\nimport { validateDrillDownMetricsSchema } from '../validation/validateDrillDownMetricsSchema';\nimport { validateAggregationIdParam } from '../middlewares/validateAggregationIdParam';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { validateDatasourceQueryParams } from '../middlewares/validateDatasourceQueryParams';\nimport { AggregationsService } from './aggregations/AggregationService';\nimport { ThresholdResolver } from '../threshold/ThresholdResolver';\n\nexport type ScorecardRouterOptions = {\n service: {\n aggregationsService: AggregationsService;\n catalogMetricService: CatalogMetricService;\n };\n metricProvidersRegistry: MetricProvidersRegistry;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n logger: LoggerService;\n thresholdResolver: ThresholdResolver;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n service,\n catalog,\n httpAuth,\n permissions,\n logger,\n thresholdResolver,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const { aggregationsService, catalogMetricService } = service;\n\n router.get(\n '/metrics',\n validateMetricIdsQueryParams,\n validateDatasourceQueryParams,\n async (req, res) => {\n const { metricIds, datasource } = req.query;\n\n if (metricIds && datasource) {\n throw new InputError('Cannot filter by both metricIds and datasource');\n }\n\n if (metricIds) {\n return res.json({\n metrics: metricProvidersRegistry.listMetrics(\n parseCommaSeparatedString(metricIds as string),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(\n datasource as string,\n ),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n },\n );\n\n router.get(\n '/metrics/catalog/:kind/:namespace/:name',\n validateMetricIdsQueryParams,\n async (req, res) => {\n const { metricIds } = req.query;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const entityRef = stringifyEntityRef({ kind, namespace, name });\n\n // Check if user has permission to read this specific catalog entity\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n\n const metricIdArray = metricIds\n ? parseCommaSeparatedString(metricIds as string)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n },\n );\n\n // Deprecated (RFC 8594): use GET /aggregations/:aggregationId instead.\n router.get(\n '/metrics/:metricId/catalog/aggregations',\n (req, res, next) => {\n const { metricId } = req.params;\n const successorPath = `${req.baseUrl}/aggregations/${encodeURIComponent(\n metricId,\n )}`;\n res.setHeader('Deprecation', 'true');\n res.setHeader('Link', `<${successorPath}>; rel=\"alternate\"`);\n next();\n },\n async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n await httpAuth.credentials(req),\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n logger.warn(\n `Deprecated Scorecard API: GET /metrics/${metricId}/catalog/aggregations is deprecated; use GET /aggregations/:aggregationId (e.g. when the aggregation id matches the metric id, GET /aggregations/${metricId}).`,\n );\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(metricId);\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/metrics/:metricId/catalog/aggregations/entities',\n async (req, res) => {\n const { metricId } = req.params;\n\n const {\n page,\n pageSize,\n status,\n owner,\n kind,\n namespace,\n entityName,\n sortBy,\n sortOrder,\n } = validateDrillDownMetricsSchema(req.query, logger);\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const metric = metricProvidersRegistry.getMetric(metricId);\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the scorecard metrics, your administrator must grant you the required permission.`,\n );\n }\n\n const userEntityRef = credentials?.principal?.userEntityRef;\n\n if (!userEntityRef) {\n throw new AuthenticationError('User entity reference not found');\n }\n\n const entityMetrics = await catalogMetricService.getEntityMetricDetails(\n metricId,\n credentials,\n {\n status,\n owner,\n kind,\n entityName,\n namespace,\n sortBy,\n sortOrder,\n page,\n limit: pageSize,\n },\n );\n\n res.json(entityMetrics);\n },\n );\n\n router.get(\n '/aggregations/:aggregationId',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n\n const { conditions } = await authorizeConditional(\n credentials,\n permissions,\n scorecardMetricReadPermission,\n );\n\n const userEntityRef = await getUserEntityRef(credentials);\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(aggregationId);\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n const entitiesOwnedByAUser = await getEntitiesOwnedByUser(userEntityRef, {\n catalog,\n credentials,\n });\n\n for (const entityRef of entitiesOwnedByAUser) {\n await checkEntityAccess(entityRef, req, permissions, httpAuth);\n }\n\n const authorizedMetrics = filterAuthorizedMetrics([metric], conditions);\n if (authorizedMetrics.length === 0) {\n throw new NotAllowedError(\n `To view the aggregation of a scorecard metric, your administrator must grant you the required permission.`,\n );\n }\n\n const thresholds = thresholdResolver.resolveMetricThresholds(metric);\n\n res.json(\n await aggregationsService.getAggregatedMetricByEntityRefs({\n metric,\n thresholds,\n aggregationConfig,\n entityRefs: entitiesOwnedByAUser,\n }),\n );\n },\n );\n\n router.get(\n '/aggregations/:aggregationId/metadata',\n validateAggregationIdParam,\n async (req, res) => {\n const { aggregationId } = req.params;\n\n const aggregationConfig =\n aggregationsService.getAggregationConfig(aggregationId);\n\n const metric = metricProvidersRegistry.getMetric(\n aggregationConfig?.metricId ?? aggregationId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregationMetadata(metric, aggregationConfig),\n );\n },\n );\n\n return router;\n}\n"],"names":["Router","express","validateMetricIdsQueryParams","validateDatasourceQueryParams","InputError","parseCommaSeparatedString","authorizeConditional","scorecardMetricReadPermission","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","NotAllowedError","AuthenticationError","getEntitiesOwnedByUser","validateDrillDownMetricsSchema","validateAggregationIdParam","getUserEntityRef","AggregatedMetricMapper"],"mappings":"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"}
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plugin-scorecard-node');
4
- var mergeEntityAndProviderThresholds = require('../utils/mergeEntityAndProviderThresholds.cjs.js');
4
+ var mergeEntityAndMetricThresholds = require('../utils/mergeEntityAndMetricThresholds.cjs.js');
5
+ var metricProviderConfigKeys = require('../utils/metricProviderConfigKeys.cjs.js');
5
6
 
6
7
  class ThresholdResolver {
7
- // providerId: thresholds
8
+ // metricId: thresholds
8
9
  constructor(config, providers) {
9
10
  this.config = config;
10
11
  for (const provider of providers) {
@@ -13,28 +14,37 @@ class ThresholdResolver {
13
14
  }
14
15
  config;
15
16
  configuredThresholds = /* @__PURE__ */ new Map();
16
- resolveMetricThresholds(metric, providerId) {
17
- return this.configuredThresholds.get(providerId) ?? metric.thresholds;
17
+ resolveMetricThresholds(metric) {
18
+ return this.configuredThresholds.get(metric.id) ?? metric.thresholds;
18
19
  }
19
- resolveEntityThresholds(entity, metric, providerId) {
20
- return mergeEntityAndProviderThresholds.mergeEntityAndProviderThresholds(
20
+ resolveEntityThresholds(entity, metric) {
21
+ return mergeEntityAndMetricThresholds.mergeEntityAndMetricThresholds(
21
22
  entity,
22
23
  metric,
23
- providerId,
24
- this.resolveMetricThresholds(metric, providerId)
24
+ this.resolveMetricThresholds(metric)
25
25
  );
26
26
  }
27
27
  setConfiguredThresholds(provider) {
28
+ const datasourceId = provider.getProviderDatasourceId();
28
29
  const providerId = provider.getProviderId();
29
- const metrics = provider.getMetrics();
30
- if (metrics.length === 0) return;
31
- const thresholds = backstagePluginScorecardNode.getThresholdsFromConfig(
32
- this.config,
33
- `scorecard.plugins.${providerId}.thresholds`,
34
- metrics[0].type
35
- );
36
- if (thresholds) {
37
- this.configuredThresholds.set(providerId, thresholds);
30
+ for (const metric of provider.getMetrics()) {
31
+ const thresholdsPath = metricProviderConfigKeys.resolveThresholdsConfigPath(
32
+ this.config,
33
+ datasourceId,
34
+ providerId,
35
+ metric.id
36
+ );
37
+ if (!thresholdsPath) {
38
+ continue;
39
+ }
40
+ const thresholds = backstagePluginScorecardNode.getThresholdsFromConfig(
41
+ this.config,
42
+ thresholdsPath,
43
+ metric.type
44
+ );
45
+ if (thresholds) {
46
+ this.configuredThresholds.set(metric.id, thresholds);
47
+ }
38
48
  }
39
49
  }
40
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ThresholdResolver.cjs.js","sources":["../../src/threshold/ThresholdResolver.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 type { Config } from '@backstage/config';\nimport type { Entity } from '@backstage/catalog-model';\nimport type {\n Metric,\n ThresholdConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n getThresholdsFromConfig,\n type MetricProvider,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\n\nexport class ThresholdResolver {\n private readonly configuredThresholds = new Map<string, ThresholdConfig>(); // providerId: thresholds\n\n constructor(private readonly config: Config, providers: MetricProvider[]) {\n for (const provider of providers) {\n this.setConfiguredThresholds(provider);\n }\n }\n\n resolveMetricThresholds(metric: Metric, providerId: string): ThresholdConfig {\n return this.configuredThresholds.get(providerId) ?? metric.thresholds;\n }\n\n resolveEntityThresholds(\n entity: Entity,\n metric: Metric,\n providerId: string,\n ): ThresholdConfig {\n return mergeEntityAndProviderThresholds(\n entity,\n metric,\n providerId,\n this.resolveMetricThresholds(metric, providerId),\n );\n }\n\n private setConfiguredThresholds(provider: MetricProvider): void {\n const providerId = provider.getProviderId();\n const metrics = provider.getMetrics();\n if (metrics.length === 0) return;\n\n const thresholds = getThresholdsFromConfig(\n this.config,\n `scorecard.plugins.${providerId}.thresholds`,\n metrics[0].type,\n );\n\n if (thresholds) {\n this.configuredThresholds.set(providerId, thresholds);\n }\n }\n}\n"],"names":["mergeEntityAndProviderThresholds","getThresholdsFromConfig"],"mappings":";;;;;AA4BO,MAAM,iBAAA,CAAkB;AAAA;AAAA,EAG7B,WAAA,CAA6B,QAAgB,SAAA,EAA6B;AAA7C,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAC3B,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,IAAA,CAAK,wBAAwB,QAAQ,CAAA;AAAA,IACvC;AAAA,EACF;AAAA,EAJ6B,MAAA;AAAA,EAFZ,oBAAA,uBAA2B,GAAA,EAA6B;AAAA,EAQzE,uBAAA,CAAwB,QAAgB,UAAA,EAAqC;AAC3E,IAAA,OAAO,IAAA,CAAK,oBAAA,CAAqB,GAAA,CAAI,UAAU,KAAK,MAAA,CAAO,UAAA;AAAA,EAC7D;AAAA,EAEA,uBAAA,CACE,MAAA,EACA,MAAA,EACA,UAAA,EACiB;AACjB,IAAA,OAAOA,iEAAA;AAAA,MACL,MAAA;AAAA,MACA,MAAA;AAAA,MACA,UAAA;AAAA,MACA,IAAA,CAAK,uBAAA,CAAwB,MAAA,EAAQ,UAAU;AAAA,KACjD;AAAA,EACF;AAAA,EAEQ,wBAAwB,QAAA,EAAgC;AAC9D,IAAA,MAAM,UAAA,GAAa,SAAS,aAAA,EAAc;AAC1C,IAAA,MAAM,OAAA,GAAU,SAAS,UAAA,EAAW;AACpC,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAE1B,IAAA,MAAM,UAAA,GAAaC,oDAAA;AAAA,MACjB,IAAA,CAAK,MAAA;AAAA,MACL,qBAAqB,UAAU,CAAA,WAAA,CAAA;AAAA,MAC/B,OAAA,CAAQ,CAAC,CAAA,CAAE;AAAA,KACb;AAEA,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,IAAA,CAAK,oBAAA,CAAqB,GAAA,CAAI,UAAA,EAAY,UAAU,CAAA;AAAA,IACtD;AAAA,EACF;AACF;;;;"}
1
+ {"version":3,"file":"ThresholdResolver.cjs.js","sources":["../../src/threshold/ThresholdResolver.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 type { Config } from '@backstage/config';\nimport type { Entity } from '@backstage/catalog-model';\nimport type {\n Metric,\n ThresholdConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n getThresholdsFromConfig,\n type MetricProvider,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { mergeEntityAndMetricThresholds } from '../utils/mergeEntityAndMetricThresholds';\nimport { resolveThresholdsConfigPath } from '../utils/metricProviderConfigKeys';\n\nexport class ThresholdResolver {\n private readonly configuredThresholds = new Map<string, ThresholdConfig>(); // metricId: thresholds\n\n constructor(private readonly config: Config, providers: MetricProvider[]) {\n for (const provider of providers) {\n this.setConfiguredThresholds(provider);\n }\n }\n\n resolveMetricThresholds(metric: Metric): ThresholdConfig {\n return this.configuredThresholds.get(metric.id) ?? metric.thresholds;\n }\n\n resolveEntityThresholds(entity: Entity, metric: Metric): ThresholdConfig {\n return mergeEntityAndMetricThresholds(\n entity,\n metric,\n this.resolveMetricThresholds(metric),\n );\n }\n\n private setConfiguredThresholds(provider: MetricProvider): void {\n const datasourceId = provider.getProviderDatasourceId();\n const providerId = provider.getProviderId();\n\n for (const metric of provider.getMetrics()) {\n const thresholdsPath = resolveThresholdsConfigPath(\n this.config,\n datasourceId,\n providerId,\n metric.id,\n );\n if (!thresholdsPath) {\n continue;\n }\n\n const thresholds = getThresholdsFromConfig(\n this.config,\n thresholdsPath,\n metric.type,\n );\n if (thresholds) {\n this.configuredThresholds.set(metric.id, thresholds);\n }\n }\n }\n}\n"],"names":["mergeEntityAndMetricThresholds","resolveThresholdsConfigPath","getThresholdsFromConfig"],"mappings":";;;;;;AA6BO,MAAM,iBAAA,CAAkB;AAAA;AAAA,EAG7B,WAAA,CAA6B,QAAgB,SAAA,EAA6B;AAA7C,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAC3B,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,IAAA,CAAK,wBAAwB,QAAQ,CAAA;AAAA,IACvC;AAAA,EACF;AAAA,EAJ6B,MAAA;AAAA,EAFZ,oBAAA,uBAA2B,GAAA,EAA6B;AAAA,EAQzE,wBAAwB,MAAA,EAAiC;AACvD,IAAA,OAAO,KAAK,oBAAA,CAAqB,GAAA,CAAI,MAAA,CAAO,EAAE,KAAK,MAAA,CAAO,UAAA;AAAA,EAC5D;AAAA,EAEA,uBAAA,CAAwB,QAAgB,MAAA,EAAiC;AACvE,IAAA,OAAOA,6DAAA;AAAA,MACL,MAAA;AAAA,MACA,MAAA;AAAA,MACA,IAAA,CAAK,wBAAwB,MAAM;AAAA,KACrC;AAAA,EACF;AAAA,EAEQ,wBAAwB,QAAA,EAAgC;AAC9D,IAAA,MAAM,YAAA,GAAe,SAAS,uBAAA,EAAwB;AACtD,IAAA,MAAM,UAAA,GAAa,SAAS,aAAA,EAAc;AAE1C,IAAA,KAAA,MAAW,MAAA,IAAU,QAAA,CAAS,UAAA,EAAW,EAAG;AAC1C,MAAA,MAAM,cAAA,GAAiBC,oDAAA;AAAA,QACrB,IAAA,CAAK,MAAA;AAAA,QACL,YAAA;AAAA,QACA,UAAA;AAAA,QACA,MAAA,CAAO;AAAA,OACT;AACA,MAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,UAAA,GAAaC,oDAAA;AAAA,QACjB,IAAA,CAAK,MAAA;AAAA,QACL,cAAA;AAAA,QACA,MAAA,CAAO;AAAA,OACT;AACA,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,IAAA,CAAK,oBAAA,CAAqB,GAAA,CAAI,MAAA,CAAO,EAAA,EAAI,UAAU,CAAA;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;;;;"}
@@ -4,10 +4,10 @@ var catalogModel = require('@backstage/catalog-model');
4
4
  var backstagePluginScorecardNode = require('@red-hat-developer-hub/backstage-plugin-scorecard-node');
5
5
  var errors = require('@backstage/errors');
6
6
 
7
- const thresholdRulesAnnotationPrefix = (providerId) => `scorecard.io/${providerId}.thresholds.rules.`;
8
- function parseEntityAnnotationThresholds(entity, providerId) {
7
+ const thresholdRulesAnnotationPrefix = (metricId) => `scorecard.io/${metricId}.thresholds.rules.`;
8
+ function parseEntityAnnotationThresholds(entity, metricId) {
9
9
  const annotations = entity.metadata?.annotations || {};
10
- const prefix = thresholdRulesAnnotationPrefix(providerId);
10
+ const prefix = thresholdRulesAnnotationPrefix(metricId);
11
11
  const overrides = [];
12
12
  for (const [annotationKey, expression] of Object.entries(annotations)) {
13
13
  if (annotationKey.startsWith(prefix) && expression) {
@@ -17,15 +17,16 @@ function parseEntityAnnotationThresholds(entity, providerId) {
17
17
  }
18
18
  return overrides;
19
19
  }
20
- function mergeEntityAndProviderThresholds(entity, metric, providerId, baseThresholds) {
20
+ function mergeEntityAndMetricThresholds(entity, metric, baseThresholds) {
21
21
  let isRulesMerged = false;
22
- const providerThresholds = baseThresholds ?? metric.thresholds;
22
+ const metricThresholds = baseThresholds ?? metric.thresholds;
23
23
  const metricType = metric.type;
24
+ const metricId = metric.id;
24
25
  const entityAnnotationThresholds = parseEntityAnnotationThresholds(
25
26
  entity,
26
- providerId
27
+ metricId
27
28
  );
28
- const mergedRules = [...providerThresholds.rules];
29
+ const mergedRules = [...metricThresholds.rules];
29
30
  for (const override of entityAnnotationThresholds) {
30
31
  const foundKey = mergedRules.findIndex((rule) => rule.key === override.key);
31
32
  if (foundKey === -1) {
@@ -34,7 +35,7 @@ function mergeEntityAndProviderThresholds(entity, metric, providerId, baseThresh
34
35
  entity
35
36
  )} thresholds by ${JSON.stringify(
36
37
  override
37
- )}, metric provider ${providerId} does not support key ${override.key}`
38
+ )}, metric ${metricId} does not support key ${override.key}`
38
39
  );
39
40
  }
40
41
  const mergedRule = { ...mergedRules[foundKey], ...override };
@@ -45,7 +46,7 @@ function mergeEntityAndProviderThresholds(entity, metric, providerId, baseThresh
45
46
  if (errors.isError(e)) {
46
47
  throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
47
48
  `Invalid threshold annotation '${thresholdRulesAnnotationPrefix(
48
- providerId
49
+ metricId
49
50
  )}${override.key}: ${override.expression}' in entity '${catalogModel.stringifyEntityRef(entity)}': ${e.message}`
50
51
  );
51
52
  }
@@ -61,5 +62,5 @@ function mergeEntityAndProviderThresholds(entity, metric, providerId, baseThresh
61
62
  };
62
63
  }
63
64
 
64
- exports.mergeEntityAndProviderThresholds = mergeEntityAndProviderThresholds;
65
- //# sourceMappingURL=mergeEntityAndProviderThresholds.cjs.js.map
65
+ exports.mergeEntityAndMetricThresholds = mergeEntityAndMetricThresholds;
66
+ //# sourceMappingURL=mergeEntityAndMetricThresholds.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeEntityAndMetricThresholds.cjs.js","sources":["../../src/utils/mergeEntityAndMetricThresholds.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 { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport type {\n Metric,\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n validateThresholdsForMetric,\n ThresholdConfigFormatError,\n validateThresholdNumberIntervals,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isError } from '@backstage/errors';\n\nconst thresholdRulesAnnotationPrefix = (metricId: string) =>\n `scorecard.io/${metricId}.thresholds.rules.`;\n\n/**\n * Extract threshold override rules from entity annotations for a given metric, doesn't validate rules.\n */\nfunction parseEntityAnnotationThresholds(\n entity: Entity,\n metricId: string,\n): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = thresholdRulesAnnotationPrefix(metricId);\n const overrides: ThresholdRule[] = [];\n\n for (const [annotationKey, expression] of Object.entries(annotations)) {\n if (annotationKey.startsWith(prefix) && expression) {\n const key = annotationKey.substring(prefix.length);\n overrides.push({ key, expression });\n }\n }\n\n return overrides;\n}\n\nexport function mergeEntityAndMetricThresholds(\n entity: Entity,\n metric: Metric,\n baseThresholds?: ThresholdConfig,\n): ThresholdConfig {\n let isRulesMerged = false;\n\n const metricThresholds = baseThresholds ?? metric.thresholds;\n const metricType = metric.type;\n const metricId = metric.id;\n const entityAnnotationThresholds = parseEntityAnnotationThresholds(\n entity,\n metricId,\n );\n\n const mergedRules = [...metricThresholds.rules];\n for (const override of entityAnnotationThresholds) {\n const foundKey = mergedRules.findIndex(rule => rule.key === override.key);\n if (foundKey === -1) {\n throw new ThresholdConfigFormatError(\n `Unable to override ${stringifyEntityRef(\n entity,\n )} thresholds by ${JSON.stringify(\n override,\n )}, metric ${metricId} does not support key ${override.key}`,\n );\n }\n\n const mergedRule: ThresholdRule = { ...mergedRules[foundKey], ...override };\n try {\n validateThresholdsForMetric({ rules: [mergedRule] }, metricType);\n\n if (!isRulesMerged) isRulesMerged = true;\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${thresholdRulesAnnotationPrefix(\n metricId,\n )}${override.key}: ${\n override.expression\n }' in entity '${stringifyEntityRef(entity)}': ${e.message}`,\n );\n }\n throw e;\n }\n\n mergedRules[foundKey] = mergedRule;\n }\n\n if (isRulesMerged) {\n validateThresholdNumberIntervals(mergedRules, metricType);\n }\n\n return {\n rules: mergedRules,\n };\n}\n"],"names":["ThresholdConfigFormatError","stringifyEntityRef","validateThresholdsForMetric","isError","validateThresholdNumberIntervals"],"mappings":";;;;;;AA6BA,MAAM,8BAAA,GAAiC,CAAC,QAAA,KACtC,CAAA,aAAA,EAAgB,QAAQ,CAAA,kBAAA,CAAA;AAK1B,SAAS,+BAAA,CACP,QACA,QAAA,EACiB;AACjB,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,EAAU,WAAA,IAAe,EAAC;AACrD,EAAA,MAAM,MAAA,GAAS,+BAA+B,QAAQ,CAAA;AACtD,EAAA,MAAM,YAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,CAAC,aAAA,EAAe,UAAU,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACrE,IAAA,IAAI,aAAA,CAAc,UAAA,CAAW,MAAM,CAAA,IAAK,UAAA,EAAY;AAClD,MAAA,MAAM,GAAA,GAAM,aAAA,CAAc,SAAA,CAAU,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,SAAA,CAAU,IAAA,CAAK,EAAE,GAAA,EAAK,UAAA,EAAY,CAAA;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,OAAO,SAAA;AACT;AAEO,SAAS,8BAAA,CACd,MAAA,EACA,MAAA,EACA,cAAA,EACiB;AACjB,EAAA,IAAI,aAAA,GAAgB,KAAA;AAEpB,EAAA,MAAM,gBAAA,GAAmB,kBAAkB,MAAA,CAAO,UAAA;AAClD,EAAA,MAAM,aAAa,MAAA,CAAO,IAAA;AAC1B,EAAA,MAAM,WAAW,MAAA,CAAO,EAAA;AACxB,EAAA,MAAM,0BAAA,GAA6B,+BAAA;AAAA,IACjC,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAAc,CAAC,GAAG,gBAAA,CAAiB,KAAK,CAAA;AAC9C,EAAA,KAAA,MAAW,YAAY,0BAAA,EAA4B;AACjD,IAAA,MAAM,WAAW,WAAA,CAAY,SAAA,CAAU,UAAQ,IAAA,CAAK,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAA,EAAI;AACnB,MAAA,MAAM,IAAIA,uDAAA;AAAA,QACR,CAAA,mBAAA,EAAsBC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAA,CAAK,SAAA;AAAA,UACtB;AAAA,SACD,CAAA,SAAA,EAAY,QAAQ,CAAA,sBAAA,EAAyB,SAAS,GAAG,CAAA;AAAA,OAC5D;AAAA,IACF;AAEA,IAAA,MAAM,aAA4B,EAAE,GAAG,YAAY,QAAQ,CAAA,EAAG,GAAG,QAAA,EAAS;AAC1E,IAAA,IAAI;AACF,MAAAC,wDAAA,CAA4B,EAAE,KAAA,EAAO,CAAC,UAAU,CAAA,IAAK,UAAU,CAAA;AAE/D,MAAA,IAAI,CAAC,eAAe,aAAA,GAAgB,IAAA;AAAA,IACtC,SAAS,CAAA,EAAG;AACV,MAAA,IAAIC,cAAA,CAAQ,CAAC,CAAA,EAAG;AACd,QAAA,MAAM,IAAIH,uDAAA;AAAA,UACR,CAAA,8BAAA,EAAiC,8BAAA;AAAA,YAC/B;AAAA,WACD,CAAA,EAAG,QAAA,CAAS,GAAG,CAAA,EAAA,EACd,QAAA,CAAS,UACX,CAAA,aAAA,EAAgBC,+BAAA,CAAmB,MAAM,CAAC,CAAA,GAAA,EAAM,EAAE,OAAO,CAAA;AAAA,SAC3D;AAAA,MACF;AACA,MAAA,MAAM,CAAA;AAAA,IACR;AAEA,IAAA,WAAA,CAAY,QAAQ,CAAA,GAAI,UAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,aAAA,EAAe;AACjB,IAAAG,6DAAA,CAAiC,aAAa,UAAU,CAAA;AAAA,EAC1D;AAEA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO;AAAA,GACT;AACF;;;;"}
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+
5
+ function getProviderLocalConfigKey(providerId, datasourceId) {
6
+ return providerId.slice(datasourceId.length + 1);
7
+ }
8
+ function getMetricLocalConfigKey(metricId, datasourceId) {
9
+ return metricId.slice(datasourceId.length + 1);
10
+ }
11
+ function getProviderThresholdsConfigPath(datasourceId, providerId) {
12
+ const providerKey = getProviderLocalConfigKey(providerId, datasourceId);
13
+ return `scorecard.metricProviders.${datasourceId}.${providerKey}.thresholds`;
14
+ }
15
+ function getMetricThresholdsConfigPath(datasourceId, providerId, metricId) {
16
+ const providerKey = getProviderLocalConfigKey(providerId, datasourceId);
17
+ const metricKey = getMetricLocalConfigKey(metricId, datasourceId);
18
+ return `scorecard.metricProviders.${datasourceId}.${providerKey}.metrics.${metricKey}.thresholds`;
19
+ }
20
+ function getProviderScheduleConfigPath(datasourceId, providerId) {
21
+ const providerKey = getProviderLocalConfigKey(providerId, datasourceId);
22
+ return `scorecard.metricProviders.${datasourceId}.${providerKey}.schedule`;
23
+ }
24
+ function resolveScheduleFromConfig(config, datasourceId, providerId) {
25
+ const schedulePath = getProviderScheduleConfigPath(datasourceId, providerId);
26
+ if (!config.has(schedulePath)) {
27
+ return void 0;
28
+ }
29
+ return backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
30
+ config.getConfig(schedulePath)
31
+ );
32
+ }
33
+ function resolveThresholdsConfigPath(config, datasourceId, providerId, metricId) {
34
+ const paths = [
35
+ getMetricThresholdsConfigPath(datasourceId, providerId, metricId),
36
+ getProviderThresholdsConfigPath(datasourceId, providerId)
37
+ ];
38
+ return paths.find((path) => config.has(path));
39
+ }
40
+
41
+ exports.getMetricLocalConfigKey = getMetricLocalConfigKey;
42
+ exports.getMetricThresholdsConfigPath = getMetricThresholdsConfigPath;
43
+ exports.getProviderLocalConfigKey = getProviderLocalConfigKey;
44
+ exports.getProviderScheduleConfigPath = getProviderScheduleConfigPath;
45
+ exports.getProviderThresholdsConfigPath = getProviderThresholdsConfigPath;
46
+ exports.resolveScheduleFromConfig = resolveScheduleFromConfig;
47
+ exports.resolveThresholdsConfigPath = resolveThresholdsConfigPath;
48
+ //# sourceMappingURL=metricProviderConfigKeys.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricProviderConfigKeys.cjs.js","sources":["../../src/utils/metricProviderConfigKeys.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 type { Config } from '@backstage/config';\nimport {\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n type SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\n\n/**\n * Local config key for a provider under `scorecard.metricProviders.<datasource>.<key>`.\n * Provider IDs are validated at registry startup as `<datasource>.<providerName>`.\n */\nexport function getProviderLocalConfigKey(\n providerId: string,\n datasourceId: string,\n): string {\n return providerId.slice(datasourceId.length + 1);\n}\n\n/**\n * Local config key for a metric under\n * `scorecard.metricProviders.<datasource>.<providerName>.metrics.<key>`.\n * Metric IDs are validated at registry startup as `<datasource>.<metricName>`.\n */\nexport function getMetricLocalConfigKey(\n metricId: string,\n datasourceId: string,\n): string {\n return metricId.slice(datasourceId.length + 1);\n}\n\n/** Get provider thresholds config key under\n * `scorecard.metricProviders.<datasource>.<providerName>.thresholds`\n */\nexport function getProviderThresholdsConfigPath(\n datasourceId: string,\n providerId: string,\n): string {\n const providerKey = getProviderLocalConfigKey(providerId, datasourceId);\n return `scorecard.metricProviders.${datasourceId}.${providerKey}.thresholds`;\n}\n\n/** Get metric thresholds config key under\n * `scorecard.metricProviders.<datasource>.<providerName>.metrics.<metricName>.thresholds`\n */\nexport function getMetricThresholdsConfigPath(\n datasourceId: string,\n providerId: string,\n metricId: string,\n): string {\n const providerKey = getProviderLocalConfigKey(providerId, datasourceId);\n const metricKey = getMetricLocalConfigKey(metricId, datasourceId);\n return (\n `scorecard.metricProviders.${datasourceId}.${providerKey}` +\n `.metrics.${metricKey}.thresholds`\n );\n}\n\n/** Get provider schedule config key under `scorecard.metricProviders.<datasource>.<providerName>.schedule` */\nexport function getProviderScheduleConfigPath(\n datasourceId: string,\n providerId: string,\n): string {\n const providerKey = getProviderLocalConfigKey(providerId, datasourceId);\n return `scorecard.metricProviders.${datasourceId}.${providerKey}.schedule`;\n}\n\n/**\n * Resolves the provider schedule from config.\n * Returns undefined when not set (caller uses the default schedule).\n */\nexport function resolveScheduleFromConfig(\n config: Config,\n datasourceId: string,\n providerId: string,\n): SchedulerServiceTaskScheduleDefinition | undefined {\n const schedulePath = getProviderScheduleConfigPath(datasourceId, providerId);\n\n if (!config.has(schedulePath)) {\n return undefined;\n }\n\n return readSchedulerServiceTaskScheduleDefinitionFromConfig(\n config.getConfig(schedulePath),\n );\n}\n\n/**\n * Resolves the thresholds config path for a metric.\n * Most specific wins: metric > provider.\n * Returns undefined when no thresholds are set.\n */\nexport function resolveThresholdsConfigPath(\n config: Config,\n datasourceId: string,\n providerId: string,\n metricId: string,\n): string | undefined {\n const paths = [\n getMetricThresholdsConfigPath(datasourceId, providerId, metricId),\n getProviderThresholdsConfigPath(datasourceId, providerId),\n ];\n return paths.find(path => config.has(path));\n}\n"],"names":["readSchedulerServiceTaskScheduleDefinitionFromConfig"],"mappings":";;;;AA0BO,SAAS,yBAAA,CACd,YACA,YAAA,EACQ;AACR,EAAA,OAAO,UAAA,CAAW,KAAA,CAAM,YAAA,CAAa,MAAA,GAAS,CAAC,CAAA;AACjD;AAOO,SAAS,uBAAA,CACd,UACA,YAAA,EACQ;AACR,EAAA,OAAO,QAAA,CAAS,KAAA,CAAM,YAAA,CAAa,MAAA,GAAS,CAAC,CAAA;AAC/C;AAKO,SAAS,+BAAA,CACd,cACA,UAAA,EACQ;AACR,EAAA,MAAM,WAAA,GAAc,yBAAA,CAA0B,UAAA,EAAY,YAAY,CAAA;AACtE,EAAA,OAAO,CAAA,0BAAA,EAA6B,YAAY,CAAA,CAAA,EAAI,WAAW,CAAA,WAAA,CAAA;AACjE;AAKO,SAAS,6BAAA,CACd,YAAA,EACA,UAAA,EACA,QAAA,EACQ;AACR,EAAA,MAAM,WAAA,GAAc,yBAAA,CAA0B,UAAA,EAAY,YAAY,CAAA;AACtE,EAAA,MAAM,SAAA,GAAY,uBAAA,CAAwB,QAAA,EAAU,YAAY,CAAA;AAChE,EAAA,OACE,CAAA,0BAAA,EAA6B,YAAY,CAAA,CAAA,EAAI,WAAW,YAC5C,SAAS,CAAA,WAAA,CAAA;AAEzB;AAGO,SAAS,6BAAA,CACd,cACA,UAAA,EACQ;AACR,EAAA,MAAM,WAAA,GAAc,yBAAA,CAA0B,UAAA,EAAY,YAAY,CAAA;AACtE,EAAA,OAAO,CAAA,0BAAA,EAA6B,YAAY,CAAA,CAAA,EAAI,WAAW,CAAA,SAAA,CAAA;AACjE;AAMO,SAAS,yBAAA,CACd,MAAA,EACA,YAAA,EACA,UAAA,EACoD;AACpD,EAAA,MAAM,YAAA,GAAe,6BAAA,CAA8B,YAAA,EAAc,UAAU,CAAA;AAE3E,EAAA,IAAI,CAAC,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAOA,qEAAA;AAAA,IACL,MAAA,CAAO,UAAU,YAAY;AAAA,GAC/B;AACF;AAOO,SAAS,2BAAA,CACd,MAAA,EACA,YAAA,EACA,UAAA,EACA,QAAA,EACoB;AACpB,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,6BAAA,CAA8B,YAAA,EAAc,UAAA,EAAY,QAAQ,CAAA;AAAA,IAChE,+BAAA,CAAgC,cAAc,UAAU;AAAA,GAC1D;AACA,EAAA,OAAO,MAAM,IAAA,CAAK,CAAA,IAAA,KAAQ,MAAA,CAAO,GAAA,CAAI,IAAI,CAAC,CAAA;AAC5C;;;;;;;;;;"}
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ function validateProviderId(providerId, datasourceId) {
4
+ const [datasource, providerName, ...rest] = providerId.split(".");
5
+ if (datasource !== datasourceId || !providerName || rest.length > 0) {
6
+ throw new Error(
7
+ `Invalid provider ID '${providerId}', must have format '${datasourceId}.<providerName>' where provider name is not empty`
8
+ );
9
+ }
10
+ }
11
+ function validateMetricId(metricId, datasourceId) {
12
+ const [datasource, metricName, ...rest] = metricId.split(".");
13
+ if (datasource !== datasourceId || !metricName || rest.length > 0) {
14
+ throw new Error(
15
+ `Invalid metric ID '${metricId}', must have format '${datasourceId}.<metricName>' where metric name is not empty`
16
+ );
17
+ }
18
+ }
19
+
20
+ exports.validateMetricId = validateMetricId;
21
+ exports.validateProviderId = validateProviderId;
22
+ //# sourceMappingURL=validateMetricProviderIds.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateMetricProviderIds.cjs.js","sources":["../../src/validation/validateMetricProviderIds.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\n/**\n * Validates provider ID format: `<datasource>.<providerName>`.\n */\nexport function validateProviderId(\n providerId: string,\n datasourceId: string,\n): void {\n const [datasource, providerName, ...rest] = providerId.split('.');\n if (datasource !== datasourceId || !providerName || rest.length > 0) {\n throw new Error(\n `Invalid provider ID '${providerId}', must have format ` +\n `'${datasourceId}.<providerName>' where provider name is not empty`,\n );\n }\n}\n\n/**\n * Validates metric ID format: must be `<datasource>.<metricName>`.\n */\nexport function validateMetricId(metricId: string, datasourceId: string): void {\n const [datasource, metricName, ...rest] = metricId.split('.');\n if (datasource !== datasourceId || !metricName || rest.length > 0) {\n throw new Error(\n `Invalid metric ID '${metricId}', must have format ` +\n `'${datasourceId}.<metricName>' where metric name is not empty`,\n );\n }\n}\n"],"names":[],"mappings":";;AAmBO,SAAS,kBAAA,CACd,YACA,YAAA,EACM;AACN,EAAA,MAAM,CAAC,YAAY,YAAA,EAAc,GAAG,IAAI,CAAA,GAAI,UAAA,CAAW,MAAM,GAAG,CAAA;AAChE,EAAA,IAAI,eAAe,YAAA,IAAgB,CAAC,YAAA,IAAgB,IAAA,CAAK,SAAS,CAAA,EAAG;AACnE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,qBAAA,EAAwB,UAAU,CAAA,qBAAA,EAC5B,YAAY,CAAA,iDAAA;AAAA,KACpB;AAAA,EACF;AACF;AAKO,SAAS,gBAAA,CAAiB,UAAkB,YAAA,EAA4B;AAC7E,EAAA,MAAM,CAAC,YAAY,UAAA,EAAY,GAAG,IAAI,CAAA,GAAI,QAAA,CAAS,MAAM,GAAG,CAAA;AAC5D,EAAA,IAAI,eAAe,YAAA,IAAgB,CAAC,UAAA,IAAc,IAAA,CAAK,SAAS,CAAA,EAAG;AACjE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,mBAAA,EAAsB,QAAQ,CAAA,qBAAA,EACxB,YAAY,CAAA,6CAAA;AAAA,KACpB;AAAA,EACF;AACF;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "@backstage/plugin-catalog-node": "^2.2.2",
48
48
  "@backstage/plugin-permission-common": "^0.9.9",
49
49
  "@backstage/plugin-permission-node": "^0.11.1",
50
- "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^3.0.1",
51
- "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^3.0.1",
50
+ "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^4.0.0",
51
+ "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^4.0.0",
52
52
  "express": "^4.17.1",
53
53
  "express-promise-router": "^4.1.0",
54
54
  "knex": "^3.1.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"mergeEntityAndProviderThresholds.cjs.js","sources":["../../src/utils/mergeEntityAndProviderThresholds.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 { stringifyEntityRef, type Entity } from '@backstage/catalog-model';\nimport type {\n Metric,\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n validateThresholdsForMetric,\n ThresholdConfigFormatError,\n validateThresholdNumberIntervals,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isError } from '@backstage/errors';\n\nconst thresholdRulesAnnotationPrefix = (providerId: string) =>\n `scorecard.io/${providerId}.thresholds.rules.`;\n\n/**\n * Extract threshold override rules from entity annotations for a given provider, doesn't validate rules.\n */\nfunction parseEntityAnnotationThresholds(\n entity: Entity,\n providerId: string,\n): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = thresholdRulesAnnotationPrefix(providerId);\n const overrides: ThresholdRule[] = [];\n\n for (const [annotationKey, expression] of Object.entries(annotations)) {\n if (annotationKey.startsWith(prefix) && expression) {\n const key = annotationKey.substring(prefix.length);\n overrides.push({ key, expression });\n }\n }\n\n return overrides;\n}\n\nexport function mergeEntityAndProviderThresholds(\n entity: Entity,\n metric: Metric,\n providerId: string,\n baseThresholds?: ThresholdConfig,\n): ThresholdConfig {\n let isRulesMerged = false;\n\n const providerThresholds = baseThresholds ?? metric.thresholds;\n const metricType = metric.type;\n const entityAnnotationThresholds = parseEntityAnnotationThresholds(\n entity,\n providerId,\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityAnnotationThresholds) {\n const foundKey = mergedRules.findIndex(rule => rule.key === override.key);\n if (foundKey === -1) {\n throw new ThresholdConfigFormatError(\n `Unable to override ${stringifyEntityRef(\n entity,\n )} thresholds by ${JSON.stringify(\n override,\n )}, metric provider ${providerId} does not support key ${override.key}`,\n );\n }\n\n const mergedRule: ThresholdRule = { ...mergedRules[foundKey], ...override };\n try {\n validateThresholdsForMetric({ rules: [mergedRule] }, metricType);\n\n if (!isRulesMerged) isRulesMerged = true;\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${thresholdRulesAnnotationPrefix(\n providerId,\n )}${override.key}: ${\n override.expression\n }' in entity '${stringifyEntityRef(entity)}': ${e.message}`,\n );\n }\n throw e;\n }\n\n mergedRules[foundKey] = mergedRule;\n }\n\n if (isRulesMerged) {\n validateThresholdNumberIntervals(mergedRules, metricType);\n }\n\n return {\n rules: mergedRules,\n };\n}\n"],"names":["ThresholdConfigFormatError","stringifyEntityRef","validateThresholdsForMetric","isError","validateThresholdNumberIntervals"],"mappings":";;;;;;AA6BA,MAAM,8BAAA,GAAiC,CAAC,UAAA,KACtC,CAAA,aAAA,EAAgB,UAAU,CAAA,kBAAA,CAAA;AAK5B,SAAS,+BAAA,CACP,QACA,UAAA,EACiB;AACjB,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,EAAU,WAAA,IAAe,EAAC;AACrD,EAAA,MAAM,MAAA,GAAS,+BAA+B,UAAU,CAAA;AACxD,EAAA,MAAM,YAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,CAAC,aAAA,EAAe,UAAU,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACrE,IAAA,IAAI,aAAA,CAAc,UAAA,CAAW,MAAM,CAAA,IAAK,UAAA,EAAY;AAClD,MAAA,MAAM,GAAA,GAAM,aAAA,CAAc,SAAA,CAAU,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,SAAA,CAAU,IAAA,CAAK,EAAE,GAAA,EAAK,UAAA,EAAY,CAAA;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,OAAO,SAAA;AACT;AAEO,SAAS,gCAAA,CACd,MAAA,EACA,MAAA,EACA,UAAA,EACA,cAAA,EACiB;AACjB,EAAA,IAAI,aAAA,GAAgB,KAAA;AAEpB,EAAA,MAAM,kBAAA,GAAqB,kBAAkB,MAAA,CAAO,UAAA;AACpD,EAAA,MAAM,aAAa,MAAA,CAAO,IAAA;AAC1B,EAAA,MAAM,0BAAA,GAA6B,+BAAA;AAAA,IACjC,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAAc,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,0BAAA,EAA4B;AACjD,IAAA,MAAM,WAAW,WAAA,CAAY,SAAA,CAAU,UAAQ,IAAA,CAAK,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAA,EAAI;AACnB,MAAA,MAAM,IAAIA,uDAAA;AAAA,QACR,CAAA,mBAAA,EAAsBC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAA,CAAK,SAAA;AAAA,UACtB;AAAA,SACD,CAAA,kBAAA,EAAqB,UAAU,CAAA,sBAAA,EAAyB,SAAS,GAAG,CAAA;AAAA,OACvE;AAAA,IACF;AAEA,IAAA,MAAM,aAA4B,EAAE,GAAG,YAAY,QAAQ,CAAA,EAAG,GAAG,QAAA,EAAS;AAC1E,IAAA,IAAI;AACF,MAAAC,wDAAA,CAA4B,EAAE,KAAA,EAAO,CAAC,UAAU,CAAA,IAAK,UAAU,CAAA;AAE/D,MAAA,IAAI,CAAC,eAAe,aAAA,GAAgB,IAAA;AAAA,IACtC,SAAS,CAAA,EAAG;AACV,MAAA,IAAIC,cAAA,CAAQ,CAAC,CAAA,EAAG;AACd,QAAA,MAAM,IAAIH,uDAAA;AAAA,UACR,CAAA,8BAAA,EAAiC,8BAAA;AAAA,YAC/B;AAAA,WACD,CAAA,EAAG,QAAA,CAAS,GAAG,CAAA,EAAA,EACd,QAAA,CAAS,UACX,CAAA,aAAA,EAAgBC,+BAAA,CAAmB,MAAM,CAAC,CAAA,GAAA,EAAM,EAAE,OAAO,CAAA;AAAA,SAC3D;AAAA,MACF;AACA,MAAA,MAAM,CAAA;AAAA,IACR;AAEA,IAAA,WAAA,CAAY,QAAQ,CAAA,GAAI,UAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,aAAA,EAAe;AACjB,IAAAG,6DAAA,CAAiC,aAAa,UAAU,CAAA;AAAA,EAC1D;AAEA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO;AAAA,GACT;AACF;;;;"}