@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.3.5 → 2.4.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,18 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 2.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7062658: Introduces custom threshold rule keys and colors that can be configured in `app-config.yaml`.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [7062658]
12
+ - Updated dependencies [dc5e31a]
13
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.4.0
14
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.4.0
15
+
3
16
  ## 2.3.5
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -102,7 +102,7 @@ To use these providers, install the corresponding backend modules:
102
102
 
103
103
  ## Thresholds
104
104
 
105
- Thresholds define conditions that determine which category a metric value belongs to (`error`, `warning`, or `success`). The Scorecard plugin provides multiple ways to configure thresholds:
105
+ Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). The Scorecard plugin provides multiple ways to configure thresholds:
106
106
 
107
107
  - **Provider Defaults**: Metric providers define default thresholds
108
108
  - **App Configuration**: Override defaults through `app-config.yaml`
package/config.d.ts CHANGED
@@ -32,9 +32,14 @@ export interface Config {
32
32
  /** Threshold configuration for the metric */
33
33
  thresholds?: {
34
34
  rules?: Array<{
35
- key: 'error' | 'warning' | 'success';
35
+ key: string;
36
36
  /** Threshold expression - supports: >=, <=, >, <, ==, !=, - (range) */
37
37
  expression: string;
38
+ /**
39
+ * Color for this threshold rule. Can be a theme palette path (e.g., 'error.main')
40
+ * or a direct color value (e.g., '#ADD8E6', 'blue', 'rgb(255,255,0)')
41
+ */
42
+ color?: string;
38
43
  }>;
39
44
  };
40
45
  schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
@@ -33,39 +33,38 @@ class DatabaseMetricValues {
33
33
  * Get aggregated metrics by status for multiple entities and metrics.
34
34
  */
35
35
  async readAggregatedMetricByEntityRefs(catalog_entity_refs, metric_id) {
36
- const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("metric_id", "catalog_entity_ref");
37
- const [row] = await this.dbClient(this.tableName).select("metric_id").count("* as total").max("timestamp as max_timestamp").select(
38
- this.dbClient.raw(
39
- "SUM(CASE WHEN status = 'success' AND value IS NOT NULL THEN 1 ELSE 0 END) as success"
40
- )
41
- ).select(
42
- this.dbClient.raw(
43
- "SUM(CASE WHEN status = 'warning' AND value IS NOT NULL THEN 1 ELSE 0 END) as warning"
44
- )
45
- ).select(
46
- this.dbClient.raw(
47
- "SUM(CASE WHEN status = 'error' AND value IS NOT NULL THEN 1 ELSE 0 END) as error"
48
- )
49
- ).whereIn("id", latestIdsSubquery).whereNotNull("status").whereNotNull("value").groupBy("metric_id");
50
- if (row) {
51
- let maxTimestamp;
52
- if (row.max_timestamp instanceof Date) {
53
- maxTimestamp = row.max_timestamp;
54
- } else if (typeof row.max_timestamp === "number" || typeof row.max_timestamp === "string") {
55
- maxTimestamp = new Date(row.max_timestamp);
56
- } else {
57
- maxTimestamp = /* @__PURE__ */ new Date();
36
+ const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("catalog_entity_ref");
37
+ const statusRows = await this.dbClient(this.tableName).select("status").count("* as count").max("timestamp as max_timestamp").whereIn("id", latestIdsSubquery).whereNotNull("status").whereNotNull("value").groupBy("status");
38
+ if (!statusRows || statusRows.length === 0) {
39
+ return void 0;
40
+ }
41
+ const normalizeTimestamp = (timestamp) => {
42
+ if (timestamp instanceof Date) {
43
+ return timestamp;
44
+ } else if (typeof timestamp === "number" || typeof timestamp === "string") {
45
+ return new Date(timestamp);
46
+ }
47
+ return /* @__PURE__ */ new Date();
48
+ };
49
+ let maxTimestamp = /* @__PURE__ */ new Date(0);
50
+ let total = 0;
51
+ const statusCounts = {};
52
+ for (const row of statusRows) {
53
+ const rowTimestamp = normalizeTimestamp(row.max_timestamp);
54
+ if (rowTimestamp > maxTimestamp) {
55
+ maxTimestamp = rowTimestamp;
58
56
  }
59
- return {
60
- metric_id: row.metric_id,
61
- total: Number(row.total),
62
- max_timestamp: maxTimestamp,
63
- success: Number(row.success),
64
- warning: Number(row.warning),
65
- error: Number(row.error)
66
- };
57
+ const name = row.status;
58
+ const count = Number(row.count);
59
+ statusCounts[name] = count;
60
+ total += count;
67
61
  }
68
- return void 0;
62
+ return {
63
+ metric_id,
64
+ total,
65
+ max_timestamp: maxTimestamp,
66
+ statusCounts
67
+ };
69
68
  }
70
69
  }
71
70
 
@@ -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\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 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('metric_id', 'catalog_entity_ref');\n\n const [row] = await this.dbClient(this.tableName)\n .select('metric_id')\n .count('* as total')\n .max('timestamp as max_timestamp')\n .select(\n this.dbClient.raw(\n \"SUM(CASE WHEN status = 'success' AND value IS NOT NULL THEN 1 ELSE 0 END) as success\",\n ),\n )\n .select(\n this.dbClient.raw(\n \"SUM(CASE WHEN status = 'warning' AND value IS NOT NULL THEN 1 ELSE 0 END) as warning\",\n ),\n )\n .select(\n this.dbClient.raw(\n \"SUM(CASE WHEN status = 'error' AND value IS NOT NULL THEN 1 ELSE 0 END) as error\",\n ),\n )\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereNotNull('value')\n .groupBy('metric_id');\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 if (row) {\n let maxTimestamp: Date;\n if (row.max_timestamp instanceof Date) {\n maxTimestamp = row.max_timestamp;\n } else if (\n typeof row.max_timestamp === 'number' ||\n typeof row.max_timestamp === 'string'\n ) {\n maxTimestamp = new Date(row.max_timestamp as string | number);\n } else {\n maxTimestamp = new Date();\n }\n\n return {\n metric_id: row.metric_id,\n total: Number(row.total),\n max_timestamp: maxTimestamp,\n success: Number(row.success),\n warning: Number(row.warning),\n error: Number(row.error),\n };\n }\n\n return undefined;\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\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 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 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 .whereNotNull('value')\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return undefined;\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: any): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n } else if (\n typeof timestamp === 'number' ||\n typeof timestamp === 'string'\n ) {\n return new Date(timestamp);\n }\n return new Date();\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 return {\n metric_id,\n total,\n max_timestamp: maxTimestamp,\n statusCounts,\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -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 AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport { AuthService } 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 { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { AggregatedMetricMapper } from './mappers';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n};\n\nexport type AggregatedMetricsByStatus = Record<\n string,\n { values: { success: number; warning: number; error: number }; total: number }\n>;\n\nexport class CatalogMetricService {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\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 }\n\n /**\n * Get latest metric results for a specific catalog entity and metric providers.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param providerIds - Optional array of provider 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 providerIds?: 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(providerIds);\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 = provider.getMetric();\n\n try {\n thresholds = mergeEntityAndProviderThresholds(entity, provider);\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 = error_message !== null && value === null;\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 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 results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper"],"mappings":";;;;;;;AA+CO,MAAM,oBAAqB,CAAA;AAAA,EACf,OAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAsC,EAAA;AAChD,IAAA,IAAA,CAAK,UAAU,OAAQ,CAAA,OAAA;AACvB,IAAA,IAAA,CAAK,OAAO,OAAQ,CAAA,IAAA;AACpB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AACxB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AAAA;AAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,sBAAA,CACJ,SACA,EAAA,WAAA,EACA,MAGyB,EAAA;AACzB,IAAA,MAAM,MAAS,GAAA,MAAM,IAAK,CAAA,OAAA,CAAQ,eAAe,SAAW,EAAA;AAAA,MAC1D,WAAa,EAAA,MAAM,IAAK,CAAA,IAAA,CAAK,wBAAyB;AAAA,KACvD,CAAA;AACD,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAA,MAAM,IAAIA,oBAAA,CAAc,CAAqB,kBAAA,EAAA,SAAS,CAAE,CAAA,CAAA;AAAA;AAG1D,IAAA,MAAM,cAAiB,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,WAAW,CAAA;AAE5D,IAAA,MAAM,wBAA2B,GAAAC,uCAAA;AAAA,MAC/B,cAAA;AAAA,MACA;AAAA,KACF;AACA,IAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,QAAS,CAAA,4BAAA;AAAA,MACrC,SAAA;AAAA,MACA,wBAAyB,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,EAAE;AAAA,KACxC;AAEA,IAAA,OAAO,UAAW,CAAA,GAAA;AAAA,MAChB,CAAC,EAAE,SAAA,EAAW,OAAO,aAAe,EAAA,SAAA,EAAW,QAAa,KAAA;AAC1D,QAAI,IAAA,UAAA;AACJ,QAAI,IAAA,cAAA;AAEJ,QAAA,MAAM,QAAW,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,SAAS,CAAA;AACpD,QAAM,MAAA,MAAA,GAAS,SAAS,SAAU,EAAA;AAElC,QAAI,IAAA;AACF,UAAa,UAAA,GAAAC,iEAAA,CAAiC,QAAQ,QAAQ,CAAA;AAE9D,UAAA,IAAI,UAAU,IAAM,EAAA;AAClB,YACE,cAAA,GAAA,wDAAA;AAAA,qBACO,aAAe,EAAA;AACxB,YAAiB,cAAA,GAAA,aAAA;AAAA;AACnB,iBACO,KAAO,EAAA;AACd,UAAA,cAAA,GAAiBC,sBAAe,KAAK,CAAA;AAAA;AAGvC,QAAM,MAAA,iBAAA,GAAoB,aAAkB,KAAA,IAAA,IAAQ,KAAU,KAAA,IAAA;AAE9D,QAAO,OAAA;AAAA,UACL,IAAI,MAAO,CAAA,EAAA;AAAA,UACX,MAAA,EAAQ,oBAAoB,OAAU,GAAA,SAAA;AAAA,UACtC,QAAU,EAAA;AAAA,YACR,OAAO,MAAO,CAAA,KAAA;AAAA,YACd,aAAa,MAAO,CAAA,WAAA;AAAA,YACpB,MAAM,MAAO,CAAA,IAAA;AAAA,YACb,SAAS,MAAO,CAAA;AAAA,WAClB;AAAA,UACA,GAAI,iBAAqB,IAAA;AAAA,YACvB,OACE,aACA,IAAAA,qBAAA,CAAe,IAAI,KAAA,CAAM,6BAA6B,CAAC;AAAA,WAC3D;AAAA,UACA,MAAQ,EAAA;AAAA,YACN,KAAA;AAAA,YACA,SAAW,EAAA,IAAI,IAAK,CAAA,SAAS,EAAE,WAAY,EAAA;AAAA,YAC3C,eAAiB,EAAA;AAAA,cACf,UAAY,EAAA,UAAA;AAAA,cACZ,MAAA,EAAQ,iBAAiB,OAAU,GAAA,SAAA;AAAA,cACnC,UAAY,EAAA,MAAA;AAAA,cACZ,GAAI,cAAA,IAAkB,EAAE,KAAA,EAAO,cAAe;AAAA;AAChD;AACF,SACF;AAAA;AACF,KACF;AAAA;AACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,+BACJ,CAAA,UAAA,EACA,QAC2B,EAAA;AAC3B,IAAI,IAAA,UAAA,CAAW,WAAW,CAAG,EAAA;AAC3B,MAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,QAAS,CAAA,gCAAA;AAAA,QAClB,UAAA;AAAA,QACA;AAAA,OACF;AAEF,MAAO,OAAAC,8BAAA,CAAuB,mBAAmB,gBAAgB,CAAA;AAAA;AAGnE,IAAA,OAAOA,+BAAuB,kBAAmB,EAAA;AAAA;AAErD;;;;"}
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 AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport { AuthService } 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 { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { AggregatedMetricMapper } from './mappers';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n};\n\nexport class CatalogMetricService {\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\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 }\n\n /**\n * Get latest metric results for a specific catalog entity and metric providers.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param providerIds - Optional array of provider 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 providerIds?: 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(providerIds);\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 = provider.getMetric();\n\n try {\n thresholds = mergeEntityAndProviderThresholds(entity, provider);\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 = error_message !== null && value === null;\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 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 results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper"],"mappings":"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"}
@@ -2,22 +2,19 @@
2
2
 
3
3
  class AggregatedMetricMapper {
4
4
  static toAggregatedMetric(aggregatedMetric) {
5
- const success = aggregatedMetric?.success ?? 0;
6
- const warning = aggregatedMetric?.warning ?? 0;
7
- const error = aggregatedMetric?.error ?? 0;
8
5
  const total = aggregatedMetric?.total ?? 0;
9
6
  const timestamp = aggregatedMetric?.max_timestamp ? new Date(aggregatedMetric.max_timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
10
7
  return {
11
- values: [
12
- { count: success, name: "success" },
13
- { count: warning, name: "warning" },
14
- { count: error, name: "error" }
15
- ],
8
+ values: aggregatedMetric?.statusCounts ?? {},
16
9
  total,
17
10
  timestamp
18
11
  };
19
12
  }
20
- static toAggregatedMetricResult(metric, aggregatedMetric) {
13
+ static toAggregatedMetricResult(metric, thresholds, aggregatedMetric) {
14
+ const allStatusCountValues = thresholds.rules.map((rule) => ({
15
+ name: rule.key,
16
+ count: aggregatedMetric.values[rule.key] ?? 0
17
+ }));
21
18
  return {
22
19
  id: metric.id,
23
20
  status: "success",
@@ -27,7 +24,11 @@ class AggregatedMetricMapper {
27
24
  type: metric.type,
28
25
  history: metric.history
29
26
  },
30
- result: aggregatedMetric
27
+ result: {
28
+ ...aggregatedMetric,
29
+ values: allStatusCountValues,
30
+ thresholds
31
+ }
31
32
  };
32
33
  }
33
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.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 AggregatedMetric,\n AggregatedMetricResult,\n Metric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const success = aggregatedMetric?.success ?? 0;\n const warning = aggregatedMetric?.warning ?? 0;\n const error = aggregatedMetric?.error ?? 0;\n const total = aggregatedMetric?.total ?? 0;\n const timestamp = aggregatedMetric?.max_timestamp\n ? new Date(aggregatedMetric.max_timestamp).toISOString()\n : new Date().toISOString();\n\n return {\n values: [\n { count: success, name: 'success' },\n { count: warning, name: 'warning' },\n { count: error, name: 'error' },\n ],\n total,\n timestamp,\n };\n }\n\n static toAggregatedMetricResult(\n metric: Metric,\n aggregatedMetric: AggregatedMetric,\n ): AggregatedMetricResult {\n return {\n id: metric.id,\n status: 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n result: aggregatedMetric,\n };\n }\n}\n"],"names":[],"mappings":";;AAuBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,OAAO,mBACL,gBACkB,EAAA;AAClB,IAAM,MAAA,OAAA,GAAU,kBAAkB,OAAW,IAAA,CAAA;AAC7C,IAAM,MAAA,OAAA,GAAU,kBAAkB,OAAW,IAAA,CAAA;AAC7C,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAA,MAAM,SAAY,GAAA,gBAAA,EAAkB,aAChC,GAAA,IAAI,IAAK,CAAA,gBAAA,CAAiB,aAAa,CAAA,CAAE,WAAY,EAAA,GAAA,iBACjD,IAAA,IAAA,IAAO,WAAY,EAAA;AAE3B,IAAO,OAAA;AAAA,MACL,MAAQ,EAAA;AAAA,QACN,EAAE,KAAA,EAAO,OAAS,EAAA,IAAA,EAAM,SAAU,EAAA;AAAA,QAClC,EAAE,KAAA,EAAO,OAAS,EAAA,IAAA,EAAM,SAAU,EAAA;AAAA,QAClC,EAAE,KAAA,EAAO,KAAO,EAAA,IAAA,EAAM,OAAQ;AAAA,OAChC;AAAA,MACA,KAAA;AAAA,MACA;AAAA,KACF;AAAA;AACF,EAEA,OAAO,wBACL,CAAA,MAAA,EACA,gBACwB,EAAA;AACxB,IAAO,OAAA;AAAA,MACL,IAAI,MAAO,CAAA,EAAA;AAAA,MACX,MAAQ,EAAA,SAAA;AAAA,MACR,QAAU,EAAA;AAAA,QACR,OAAO,MAAO,CAAA,KAAA;AAAA,QACd,aAAa,MAAO,CAAA,WAAA;AAAA,QACpB,MAAM,MAAO,CAAA,IAAA;AAAA,QACb,SAAS,MAAO,CAAA;AAAA,OAClB;AAAA,MACA,MAAQ,EAAA;AAAA,KACV;AAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.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 AggregatedMetric,\n AggregatedMetricResult,\n Metric,\n ThresholdConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const total = aggregatedMetric?.total ?? 0;\n const timestamp = aggregatedMetric?.max_timestamp\n ? new Date(aggregatedMetric.max_timestamp).toISOString()\n : new Date().toISOString();\n\n return {\n values: aggregatedMetric?.statusCounts ?? {},\n total,\n timestamp,\n };\n }\n\n static toAggregatedMetricResult(\n metric: Metric,\n thresholds: ThresholdConfig,\n aggregatedMetric: AggregatedMetric,\n ): AggregatedMetricResult {\n // Build values in threshold rules order, filling missing ones with 0\n const allStatusCountValues = thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n }));\n\n return {\n id: metric.id,\n status: 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n result: {\n ...aggregatedMetric,\n values: allStatusCountValues,\n thresholds,\n },\n };\n }\n}\n"],"names":[],"mappings":";;AAwBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,OAAO,mBACL,gBACkB,EAAA;AAClB,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAA,MAAM,SAAY,GAAA,gBAAA,EAAkB,aAChC,GAAA,IAAI,IAAK,CAAA,gBAAA,CAAiB,aAAa,CAAA,CAAE,WAAY,EAAA,GAAA,iBACjD,IAAA,IAAA,IAAO,WAAY,EAAA;AAE3B,IAAO,OAAA;AAAA,MACL,MAAA,EAAQ,gBAAkB,EAAA,YAAA,IAAgB,EAAC;AAAA,MAC3C,KAAA;AAAA,MACA;AAAA,KACF;AAAA;AACF,EAEA,OAAO,wBAAA,CACL,MACA,EAAA,UAAA,EACA,gBACwB,EAAA;AAExB,IAAA,MAAM,oBAAuB,GAAA,UAAA,CAAW,KAAM,CAAA,GAAA,CAAI,CAAS,IAAA,MAAA;AAAA,MACzD,MAAM,IAAK,CAAA,GAAA;AAAA,MACX,KAAO,EAAA,gBAAA,CAAiB,MAAO,CAAA,IAAA,CAAK,GAAG,CAAK,IAAA;AAAA,KAC5C,CAAA,CAAA;AAEF,IAAO,OAAA;AAAA,MACL,IAAI,MAAO,CAAA,EAAA;AAAA,MACX,MAAQ,EAAA,SAAA;AAAA,MACR,QAAU,EAAA;AAAA,QACR,OAAO,MAAO,CAAA,KAAA;AAAA,QACd,aAAa,MAAO,CAAA,WAAA;AAAA,QACpB,MAAM,MAAO,CAAA,IAAA;AAAA,QACb,SAAS,MAAO,CAAA;AAAA,OAClB;AAAA,MACA,MAAQ,EAAA;AAAA,QACN,GAAG,gBAAA;AAAA,QACH,MAAQ,EAAA,oBAAA;AAAA,QACR;AAAA;AACF,KACF;AAAA;AAEJ;;;;"}
@@ -89,7 +89,8 @@ async function createRouter({
89
89
  req,
90
90
  backstagePluginScorecardCommon.scorecardMetricReadPermission
91
91
  );
92
- const metric = metricProvidersRegistry.getMetric(metricId);
92
+ const provider = metricProvidersRegistry.getProvider(metricId);
93
+ const metric = provider.getMetric();
93
94
  const authorizedMetrics = permissionUtils.filterAuthorizedMetrics([metric], conditions);
94
95
  if (authorizedMetrics.length === 0) {
95
96
  throw new errors.NotAllowedError(
@@ -108,12 +109,17 @@ async function createRouter({
108
109
  for (const entityRef of entitiesOwnedByAUser) {
109
110
  await permissionUtils.checkEntityAccess(entityRef, req, permissions, httpAuth);
110
111
  }
112
+ const thresholds = provider.getMetricThresholds();
111
113
  const aggregatedMetric = await catalogMetricService.getAggregatedMetricByEntityRefs(
112
114
  entitiesOwnedByAUser,
113
115
  metricId
114
116
  );
115
117
  res.json(
116
- mappers.AggregatedMetricMapper.toAggregatedMetricResult(metric, aggregatedMetric)
118
+ mappers.AggregatedMetricMapper.toAggregatedMetricResult(
119
+ metric,
120
+ thresholds,
121
+ aggregatedMetric
122
+ )
117
123
  );
118
124
  });
119
125
  return router;
@@ -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, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateCatalogMetricsSchema } from '../validation/validateCatalogMetricsSchema';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { validateMetricsSchema } from '../validation/validateMetricsSchema';\nimport { AggregatedMetricMapper } from './mappers';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { metricIds, datasource } = validateMetricsSchema(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),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(datasource),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const { metricIds } = validateCatalogMetricsSchema(req.query);\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)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n router.get('/metrics/:metricId/catalog/aggregations', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n req,\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 aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metricId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(metric, aggregatedMetric),\n );\n });\n\n return router;\n}\n"],"names":["Router","express","AuthorizeResult","NotAllowedError","validateMetricsSchema","InputError","parseCommaSeparatedString","scorecardMetricReadPermission","validateCatalogMetricsSchema","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","AuthenticationError","getEntitiesOwnedByUser","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, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport type { CatalogMetricService } from './CatalogMetricService';\nimport type { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport {\n type HttpAuthService,\n type PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport type { CatalogService } from '@backstage/plugin-catalog-node';\nimport {\n AuthorizeResult,\n BasicPermission,\n PolicyDecision,\n ResourcePermission,\n} from '@backstage/plugin-permission-common';\nimport { scorecardMetricReadPermission } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n filterAuthorizedMetrics,\n checkEntityAccess,\n} from '../permissions/permissionUtils';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { validateCatalogMetricsSchema } from '../validation/validateCatalogMetricsSchema';\nimport { getEntitiesOwnedByUser } from '../utils/getEntitiesOwnedByUser';\nimport { parseCommaSeparatedString } from '../utils/parseCommaSeparatedString';\nimport { validateMetricsSchema } from '../validation/validateMetricsSchema';\nimport { AggregatedMetricMapper } from './mappers';\n\nexport type ScorecardRouterOptions = {\n metricProvidersRegistry: MetricProvidersRegistry;\n catalogMetricService: CatalogMetricService;\n catalog: CatalogService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n};\n\nexport async function createRouter({\n metricProvidersRegistry,\n catalogMetricService,\n catalog,\n httpAuth,\n permissions,\n}: ScorecardRouterOptions): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const authorizeConditional = async (\n request: Request,\n permission: ResourcePermission<'scorecard-metric'> | BasicPermission,\n ) => {\n const credentials = await httpAuth.credentials(request);\n let decision: PolicyDecision;\n\n if (permission.type === 'resource') {\n decision = (\n await permissions.authorizeConditional([{ permission }], {\n credentials,\n })\n )[0];\n } else {\n decision = (\n await permissions.authorize([{ permission }], {\n credentials,\n })\n )[0];\n }\n\n if (decision.result === AuthorizeResult.DENY) {\n throw new NotAllowedError(); // 403\n }\n\n return {\n decision,\n conditions:\n decision.result === AuthorizeResult.CONDITIONAL\n ? decision.conditions\n : undefined,\n };\n };\n\n router.get('/metrics', async (req, res) => {\n const { metricIds, datasource } = validateMetricsSchema(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),\n ),\n });\n }\n\n if (datasource) {\n return res.json({\n metrics: metricProvidersRegistry.listMetricsByDatasource(datasource),\n });\n }\n\n return res.json({ metrics: metricProvidersRegistry.listMetrics() });\n });\n\n router.get('/metrics/catalog/:kind/:namespace/:name', async (req, res) => {\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const { kind, namespace, name } = req.params;\n\n const { metricIds } = validateCatalogMetricsSchema(req.query);\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)\n : undefined;\n\n const results = await catalogMetricService.getLatestEntityMetrics(\n entityRef,\n metricIdArray,\n conditions,\n );\n res.json(results);\n });\n\n router.get('/metrics/:metricId/catalog/aggregations', async (req, res) => {\n const { metricId } = req.params;\n\n const { conditions } = await authorizeConditional(\n req,\n scorecardMetricReadPermission,\n );\n\n const provider = metricProvidersRegistry.getProvider(metricId);\n const metric = provider.getMetric();\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 = provider.getMetricThresholds();\n const aggregatedMetric =\n await catalogMetricService.getAggregatedMetricByEntityRefs(\n entitiesOwnedByAUser,\n metricId,\n );\n\n res.json(\n AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n thresholds,\n aggregatedMetric,\n ),\n );\n });\n\n return router;\n}\n"],"names":["Router","express","AuthorizeResult","NotAllowedError","validateMetricsSchema","InputError","parseCommaSeparatedString","scorecardMetricReadPermission","validateCatalogMetricsSchema","stringifyEntityRef","checkEntityAccess","filterAuthorizedMetrics","AuthenticationError","getEntitiesOwnedByUser","AggregatedMetricMapper"],"mappings":"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"}
@@ -4,37 +4,26 @@ 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
- function parseEntityOverrideThresholds(entity, providerId, metricType) {
7
+ const thresholdRulesAnnotationPrefix = (providerId) => `scorecard.io/${providerId}.thresholds.rules.`;
8
+ function parseEntityOverrideThresholds(entity, providerId) {
8
9
  const annotations = entity.metadata?.annotations || {};
9
- const prefix = `scorecard.io/${providerId}.thresholds.rules.`;
10
+ const prefix = thresholdRulesAnnotationPrefix(providerId);
10
11
  const overrides = [];
11
12
  for (const [annotationKey, expression] of Object.entries(annotations)) {
12
13
  if (annotationKey.startsWith(prefix) && expression) {
13
14
  const key = annotationKey.substring(prefix.length);
14
- const entityRule = { key, expression };
15
- try {
16
- backstagePluginScorecardNode.validateThresholds({ rules: [entityRule] }, metricType);
17
- overrides.push(entityRule);
18
- } catch (e) {
19
- if (errors.isError(e)) {
20
- throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
21
- `Invalid threshold annotation '${annotationKey}: ${expression}' in entity '${catalogModel.stringifyEntityRef(
22
- entity
23
- )}': ${e.message}`
24
- );
25
- }
26
- throw e;
27
- }
15
+ overrides.push({ key, expression });
28
16
  }
29
17
  }
30
18
  return overrides;
31
19
  }
32
20
  function mergeEntityAndProviderThresholds(entity, provider) {
21
+ const providerId = provider.getProviderId();
33
22
  const providerThresholds = provider.getMetricThresholds();
23
+ const providerMetricType = provider.getMetricType();
34
24
  const entityOverrideThresholds = parseEntityOverrideThresholds(
35
25
  entity,
36
- provider.getProviderId(),
37
- provider.getMetricType()
26
+ providerId
38
27
  );
39
28
  const mergedRules = [...providerThresholds.rules];
40
29
  for (const override of entityOverrideThresholds) {
@@ -45,10 +34,23 @@ function mergeEntityAndProviderThresholds(entity, provider) {
45
34
  entity
46
35
  )} thresholds by ${JSON.stringify(
47
36
  override
48
- )}, metric provider ${provider.getProviderId()} does not support key ${override.key}`
37
+ )}, metric provider ${providerId} does not support key ${override.key}`
49
38
  );
50
39
  }
51
- mergedRules[foundKey] = override;
40
+ const mergedRule = { ...mergedRules[foundKey], ...override };
41
+ try {
42
+ backstagePluginScorecardNode.validateThresholds({ rules: [mergedRule] }, providerMetricType);
43
+ } catch (e) {
44
+ if (errors.isError(e)) {
45
+ throw new backstagePluginScorecardNode.ThresholdConfigFormatError(
46
+ `Invalid threshold annotation '${thresholdRulesAnnotationPrefix(
47
+ providerId
48
+ )}${override.key}: ${override.expression}' in entity '${catalogModel.stringifyEntityRef(entity)}': ${e.message}`
49
+ );
50
+ }
51
+ throw e;
52
+ }
53
+ mergedRules[foundKey] = mergedRule;
52
54
  }
53
55
  return {
54
56
  rules: mergedRules
@@ -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 MetricType,\n ThresholdConfig,\n ThresholdRule,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n ThresholdConfigFormatError,\n validateThresholds,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isError } from '@backstage/errors';\nimport type { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\n\nfunction parseEntityOverrideThresholds(\n entity: Entity,\n providerId: string,\n metricType: MetricType,\n): ThresholdRule[] {\n const annotations = entity.metadata?.annotations || {};\n const prefix = `scorecard.io/${providerId}.thresholds.rules.`;\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 const entityRule = { key, expression };\n try {\n validateThresholds({ rules: [entityRule] }, metricType);\n overrides.push(entityRule);\n } catch (e) {\n if (isError(e)) {\n throw new ThresholdConfigFormatError(\n `Invalid threshold annotation '${annotationKey}: ${expression}' in entity '${stringifyEntityRef(\n entity,\n )}': ${e.message}`,\n );\n }\n throw e;\n }\n }\n }\n\n return overrides;\n}\n\nexport function mergeEntityAndProviderThresholds(\n entity: Entity,\n provider: MetricProvider,\n): ThresholdConfig {\n const providerThresholds = provider.getMetricThresholds();\n const entityOverrideThresholds = parseEntityOverrideThresholds(\n entity,\n provider.getProviderId(),\n provider.getMetricType(),\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityOverrideThresholds) {\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 ${provider.getProviderId()} does not support key ${\n override.key\n }`,\n );\n }\n\n mergedRules[foundKey] = override;\n }\n\n return {\n rules: mergedRules,\n };\n}\n"],"names":["validateThresholds","isError","ThresholdConfigFormatError","stringifyEntityRef"],"mappings":";;;;;;AA6BA,SAAS,6BAAA,CACP,MACA,EAAA,UAAA,EACA,UACiB,EAAA;AACjB,EAAA,MAAM,WAAc,GAAA,MAAA,CAAO,QAAU,EAAA,WAAA,IAAe,EAAC;AACrD,EAAM,MAAA,MAAA,GAAS,gBAAgB,UAAU,CAAA,kBAAA,CAAA;AACzC,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,MAAM,MAAA,UAAA,GAAa,EAAE,GAAA,EAAK,UAAW,EAAA;AACrC,MAAI,IAAA;AACF,QAAAA,+CAAA,CAAmB,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,UAAU,CAAA;AACtD,QAAA,SAAA,CAAU,KAAK,UAAU,CAAA;AAAA,eAClB,CAAG,EAAA;AACV,QAAI,IAAAC,cAAA,CAAQ,CAAC,CAAG,EAAA;AACd,UAAA,MAAM,IAAIC,uDAAA;AAAA,YACR,CAAiC,8BAAA,EAAA,aAAa,CAAK,EAAA,EAAA,UAAU,CAAgB,aAAA,EAAAC,+BAAA;AAAA,cAC3E;AAAA,aACD,CAAM,GAAA,EAAA,CAAA,CAAE,OAAO,CAAA;AAAA,WAClB;AAAA;AAEF,QAAM,MAAA,CAAA;AAAA;AACR;AACF;AAGF,EAAO,OAAA,SAAA;AACT;AAEgB,SAAA,gCAAA,CACd,QACA,QACiB,EAAA;AACjB,EAAM,MAAA,kBAAA,GAAqB,SAAS,mBAAoB,EAAA;AACxD,EAAA,MAAM,wBAA2B,GAAA,6BAAA;AAAA,IAC/B,MAAA;AAAA,IACA,SAAS,aAAc,EAAA;AAAA,IACvB,SAAS,aAAc;AAAA,GACzB;AAEA,EAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,wBAA0B,EAAA;AAC/C,IAAA,MAAM,WAAW,WAAY,CAAA,SAAA,CAAU,UAAQ,IAAK,CAAA,GAAA,KAAQ,SAAS,GAAG,CAAA;AACxE,IAAA,IAAI,aAAa,EAAI,EAAA;AACnB,MAAA,MAAM,IAAID,uDAAA;AAAA,QACR,CAAsB,mBAAA,EAAAC,+BAAA;AAAA,UACpB;AAAA,SACD,kBAAkB,IAAK,CAAA,SAAA;AAAA,UACtB;AAAA,SACD,CAAqB,kBAAA,EAAA,QAAA,CAAS,eAAe,CAAA,sBAAA,EAC5C,SAAS,GACX,CAAA;AAAA,OACF;AAAA;AAGF,IAAA,WAAA,CAAY,QAAQ,CAAI,GAAA,QAAA;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 ThresholdConfigFormatError,\n validateThresholds,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { isError } from '@backstage/errors';\nimport type { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\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 parseEntityOverrideThresholds(\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 entityOverrideThresholds = parseEntityOverrideThresholds(\n entity,\n providerId,\n );\n\n const mergedRules = [...providerThresholds.rules];\n for (const override of entityOverrideThresholds) {\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 validateThresholds({ 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","validateThresholds","isError"],"mappings":";;;;;;AA4BA,MAAM,8BAAiC,GAAA,CAAC,UACtC,KAAA,CAAA,aAAA,EAAgB,UAAU,CAAA,kBAAA,CAAA;AAK5B,SAAS,6BAAA,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,wBAA2B,GAAA,6BAAA;AAAA,IAC/B,MAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,WAAc,GAAA,CAAC,GAAG,kBAAA,CAAmB,KAAK,CAAA;AAChD,EAAA,KAAA,MAAW,YAAY,wBAA0B,EAAA;AAC/C,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,+CAAA,CAAmB,EAAE,KAAO,EAAA,CAAC,UAAU,CAAA,IAAK,kBAAkB,CAAA;AAAA,aACvD,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;;;;"}
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright Red Hat, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ exports.up = async function up(knex) {
18
+ // Remove the status_check constraint that limits status values to 'success', 'warning', 'error'
19
+ const client = knex.client.config.client;
20
+
21
+ if (client === 'sqlite3' || client === 'better-sqlite3') {
22
+ await knex.raw(`
23
+ ALTER TABLE metric_values
24
+ RENAME COLUMN status TO status_old;
25
+ `);
26
+
27
+ await knex.raw(`
28
+ ALTER TABLE metric_values
29
+ ADD COLUMN status VARCHAR(255) NULL;
30
+ `);
31
+
32
+ await knex.raw(`
33
+ UPDATE metric_values
34
+ SET status = status_old;
35
+ `);
36
+
37
+ await knex.raw(`
38
+ ALTER TABLE metric_values
39
+ DROP COLUMN status_old;
40
+ `);
41
+ } else {
42
+ await knex.schema.alterTable('metric_values', table => {
43
+ table.dropChecks(['status_check']);
44
+ });
45
+ }
46
+ };
47
+
48
+ exports.down = async function down(knex) {
49
+ // Re-add the status_check constraint
50
+
51
+ // Fail if any incompatible rows with status values that don't match the constraint
52
+ const incompatibleRows = await knex('metric_values')
53
+ .whereNotIn('status', ['success', 'warning', 'error'])
54
+ .whereNotNull('status')
55
+ .count('* as count')
56
+ .first();
57
+ if (incompatibleRows && incompatibleRows.count > 0) {
58
+ throw new Error(
59
+ `Cannot rollback migration: Found ${incompatibleRows.count} rows with status values ` +
60
+ `outside of ['success', 'warning', 'error']. Please migrate or remove these rows before rolling back.`,
61
+ );
62
+ }
63
+
64
+ const client = knex.client.config.client;
65
+
66
+ if (client === 'sqlite3' || client === 'better-sqlite3') {
67
+ await knex.raw(`
68
+ ALTER TABLE metric_values
69
+ RENAME COLUMN status TO status_old;
70
+ `);
71
+
72
+ await knex.raw(`
73
+ ALTER TABLE metric_values
74
+ ADD COLUMN status VARCHAR(255) NULL
75
+ CHECK (status IN ('success', 'warning', 'error'));
76
+ `);
77
+
78
+ await knex.raw(`
79
+ UPDATE metric_values
80
+ SET status = status_old;
81
+ `);
82
+
83
+ await knex.raw(`
84
+ ALTER TABLE metric_values
85
+ DROP COLUMN status_old;
86
+ `);
87
+ } else {
88
+ await knex.raw(
89
+ "ALTER TABLE metric_values ADD CONSTRAINT status_check CHECK (status IN ('success', 'warning', 'error'))",
90
+ );
91
+ }
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
3
- "version": "2.3.5",
3
+ "version": "2.4.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": "^1.20.0",
48
48
  "@backstage/plugin-permission-common": "^0.9.3",
49
49
  "@backstage/plugin-permission-node": "^0.10.6",
50
- "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.3.5",
51
- "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.3.5",
50
+ "@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.4.0",
51
+ "@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.4.0",
52
52
  "express": "^4.17.1",
53
53
  "express-promise-router": "^4.1.0",
54
54
  "knex": "^3.1.0",