@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.7.3 → 2.7.5

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,30 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 2.7.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 5115044: ### Threshold validation
8
+
9
+ Implemented threshold interval validation for Scorecard: joint coverage on the real line, gap detection and error messages, and overlap handling versus rule order (including aggregation KPI `options.thresholds` for `average`).
10
+
11
+ ### Aggregation
12
+
13
+ For **`type: average`** aggregations, **`result.averageScore`** returned by **`GET /aggregations/:aggregationId`** (and the same shape wherever it appears) is a **portfolio percentage in \[0, 100\]** with **one decimal place** — the same scale used for **`options.thresholds`** evaluation and the homepage donut.
14
+
15
+ Previously **`averageScore`** was a **normalized ratio in \[0, 1\]** (rounded to **three** decimal places). Any consumer that treated the old value as a fraction and multiplied by **100** for display, or compared it to thresholds on a 0–100 scale without converting, must **stop scaling**: use **`averageScore`** directly as the percentage. If you stored historical API payloads, recompute or re-fetch rather than assuming the old fractional scale.
16
+
17
+ - Updated dependencies [5115044]
18
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.5
19
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.5
20
+
21
+ ## 2.7.4
22
+
23
+ ### Patch Changes
24
+
25
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.4
26
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.4
27
+
3
28
  ## 2.7.3
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -116,9 +116,9 @@ Thresholds define conditions to assign metric values to specific visual categori
116
116
  - **App Configuration**: Override defaults through `app-config.yaml`
117
117
  - **Entity Annotations**: Override specific thresholds per entity using catalog annotations
118
118
 
119
- Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`).
119
+ Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`). For **number** metrics, configurations loaded through validated paths must cover the **entire real line** when two or more rules are defined (no gaps between intervals); **`average`** KPI **`options.thresholds`** follow the same rule.
120
120
 
121
- For comprehensive threshold configuration guide, examples, best practices, and **aggregation KPI result thresholds** for **`type: average`**, see [thresholds.md](./docs/thresholds.md).
121
+ For comprehensive threshold configuration guide, examples, best practices, interval validation, and **aggregation KPI result thresholds** for **`type: average`**, see [thresholds.md](./docs/thresholds.md).
122
122
 
123
123
  ## Aggregation KPIs (homepage and `GET /aggregations`)
124
124
 
@@ -154,20 +154,20 @@ scorecard:
154
154
  expression: '>=75'
155
155
  color: success.main
156
156
  - key: warning
157
- expression: '10-74'
157
+ expression: '10-75'
158
158
  color: warning.main
159
159
  - key: error
160
160
  expression: '<10'
161
161
  color: error.main
162
162
  ```
163
163
 
164
- | Field | Description |
165
- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
- | `title` | Display title for this aggregation (returned in API metadata). |
167
- | `description` | Display description for this aggregation. |
168
- | `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `average` (normalized weighted score). |
169
- | `metricId` | Metric provider id used to load thresholds and compute counts. |
170
- | `options` | Optional for `statusGrouped`. **Required** for `average`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-average-type)); evaluated on **`averageScore × 100`**; first match sets **`aggregationChartDisplayColor`**. The API includes **`averageScore`** (ratio **0–1**, **one decimal**), **`averageWeightedSum`**, **`averageMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). |
164
+ | Field | Description |
165
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
166
+ | `title` | Display title for this aggregation (returned in API metadata). |
167
+ | `description` | Display description for this aggregation. |
168
+ | `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `average` (normalized weighted score). |
169
+ | `metricId` | Metric provider id used to load thresholds and compute counts. |
170
+ | `options` | Optional for `statusGrouped`. **Required** for `average`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-average-type)); evaluated on **`averageScore`** (**0–100** portfolio percentage, **one decimal**); first match sets **`aggregationChartDisplayColor`**. The API includes **`averageScore`**, **`averageWeightedSum`**, **`averageMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). |
171
171
 
172
172
  - **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
173
173
  - If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
@@ -12,7 +12,7 @@ const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS = {
12
12
  },
13
13
  {
14
14
  key: "warning",
15
- expression: "30-79",
15
+ expression: "30-80",
16
16
  color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.WARNING
17
17
  },
18
18
  {
@@ -1 +1 @@
1
- {"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ThresholdConfig,\n ScorecardThresholdRuleColors,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n\n/**\n * Default applied by `AverageAggregationStrategy` when `options.thresholds` is omitted\n * from app-config. Higher headline percentage (0–100) = better. Evaluated in order; first match wins.\n */\nexport const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS: ThresholdConfig = {\n rules: [\n {\n key: 'success',\n expression: '>=80',\n color: ScorecardThresholdRuleColors.SUCCESS,\n },\n {\n key: 'warning',\n expression: '30-79',\n color: ScorecardThresholdRuleColors.WARNING,\n },\n {\n key: 'error',\n expression: '<30',\n color: ScorecardThresholdRuleColors.ERROR,\n },\n ],\n};\n"],"names":["ScorecardThresholdRuleColors"],"mappings":";;;;AAqBO,MAAM,4BACX,GAAA;AAMK,MAAM,qCAAyD,GAAA;AAAA,EACpE,KAAO,EAAA;AAAA,IACL;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,MAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,OAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,OAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA;AACtC;AAEJ;;;;;"}
1
+ {"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ThresholdConfig,\n ScorecardThresholdRuleColors,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n\n/**\n * Default applied by `AverageAggregationStrategy` when `options.thresholds` is omitted\n * from app-config. Higher headline percentage (0–100) = better. Evaluated in order; first match wins.\n */\nexport const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS: ThresholdConfig = {\n rules: [\n {\n key: 'success',\n expression: '>=80',\n color: ScorecardThresholdRuleColors.SUCCESS,\n },\n {\n key: 'warning',\n expression: '30-80',\n color: ScorecardThresholdRuleColors.WARNING,\n },\n {\n key: 'error',\n expression: '<30',\n color: ScorecardThresholdRuleColors.ERROR,\n },\n ],\n};\n"],"names":["ScorecardThresholdRuleColors"],"mappings":";;;;AAqBO,MAAM,4BACX,GAAA;AAMK,MAAM,qCAAyD,GAAA;AAAA,EACpE,KAAO,EAAA;AAAA,IACL;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,MAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,OAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,OAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA;AACtC;AAEJ;;;;;"}
@@ -4,6 +4,7 @@ class DatabaseMetricValues {
4
4
  constructor(dbClient) {
5
5
  this.dbClient = dbClient;
6
6
  }
7
+ dbClient;
7
8
  tableName = "metric_values";
8
9
  /**
9
10
  * Insert multiple metric values
@@ -1 +1 @@
1
- {"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.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 { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n if (catalog_entity_refs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n // `value` is a JSON column. Depending on database/driver, a \"missing\" metric value can\n // arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).\n const metricValueIsMissingExpr =\n \"(value IS NULL OR CAST(value AS TEXT) = 'null')\";\n\n // One round-trip for latest-row count, calculation-error count, and max timestamp\n // (same latest-id set as the status breakdown query below).\n const statsRow = await this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .select(\n this.dbClient.raw('COUNT(*) as latest_row_count'),\n this.dbClient.raw(\n `SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`,\n ),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const latestRowCount = Number(\n (statsRow as { latest_row_count?: string | number } | undefined)\n ?.latest_row_count ?? 0,\n );\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const calculation_error_count = Number(\n (statsRow as { calculation_error_count?: string | number } | undefined)\n ?.calculation_error_count ?? 0,\n );\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: unknown): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n }\n if (typeof timestamp === 'number' || typeof timestamp === 'string') {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n const maxTimestampAllLatest = normalizeTimestamp(\n (statsRow as { max_timestamp?: unknown })?.max_timestamp,\n );\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereRaw(`NOT ${metricValueIsMissingExpr}`)\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return {\n metric_id,\n total: 0,\n max_timestamp: maxTimestampAllLatest,\n statusCounts: {},\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n const mergedMax =\n maxTimestampAllLatest.getTime() >= maxTimestamp.getTime()\n ? maxTimestampAllLatest\n : maxTimestamp;\n\n return {\n metric_id,\n total,\n max_timestamp: mergedMax,\n statusCounts,\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.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 { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n if (catalog_entity_refs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n // `value` is a JSON column. Depending on database/driver, a \"missing\" metric value can\n // arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).\n const metricValueIsMissingExpr =\n \"(value IS NULL OR CAST(value AS TEXT) = 'null')\";\n\n // One round-trip for latest-row count, calculation-error count, and max timestamp\n // (same latest-id set as the status breakdown query below).\n const statsRow = await this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .select(\n this.dbClient.raw('COUNT(*) as latest_row_count'),\n this.dbClient.raw(\n `SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`,\n ),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const latestRowCount = Number(\n (statsRow as { latest_row_count?: string | number } | undefined)\n ?.latest_row_count ?? 0,\n );\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const calculation_error_count = Number(\n (statsRow as { calculation_error_count?: string | number } | undefined)\n ?.calculation_error_count ?? 0,\n );\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: unknown): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n }\n if (typeof timestamp === 'number' || typeof timestamp === 'string') {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n const maxTimestampAllLatest = normalizeTimestamp(\n (statsRow as { max_timestamp?: unknown })?.max_timestamp,\n );\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereRaw(`NOT ${metricValueIsMissingExpr}`)\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return {\n metric_id,\n total: 0,\n max_timestamp: maxTimestampAllLatest,\n statusCounts: {},\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n const mergedMax =\n maxTimestampAllLatest.getTime() >= maxTimestamp.getTime()\n ? maxTimestampAllLatest\n : maxTimestamp;\n\n return {\n metric_id,\n total,\n max_timestamp: mergedMax,\n statusCounts,\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@ class AggregatedMetricLoader {
6
6
  constructor(database) {
7
7
  this.database = database;
8
8
  }
9
+ database;
9
10
  async loadStatusGroupedMetricByEntityRefs(entityRefs, metricId) {
10
11
  if (entityRefs.length === 0) {
11
12
  return mappers.AggregatedMetricMapper.toAggregatedMetric();
@@ -1 +1 @@
1
- {"version":3,"file":"AggregatedMetricLoader.cjs.js","sources":["../../../src/service/aggregations/AggregatedMetricLoader.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 { AggregatedMetric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport { AggregatedMetricMapper } from '../mappers';\n\nexport class AggregatedMetricLoader {\n constructor(private readonly database: DatabaseMetricValues) {}\n\n async loadStatusGroupedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length === 0) {\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAoBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,YAA6B,QAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAAiC,EAE9D,MAAM,mCACJ,CAAA,UAAA,EACA,QAC2B,EAAA;AAC3B,IAAI,IAAA,UAAA,CAAW,WAAW,CAAG,EAAA;AAC3B,MAAA,OAAOA,+BAAuB,kBAAmB,EAAA;AAAA;AAGnD,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,QAAS,CAAA,gCAAA;AAAA,MAClB,UAAA;AAAA,MACA;AAAA,KACF;AAEF,IAAO,OAAAA,8BAAA,CAAuB,mBAAmB,gBAAgB,CAAA;AAAA;AAErE;;;;"}
1
+ {"version":3,"file":"AggregatedMetricLoader.cjs.js","sources":["../../../src/service/aggregations/AggregatedMetricLoader.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 { AggregatedMetric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport { AggregatedMetricMapper } from '../mappers';\n\nexport class AggregatedMetricLoader {\n constructor(private readonly database: DatabaseMetricValues) {}\n\n async loadStatusGroupedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length === 0) {\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAoBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,YAA6B,QAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAAiC,EAAjC,QAAA;AAAA,EAE7B,MAAM,mCACJ,CAAA,UAAA,EACA,QAC2B,EAAA;AAC3B,IAAI,IAAA,UAAA,CAAW,WAAW,CAAG,EAAA;AAC3B,MAAA,OAAOA,+BAAuB,kBAAmB,EAAA;AAAA;AAGnD,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,QAAS,CAAA,gCAAA;AAAA,MAClB,UAAA;AAAA,MACA;AAAA,KACF;AAEF,IAAO,OAAAA,8BAAA,CAAuB,mBAAmB,gBAAgB,CAAA;AAAA;AAErE;;;;"}
@@ -9,6 +9,8 @@ class AverageAggregationStrategy {
9
9
  this.loader = loader;
10
10
  this.logger = logger;
11
11
  }
12
+ loader;
13
+ logger;
12
14
  async aggregate({
13
15
  entityRefs,
14
16
  metric,
@@ -42,14 +44,13 @@ class AverageAggregationStrategy {
42
44
  thresholds.rules,
43
45
  weightedSum
44
46
  );
45
- const scorePercent = averageScore * 100;
46
47
  const aggregationChartDisplayColor = this.getAggregationChartDisplayColor(
47
- scorePercent,
48
+ averageScore,
48
49
  headlineThresholds
49
50
  );
50
51
  if (!aggregationChartDisplayColor) {
51
52
  throw new Error(
52
- `The color for percentage '${scorePercent}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`
53
+ `The color for percentage '${averageScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`
53
54
  );
54
55
  }
55
56
  const result = {
@@ -100,8 +101,7 @@ class AverageAggregationStrategy {
100
101
  const statusScoresValues = rules.map((r) => statusScores[r.key] ?? 0);
101
102
  const maxScore = Math.max(0, ...statusScoresValues);
102
103
  const maxPossibleScore = maxScore * numberOfEntities;
103
- const precision = 1e3;
104
- const averageScore = numberOfEntities > 0 && maxPossibleScore > 0 ? Math.round(weightedSum / maxPossibleScore * precision) / precision : 0;
104
+ const averageScore = numberOfEntities > 0 && maxPossibleScore > 0 ? Math.round(weightedSum / maxPossibleScore * 1e3) / 10 : 0;
105
105
  return { averageScore, maxPossibleScore };
106
106
  }
107
107
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AverageAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/AverageAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AggregatedMetric,\n type AggregatedMetricAverageResult,\n type AggregatedMetricResult,\n type ThresholdConfig,\n ThresholdRule,\n type AggregationConfigOptions,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\n\nexport class AverageAggregationStrategy implements AggregationStrategy {\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n const { options } = aggregationConfig;\n\n if (!options?.statusScores) {\n throw new Error(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores\" is required for average aggregation`,\n );\n }\n\n if (!options.thresholds) {\n this.logger.info(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds\" is not configured for average aggregation; ` +\n 'using the default 0–100% health scale (higher is better).',\n );\n }\n\n const headlineThresholds =\n options.thresholds ?? DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n options.statusScores,\n metric.id,\n );\n\n const { averageScore, maxPossibleScore } = this.prepareScoreValues(\n aggregatedMetric.total,\n options.statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const scorePercent = averageScore * 100;\n\n const aggregationChartDisplayColor = this.getAggregationChartDisplayColor(\n scorePercent,\n headlineThresholds,\n );\n\n if (!aggregationChartDisplayColor) {\n throw new Error(\n `The color for percentage '${scorePercent}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n );\n }\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: options.statusScores[rule.key] ?? 0,\n })),\n thresholds,\n averageScore,\n averageWeightedSum: weightedSum,\n averageMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as AggregatedMetricAverageResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: AggregationConfigOptions['statusScores'],\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for average aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private getAggregationChartDisplayColor(\n scorePercent: number,\n thresholds: ThresholdConfig,\n ): string | undefined {\n const thresholdEvaluator = new ThresholdEvaluator();\n\n const matchedThresholdKey = thresholdEvaluator.getFirstMatchingThreshold(\n scorePercent,\n 'number',\n thresholds,\n );\n\n return thresholds.rules.find(r => r.key === matchedThresholdKey)?.color;\n }\n\n private prepareScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: AggregationConfigOptions['statusScores'],\n rules: ThresholdRule[],\n weightedSum: number,\n ): { averageScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const precision = 1000;\n\n const averageScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * precision) / precision\n : 0;\n\n return { averageScore, maxPossibleScore };\n }\n}\n"],"names":["DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS","AggregatedMetricMapper","ThresholdEvaluator"],"mappings":"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"}
1
+ {"version":3,"file":"AverageAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/AverageAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AggregatedMetric,\n type AggregatedMetricAverageResult,\n type AggregatedMetricResult,\n type ThresholdConfig,\n ThresholdRule,\n type AggregationConfigOptions,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\n\nexport class AverageAggregationStrategy implements AggregationStrategy {\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n const { options } = aggregationConfig;\n\n if (!options?.statusScores) {\n throw new Error(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores\" is required for average aggregation`,\n );\n }\n\n if (!options.thresholds) {\n this.logger.info(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds\" is not configured for average aggregation; ` +\n 'using the default 0–100% health scale (higher is better).',\n );\n }\n\n const headlineThresholds =\n options.thresholds ?? DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n options.statusScores,\n metric.id,\n );\n\n const { averageScore, maxPossibleScore } = this.prepareScoreValues(\n aggregatedMetric.total,\n options.statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const aggregationChartDisplayColor = this.getAggregationChartDisplayColor(\n averageScore,\n headlineThresholds,\n );\n\n if (!aggregationChartDisplayColor) {\n throw new Error(\n `The color for percentage '${averageScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n );\n }\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: options.statusScores[rule.key] ?? 0,\n })),\n thresholds,\n averageScore,\n averageWeightedSum: weightedSum,\n averageMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as AggregatedMetricAverageResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: AggregationConfigOptions['statusScores'],\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for average aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private getAggregationChartDisplayColor(\n scorePercent: number,\n thresholds: ThresholdConfig,\n ): string | undefined {\n const thresholdEvaluator = new ThresholdEvaluator();\n\n const matchedThresholdKey = thresholdEvaluator.getFirstMatchingThreshold(\n scorePercent,\n 'number',\n thresholds,\n );\n\n return thresholds.rules.find(r => r.key === matchedThresholdKey)?.color;\n }\n\n private prepareScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: AggregationConfigOptions['statusScores'],\n rules: ThresholdRule[],\n weightedSum: number,\n ): { averageScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const averageScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * 1000) / 10\n : 0;\n\n return { averageScore, maxPossibleScore };\n }\n}\n"],"names":["DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS","AggregatedMetricMapper","ThresholdEvaluator"],"mappings":"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"}
@@ -6,6 +6,7 @@ class StatusGroupedAggregationStrategy {
6
6
  constructor(loader) {
7
7
  this.loader = loader;
8
8
  }
9
+ loader;
9
10
  async aggregate(options) {
10
11
  const { entityRefs, metric, thresholds, aggregationConfig } = options;
11
12
  const aggregatedMetric = await this.loader.loadStatusGroupedMetricByEntityRefs(
@@ -1 +1 @@
1
- {"version":3,"file":"StatusGroupedAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/StatusGroupedAggregationStrategy.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 {\n AggregatedMetricResult,\n StatusGroupedAggregationResult,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\n\nexport class StatusGroupedAggregationStrategy implements AggregationStrategy {\n constructor(private readonly loader: AggregatedMetricLoader) {}\n\n async aggregate(\n options: AggregationOptions,\n ): Promise<AggregatedMetricResult> {\n const { entityRefs, metric, thresholds, aggregationConfig } = options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n })),\n thresholds,\n } as StatusGroupedAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAyBO,MAAM,gCAAgE,CAAA;AAAA,EAC3E,YAA6B,MAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAiC,EAE9D,MAAM,UACJ,OACiC,EAAA;AACjC,IAAA,MAAM,EAAE,UAAA,EAAY,MAAQ,EAAA,UAAA,EAAY,mBAAsB,GAAA,OAAA;AAE9D,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,MAAO,CAAA,mCAAA;AAAA,MAChB,UAAA;AAAA,MACA,MAAO,CAAA;AAAA,KACT;AAEF,IAAA,MAAM,MAAS,GAAA;AAAA,MACb,OAAO,gBAAiB,CAAA,KAAA;AAAA,MACxB,WAAW,gBAAiB,CAAA,SAAA;AAAA,MAC5B,oBAAoB,gBAAiB,CAAA,kBAAA;AAAA,MACrC,uBAAuB,gBAAiB,CAAA,qBAAA;AAAA,MACxC,MAAQ,EAAA,UAAA,CAAW,KAAM,CAAA,GAAA,CAAI,CAAS,IAAA,MAAA;AAAA,QACpC,MAAM,IAAK,CAAA,GAAA;AAAA,QACX,KAAO,EAAA,gBAAA,CAAiB,MAAO,CAAA,IAAA,CAAK,GAAG,CAAK,IAAA;AAAA,OAC5C,CAAA,CAAA;AAAA,MACF;AAAA,KACF;AAEA,IAAA,OAAOA,8BAAuB,CAAA,wBAAA;AAAA,MAC5B,MAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF;AAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"StatusGroupedAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/StatusGroupedAggregationStrategy.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 {\n AggregatedMetricResult,\n StatusGroupedAggregationResult,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\n\nexport class StatusGroupedAggregationStrategy implements AggregationStrategy {\n constructor(private readonly loader: AggregatedMetricLoader) {}\n\n async aggregate(\n options: AggregationOptions,\n ): Promise<AggregatedMetricResult> {\n const { entityRefs, metric, thresholds, aggregationConfig } = options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n })),\n thresholds,\n } as StatusGroupedAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAyBO,MAAM,gCAAgE,CAAA;AAAA,EAC3E,YAA6B,MAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAiC,EAAjC,MAAA;AAAA,EAE7B,MAAM,UACJ,OACiC,EAAA;AACjC,IAAA,MAAM,EAAE,UAAA,EAAY,MAAQ,EAAA,UAAA,EAAY,mBAAsB,GAAA,OAAA;AAE9D,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,MAAO,CAAA,mCAAA;AAAA,MAChB,UAAA;AAAA,MACA,MAAO,CAAA;AAAA,KACT;AAEF,IAAA,MAAM,MAAS,GAAA;AAAA,MACb,OAAO,gBAAiB,CAAA,KAAA;AAAA,MACxB,WAAW,gBAAiB,CAAA,SAAA;AAAA,MAC5B,oBAAoB,gBAAiB,CAAA,kBAAA;AAAA,MACrC,uBAAuB,gBAAiB,CAAA,qBAAA;AAAA,MACxC,MAAQ,EAAA,UAAA,CAAW,KAAM,CAAA,GAAA,CAAI,CAAS,IAAA,MAAA;AAAA,QACpC,MAAM,IAAK,CAAA,GAAA;AAAA,QACX,KAAO,EAAA,gBAAA,CAAiB,MAAO,CAAA,IAAA,CAAK,GAAG,CAAK,IAAA;AAAA,OAC5C,CAAA,CAAA;AAAA,MACF;AAAA,KACF;AAEA,IAAA,OAAOA,8BAAuB,CAAA,wBAAA;AAAA,MAC5B,MAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF;AAAA;AAEJ;;;;"}
@@ -18,6 +18,7 @@ function parseEntityAnnotationThresholds(entity, providerId) {
18
18
  return overrides;
19
19
  }
20
20
  function mergeEntityAndProviderThresholds(entity, provider) {
21
+ let isRulesMerged = false;
21
22
  const providerId = provider.getProviderId();
22
23
  const providerThresholds = provider.getMetricThresholds();
23
24
  const providerMetricType = provider.getMetricType();
@@ -40,6 +41,7 @@ function mergeEntityAndProviderThresholds(entity, provider) {
40
41
  const mergedRule = { ...mergedRules[foundKey], ...override };
41
42
  try {
42
43
  backstagePluginScorecardNode.validateThresholdsForMetric({ rules: [mergedRule] }, providerMetricType);
44
+ if (!isRulesMerged) isRulesMerged = true;
43
45
  } catch (e) {
44
46
  if (errors.isError(e)) {
45
47
  throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
@@ -52,6 +54,9 @@ function mergeEntityAndProviderThresholds(entity, provider) {
52
54
  }
53
55
  mergedRules[foundKey] = mergedRule;
54
56
  }
57
+ if (isRulesMerged) {
58
+ backstagePluginScorecardNode.validateThresholdNumberIntervals(mergedRules, providerMetricType);
59
+ }
55
60
  return {
56
61
  rules: mergedRules
57
62
  };
@@ -1 +1 @@
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 ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n type MetricProvider,\n validateThresholdsForMetric,\n ThresholdConfigFormatError,\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 provider: MetricProvider,\n): ThresholdConfig {\n const providerId = provider.getProviderId();\n const providerThresholds = provider.getMetricThresholds();\n const providerMetricType = provider.getMetricType();\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] }, providerMetricType);\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 return {\n rules: mergedRules,\n };\n}\n"],"names":["ThresholdConfigFormatError","stringifyEntityRef","validateThresholdsForMetric","isError"],"mappings":";;;;;;AA4BA,MAAM,8BAAiC,GAAA,CAAC,UACtC,KAAA,CAAA,aAAA,EAAgB,UAAU,CAAA,kBAAA,CAAA;AAK5B,SAAS,+BAAA,CACP,QACA,UACiB,EAAA;AACjB,EAAA,MAAM,WAAc,GAAA,MAAA,CAAO,QAAU,EAAA,WAAA,IAAe,EAAC;AACrD,EAAM,MAAA,MAAA,GAAS,+BAA+B,UAAU,CAAA;AACxD,EAAA,MAAM,YAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,CAAC,aAAe,EAAA,UAAU,KAAK,MAAO,CAAA,OAAA,CAAQ,WAAW,CAAG,EAAA;AACrE,IAAA,IAAI,aAAc,CAAA,UAAA,CAAW,MAAM,CAAA,IAAK,UAAY,EAAA;AAClD,MAAA,MAAM,GAAM,GAAA,aAAA,CAAc,SAAU,CAAA,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,SAAA,CAAU,IAAK,CAAA,EAAE,GAAK,EAAA,UAAA,EAAY,CAAA;AAAA;AACpC;AAGF,EAAO,OAAA,SAAA;AACT;AAEgB,SAAA,gCAAA,CACd,QACA,QACiB,EAAA;AACjB,EAAM,MAAA,UAAA,GAAa,SAAS,aAAc,EAAA;AAC1C,EAAM,MAAA,kBAAA,GAAqB,SAAS,mBAAoB,EAAA;AACxD,EAAM,MAAA,kBAAA,GAAqB,SAAS,aAAc,EAAA;AAClD,EAAA,MAAM,0BAA6B,GAAA,+BAAA;AAAA,IACjC,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,0BAA4B,EAAA;AACjD,IAAA,MAAM,WAAW,WAAY,CAAA,SAAA,CAAU,UAAQ,IAAK,CAAA,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAI,EAAA;AACnB,MAAA,MAAM,IAAIA,uDAAA;AAAA,QACR,CAAsB,mBAAA,EAAAC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAK,CAAA,SAAA;AAAA,UACtB;AAAA,SACD,CAAA,kBAAA,EAAqB,UAAU,CAAA,sBAAA,EAAyB,SAAS,GAAG,CAAA;AAAA,OACvE;AAAA;AAGF,IAAA,MAAM,aAA4B,EAAE,GAAG,YAAY,QAAQ,CAAA,EAAG,GAAG,QAAS,EAAA;AAC1E,IAAI,IAAA;AACF,MAAAC,wDAAA,CAA4B,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,kBAAkB,CAAA;AAAA,aAChE,CAAG,EAAA;AACV,MAAI,IAAAC,cAAA,CAAQ,CAAC,CAAG,EAAA;AACd,QAAA,MAAM,IAAIH,uDAAA;AAAA,UACR,CAAiC,8BAAA,EAAA,8BAAA;AAAA,YAC/B;AAAA,WACD,CAAA,EAAG,QAAS,CAAA,GAAG,CACd,EAAA,EAAA,QAAA,CAAS,UACX,CAAA,aAAA,EAAgBC,+BAAmB,CAAA,MAAM,CAAC,CAAA,GAAA,EAAM,EAAE,OAAO,CAAA;AAAA,SAC3D;AAAA;AAEF,MAAM,MAAA,CAAA;AAAA;AAGR,IAAA,WAAA,CAAY,QAAQ,CAAI,GAAA,UAAA;AAAA;AAG1B,EAAO,OAAA;AAAA,IACL,KAAO,EAAA;AAAA,GACT;AACF;;;;"}
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 ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n type MetricProvider,\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 provider: MetricProvider,\n): ThresholdConfig {\n let isRulesMerged = false;\n\n const providerId = provider.getProviderId();\n const providerThresholds = provider.getMetricThresholds();\n const providerMetricType = provider.getMetricType();\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] }, providerMetricType);\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, providerMetricType);\n }\n\n return {\n rules: mergedRules,\n };\n}\n"],"names":["ThresholdConfigFormatError","stringifyEntityRef","validateThresholdsForMetric","isError","validateThresholdNumberIntervals"],"mappings":";;;;;;AA6BA,MAAM,8BAAiC,GAAA,CAAC,UACtC,KAAA,CAAA,aAAA,EAAgB,UAAU,CAAA,kBAAA,CAAA;AAK5B,SAAS,+BAAA,CACP,QACA,UACiB,EAAA;AACjB,EAAA,MAAM,WAAc,GAAA,MAAA,CAAO,QAAU,EAAA,WAAA,IAAe,EAAC;AACrD,EAAM,MAAA,MAAA,GAAS,+BAA+B,UAAU,CAAA;AACxD,EAAA,MAAM,YAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,CAAC,aAAe,EAAA,UAAU,KAAK,MAAO,CAAA,OAAA,CAAQ,WAAW,CAAG,EAAA;AACrE,IAAA,IAAI,aAAc,CAAA,UAAA,CAAW,MAAM,CAAA,IAAK,UAAY,EAAA;AAClD,MAAA,MAAM,GAAM,GAAA,aAAA,CAAc,SAAU,CAAA,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,SAAA,CAAU,IAAK,CAAA,EAAE,GAAK,EAAA,UAAA,EAAY,CAAA;AAAA;AACpC;AAGF,EAAO,OAAA,SAAA;AACT;AAEgB,SAAA,gCAAA,CACd,QACA,QACiB,EAAA;AACjB,EAAA,IAAI,aAAgB,GAAA,KAAA;AAEpB,EAAM,MAAA,UAAA,GAAa,SAAS,aAAc,EAAA;AAC1C,EAAM,MAAA,kBAAA,GAAqB,SAAS,mBAAoB,EAAA;AACxD,EAAM,MAAA,kBAAA,GAAqB,SAAS,aAAc,EAAA;AAClD,EAAA,MAAM,0BAA6B,GAAA,+BAAA;AAAA,IACjC,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,0BAA4B,EAAA;AACjD,IAAA,MAAM,WAAW,WAAY,CAAA,SAAA,CAAU,UAAQ,IAAK,CAAA,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAI,EAAA;AACnB,MAAA,MAAM,IAAIA,uDAAA;AAAA,QACR,CAAsB,mBAAA,EAAAC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAK,CAAA,SAAA;AAAA,UACtB;AAAA,SACD,CAAA,kBAAA,EAAqB,UAAU,CAAA,sBAAA,EAAyB,SAAS,GAAG,CAAA;AAAA,OACvE;AAAA;AAGF,IAAA,MAAM,aAA4B,EAAE,GAAG,YAAY,QAAQ,CAAA,EAAG,GAAG,QAAS,EAAA;AAC1E,IAAI,IAAA;AACF,MAAAC,wDAAA,CAA4B,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,kBAAkB,CAAA;AAEvE,MAAI,IAAA,CAAC,eAA+B,aAAA,GAAA,IAAA;AAAA,aAC7B,CAAG,EAAA;AACV,MAAI,IAAAC,cAAA,CAAQ,CAAC,CAAG,EAAA;AACd,QAAA,MAAM,IAAIH,uDAAA;AAAA,UACR,CAAiC,8BAAA,EAAA,8BAAA;AAAA,YAC/B;AAAA,WACD,CAAA,EAAG,QAAS,CAAA,GAAG,CACd,EAAA,EAAA,QAAA,CAAS,UACX,CAAA,aAAA,EAAgBC,+BAAmB,CAAA,MAAM,CAAC,CAAA,GAAA,EAAM,EAAE,OAAO,CAAA;AAAA,SAC3D;AAAA;AAEF,MAAM,MAAA,CAAA;AAAA;AAGR,IAAA,WAAA,CAAY,QAAQ,CAAI,GAAA,UAAA;AAAA;AAG1B,EAAA,IAAI,aAAe,EAAA;AACjB,IAAAG,6DAAA,CAAiC,aAAa,kBAAkB,CAAA;AAAA;AAGlE,EAAO,OAAA;AAAA,IACL,KAAO,EAAA;AAAA,GACT;AACF;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
3
- "version": "2.7.3",
3
+ "version": "2.7.5",
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.1.0",
48
48
  "@backstage/plugin-permission-common": "^0.9.7",
49
49
  "@backstage/plugin-permission-node": "^0.10.11",
50
- "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.7.3",
51
- "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.7.3",
50
+ "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.7.5",
51
+ "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.7.5",
52
52
  "express": "^4.17.1",
53
53
  "express-promise-router": "^4.1.0",
54
54
  "knex": "^3.1.0",