@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.3.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +90 -9
  3. package/config.d.ts +75 -2
  4. package/dist/constants/aggregationKPIs.cjs.js +6 -0
  5. package/dist/constants/aggregationKPIs.cjs.js.map +1 -0
  6. package/dist/database/DatabaseMetricValues.cjs.js +94 -30
  7. package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
  8. package/dist/middlewares/validateAggregationIdParam.cjs.js +20 -0
  9. package/dist/middlewares/validateAggregationIdParam.cjs.js.map +1 -0
  10. package/dist/middlewares/validateDatasourceQueryParams.cjs.js +18 -0
  11. package/dist/middlewares/validateDatasourceQueryParams.cjs.js.map +1 -0
  12. package/dist/middlewares/validateMetricIdsQueryParams.cjs.js +18 -0
  13. package/dist/middlewares/validateMetricIdsQueryParams.cjs.js.map +1 -0
  14. package/dist/permissions/permissionUtils.cjs.js +26 -0
  15. package/dist/permissions/permissionUtils.cjs.js.map +1 -1
  16. package/dist/permissions/rules.cjs.js +4 -1
  17. package/dist/permissions/rules.cjs.js.map +1 -1
  18. package/dist/plugin.cjs.js +10 -2
  19. package/dist/plugin.cjs.js.map +1 -1
  20. package/dist/providers/MetricProvidersRegistry.cjs.js +3 -0
  21. package/dist/providers/MetricProvidersRegistry.cjs.js.map +1 -1
  22. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +25 -3
  23. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
  24. package/dist/service/CatalogMetricService.cjs.js +216 -8
  25. package/dist/service/CatalogMetricService.cjs.js.map +1 -1
  26. package/dist/service/mappers.cjs.js +24 -16
  27. package/dist/service/mappers.cjs.js.map +1 -1
  28. package/dist/service/router.cjs.js +219 -86
  29. package/dist/service/router.cjs.js.map +1 -1
  30. package/dist/threshold/ThresholdEvaluator.cjs.js +2 -1
  31. package/dist/threshold/ThresholdEvaluator.cjs.js.map +1 -1
  32. package/dist/utils/buildAggregationConfig.cjs.js +15 -0
  33. package/dist/utils/buildAggregationConfig.cjs.js.map +1 -0
  34. package/dist/utils/mergeEntityAndProviderThresholds.cjs.js +24 -22
  35. package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +1 -1
  36. package/dist/utils/metricUtils.cjs.js +35 -0
  37. package/dist/utils/metricUtils.cjs.js.map +1 -0
  38. package/dist/utils/normalizeOwnerRef.cjs.js +24 -0
  39. package/dist/utils/normalizeOwnerRef.cjs.js.map +1 -0
  40. package/dist/validation/validateAggregationConfig.cjs.js +47 -0
  41. package/dist/validation/validateAggregationConfig.cjs.js.map +1 -0
  42. package/dist/validation/validateDrillDownMetricsSchema.cjs.js +53 -0
  43. package/dist/validation/validateDrillDownMetricsSchema.cjs.js.map +1 -0
  44. package/migrations/20260206115752_remove_status_check_constraint.js +92 -0
  45. package/migrations/20260217152637_add_entity_metadata_columns.js +37 -0
  46. package/package.json +14 -13
  47. package/dist/validation/validateCatalogMetricsSchema.cjs.js +0 -18
  48. package/dist/validation/validateCatalogMetricsSchema.cjs.js.map +0 -1
  49. package/dist/validation/validateMetricsSchema.cjs.js +0 -19
  50. package/dist/validation/validateMetricsSchema.cjs.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 2.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d706601: Backstage version bump to v1.49.3
8
+ - 55226c2: Introduces custom threshold rule icons that can be configured in `app-config.yaml`.
9
+ - bd5f3e8: added disable metric feature to the pull metric task
10
+ - 243ad0a: Aggregated scorecards now use **aggregation IDs** and dedicated HTTP routes. The old catalog-aggregations URL still works but is **deprecated** (not removed).
11
+
12
+ **Backend (`@red-hat-developer-hub/backstage-plugin-scorecard-backend`)**
13
+
14
+ - **Deprecated:** `GET /metrics/:metricId/catalog/aggregations` — responses are unchanged, but the handler emits [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) `Deprecation` and `Link` headers (alternate successor: `GET .../aggregations/:aggregationId`) and logs a warning. Prefer **`GET /aggregations/:aggregationId`** for new integrations.
15
+ - **Added:** `GET /aggregations/:aggregationId` for aggregated results using configured aggregation.
16
+ - **Added:** `GET /aggregations/:aggregationId/metadata` for KPI titles, descriptions, and aggregation metadata consumed by the UI.
17
+
18
+ **Common (`@red-hat-developer-hub/backstage-plugin-scorecard-common`)**
19
+
20
+ - Types and constants aligned with the aggregation config and new API shapes.
21
+
22
+ **Frontend (`@red-hat-developer-hub/backstage-plugin-scorecard`)**
23
+
24
+ - Homepage and aggregated flows resolve cards via **`aggregationId`**, fetch metadata from the new endpoint, and keep localized threshold and error strings where translation keys exist.
25
+
26
+ **Action for adopters:** Configure aggregated scorecards with `aggregationId` values that match backend aggregation config, replace direct calls to `GET /metrics/:metricId/catalog/aggregations` with `GET /aggregations/:aggregationId` (and metadata if you need the same labels as the plugin UI).
27
+
28
+ - c83b206: Adds the ability to drill down from aggregated scorecard KPIs to view the individual entities that contribute to the overall score. This enables managers and platform engineers to identify specific services impacting metrics and troubleshoot issues at the entity level.
29
+
30
+ ### Patch Changes
31
+
32
+ - 4f483a6: Normalize entity owners to their full canonical reference when syncing. Catalog entities are able to define a short name or the full entity reference for group ownership. This can lead to inconsistent filtering as we utilize the full entity reference for users and groups
33
+ - 89b273f: Adds the ability to sort by status that was originally missed during the initial implementation of the drill down feature
34
+ - Updated dependencies [d706601]
35
+ - Updated dependencies [55226c2]
36
+ - Updated dependencies [243ad0a]
37
+ - Updated dependencies [c83b206]
38
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.5.0
39
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.5.0
40
+
41
+ ## 2.4.0
42
+
43
+ ### Minor Changes
44
+
45
+ - 7062658: Introduces custom threshold rule keys and colors that can be configured in `app-config.yaml`.
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies [7062658]
50
+ - Updated dependencies [dc5e31a]
51
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@2.4.0
52
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@2.4.0
53
+
3
54
  ## 2.3.5
4
55
 
5
56
  ### Patch Changes
package/README.md CHANGED
@@ -88,21 +88,27 @@ For more information about schedule configuration options, see the [Metric Colle
88
88
 
89
89
  The following metric providers are available:
90
90
 
91
- | Provider | Metric ID | Title | Description | Type |
92
- | ----------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ |
93
- | **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
94
- | **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
95
- | **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
91
+ | Provider | Metric ID | Title | Description | Type |
92
+ | -------------- | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ |
93
+ | **GitHub** | `github.open_prs` | GitHub open PRs | Count of open Pull Requests in GitHub | number |
94
+ | **Jira** | `jira.open_issues` | Jira open issues | The number of opened issues in Jira | number |
95
+ | **OpenSSF** | `openssf.*` | OpenSSF Security Scorecards | 18 security metrics from OpenSSF Scorecards (e.g., `openssf.code_review`, `openssf.maintained`). Each returns a score from 0-10. | number |
96
+ | **Dependabot** | `dependabot.*` | Dependabot Alerts | Critical, High, Medium and Low CVE Alerts | number |
96
97
 
97
98
  To use these providers, install the corresponding backend modules:
98
99
 
99
100
  - GitHub: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github`](../scorecard-backend-module-github/README.md)
100
101
  - Jira: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira`](../scorecard-backend-module-jira/README.md)
101
102
  - OpenSSF: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf`](../scorecard-backend-module-openssf/README.md)
103
+ - Dependabot: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot`](../scorecard-backend-module-dependabot/README.md)
104
+
105
+ ### Disabling Metrics
106
+
107
+ Administrators can disable metric checks globally via app-config, and users can disable them for concrete entity via `scorecard.io/disabled-metrics` annotation. For more details, see [disabled-metrics-logic.md](./docs/disabled-metrics-logic.md).
102
108
 
103
109
  ## Thresholds
104
110
 
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:
111
+ 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
112
 
107
113
  - **Provider Defaults**: Metric providers define default thresholds
108
114
  - **App Configuration**: Override defaults through `app-config.yaml`
@@ -112,6 +118,38 @@ Thresholds are evaluated in order, and the first matching rule determines the ca
112
118
 
113
119
  For comprehensive threshold configuration guide, examples, and best practices, see [thresholds.md](./docs/thresholds.md).
114
120
 
121
+ ## Aggregation KPIs (homepage and `GET /aggregations`)
122
+
123
+ Aggregated scorecard data for the authenticated user’s owned entities is exposed under **`GET /aggregations/:aggregationId`**. Optional entries in **`scorecard.aggregationKPIs`** assign a stable **aggregation id** (KPI key), custom **title** and **description**, **type**, and the backing **metricId**.
124
+
125
+ ```yaml
126
+ scorecard:
127
+ aggregationKPIs:
128
+ openIssuesKpi:
129
+ title: 'Jira open issues KPI'
130
+ description: 'Open issues across entities you own, grouped by status.'
131
+ type: statusGrouped
132
+ metricId: jira.open_issues
133
+ openPrsKpi:
134
+ title: 'GitHub open PRs KPI'
135
+ description: 'Open pull requests grouped by status.'
136
+ type: statusGrouped
137
+ metricId: github.open_prs
138
+ ```
139
+
140
+ | Field | Description |
141
+ | ------------- | -------------------------------------------------------------- |
142
+ | `title` | Display title for this aggregation (returned in API metadata). |
143
+ | `description` | Display description for this aggregation. |
144
+ | `type` | Aggregation algorithm; currently `statusGrouped`. |
145
+ | `metricId` | Metric provider id used to load thresholds and compute counts. |
146
+
147
+ - **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
148
+ - If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
149
+ - Invalid KPI entries may be logged and skipped at runtime; prefer fixing config per backend validation.
150
+
151
+ **Homepage cards** are configured in the app (for example Dynamic Home Page mount points). They should pass **`aggregationId`** matching a key in `aggregationKPIs` or the metric id for the default case. See the [Scorecard frontend plugin README](../scorecard/README.md#homepage-scorecard-cards).
152
+
115
153
  ## API Endpoints
116
154
 
117
155
  ### `GET /metrics`
@@ -177,9 +215,52 @@ curl -X GET "{{url}}/api/scorecard/metrics/catalog/component/default/my-service?
177
215
  -H "Authorization: Bearer <token>"
178
216
  ```
179
217
 
180
- ### `GET /metrics/:metricId/catalog/aggregations`
218
+ ### `GET /aggregations/:aggregationId`
219
+
220
+ Returns aggregated metrics for the authenticated user across all catalog entities they own (same ownership rules as the legacy route; see [aggregation.md](./docs/aggregation.md)).
221
+
222
+ The **`aggregationId`** is either:
223
+
224
+ - A key under **`scorecard.aggregationKPIs`** in app-config (KPI-specific title, description, type, and `metricId`), or
225
+ - The **metric id** itself when no KPI entry exists (default **statusGrouped** behavior).
226
+
227
+ #### Path Parameters
228
+
229
+ | Parameter | Type | Required | Description |
230
+ | --------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
231
+ | `aggregationId` | string | Yes | KPI id from `scorecard.aggregationKPIs` or the metric id for the default aggregation. |
232
+
233
+ #### Authentication / permissions
234
+
235
+ Same as the deprecated aggregation route: user authentication, `scorecard.metric.read`, and `catalog.entity.read` for each aggregated entity.
236
+
237
+ #### Example Request
238
+
239
+ ```bash
240
+ curl -X GET "{{url}}/api/scorecard/aggregations/openIssuesKpi" \
241
+ -H "Authorization: Bearer <token>"
242
+
243
+ # Default aggregation when no KPI is configured (id equals metric id)
244
+ curl -X GET "{{url}}/api/scorecard/aggregations/github.open_prs" \
245
+ -H "Authorization: Bearer <token>"
246
+ ```
247
+
248
+ ### `GET /aggregations/:aggregationId/metadata`
249
+
250
+ Returns **title**, **description**, **type**, **history**, and **aggregationType** for the aggregation without computing full aggregate counts. Uses the same resolution rules as `GET /aggregations/:aggregationId` (KPI config vs metric id fallback).
251
+
252
+ ```bash
253
+ curl -X GET "{{url}}/api/scorecard/aggregations/openIssuesKpi/metadata" \
254
+ -H "Authorization: Bearer <token>"
255
+ ```
256
+
257
+ ### `GET /metrics/:metricId/catalog/aggregations` (deprecated; removal planned)
258
+
259
+ This endpoint **remains available** for backward compatibility and behaves like the default case of **`GET /aggregations/:metricId`** (status-grouped aggregation for that metric). **It will be removed in a future major release** of the plugin - migrate to **`GET /aggregations/:aggregationId`**.
260
+
261
+ Responses include **RFC 8594** deprecation signals: header **`Deprecation: true`** and **`Link: <…/aggregations/<metricId>>; rel="alternate"`** pointing at the successor URL. The backend also logs a warning when this route is used.
181
262
 
182
- Returns aggregated metrics for a specific metric across all entities owned by the authenticated user. This endpoint aggregates metrics from:
263
+ Returns aggregated metrics for a specific metric across all entities owned by the authenticated user:
183
264
 
184
265
  - Entities directly owned by the user
185
266
  - Entities owned by groups the user is a direct member of (only direct parent groups are considered)
@@ -204,7 +285,7 @@ Requires `scorecard.metric.read` permission. Additionally:
204
285
  #### Example Request
205
286
 
206
287
  ```bash
207
- # Get aggregated metrics for a specific metric
288
+ # Deprecated prefer GET /aggregations/github.open_prs (or your KPI id)
208
289
  curl -X GET "{{url}}/api/scorecard/metrics/github.open_prs/catalog/aggregations" \
209
290
  -H "Authorization: Bearer <token>"
210
291
  ```
package/config.d.ts CHANGED
@@ -15,12 +15,39 @@
15
15
  */
16
16
 
17
17
  import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
18
+ import { AggregationType } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
18
19
 
19
20
  export interface Config {
20
21
  /** Configuration for scorecard plugin */
21
22
  scorecard?: {
23
+ /** Configuration for scorecard aggregation KPIs */
24
+ aggregationKPIs?: {
25
+ /** Unique identifier for scorecard aggregation KPIs */
26
+ [aggregationId: string]: {
27
+ /** Title of the aggregation */
28
+ title: string;
29
+ /** Description of the aggregation */
30
+ description: string;
31
+ /** Type of the aggregation */
32
+ type: AggregationType;
33
+ /** Metric ID for which the aggregation is calculated */
34
+ metricId: string;
35
+ };
36
+ };
22
37
  /** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
23
38
  dataRetentionDays?: number;
39
+ /** List of metric IDs (e.g. openssf.packaging) that are disabled globally. Entity annotations cannot override this. */
40
+ disabledMetrics?: string[];
41
+ /** Control whether users can override behavior via entity annotations. */
42
+ entityAnnotations?: {
43
+ /** Whether entity scorecard.io/disabled-metrics annotation can override. Only affects annotations; global disabledMetrics is unchanged. */
44
+ disabledMetrics?: {
45
+ /** If true (default), entities can disable metrics that are not mentioned in `except` list via `scorecard.io/disabled-metrics` annotation; if false, the annotation has no effect */
46
+ enabled?: boolean;
47
+ /** When enabled is true: entity annotations cannot disable metric IDs listed here (these checks always run). */
48
+ except?: string[];
49
+ };
50
+ };
24
51
  /** Configuration for scorecard metric providers */
25
52
  plugins?: {
26
53
  /** Configuration for datasource */
@@ -31,12 +58,58 @@ export interface Config {
31
58
  [metricName: string]: {
32
59
  /** Threshold configuration for the metric */
33
60
  thresholds?: {
61
+ /**
62
+ * Rules describe how metric values are categorized and how that category is presented in the UI.
63
+ * They are evaluated in order and the first matching rule is applied.
64
+ */
34
65
  rules?: Array<{
35
- key: 'error' | 'warning' | 'success';
36
- /** Threshold expression - supports: >=, <=, >, <, ==, !=, - (range) */
66
+ /**
67
+ * Threshold category key that a metric value is assigned to when this rule
68
+ * matches (for example `success`, `warning`, `error`, or a custom key).
69
+ */
70
+ key: string;
71
+ /**
72
+ * Threshold expression that determines whether a metric value matches this
73
+ * rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
74
+ *
75
+ * @example `<= 10` - Metric value must be less than or equal to 10.
76
+ * @example `10-60` - Metric value must be between 10 and 60 (inclusive).
77
+ */
37
78
  expression: string;
79
+ /**
80
+ * Color configuration - supports multiple formats:
81
+ * - theme palette reference (`success.main` / `warning.main` / `error.main`)
82
+ * - HEX code (e.g. '#FFA500')
83
+ * - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
84
+ *
85
+ * Threshold rules 'success', 'warning' and 'error' have default colors.
86
+ */
87
+ color?: string;
88
+ /**
89
+ * Icon configuration - supports multiple formats:
90
+ * - Backstage system icons: 'kind:component', 'kind:api', etc.
91
+ * - Material Design icons: 'settings', 'home', 'build', etc.
92
+ * - SVG strings: '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
93
+ * - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
94
+ * - Data URIs: 'data:image/svg+xml;base64,...'
95
+ *
96
+ * Threshold rules 'success', 'warning' and 'error' have default icons.
97
+ */
98
+ icon?: string;
38
99
  }>;
39
100
  };
101
+ /**
102
+ * Schedule for collecting this metric. If not set, the default hourly schedule is used.
103
+ *
104
+ * Default schedule:
105
+ * ```ts
106
+ * {
107
+ * frequency: { hours: 1 },
108
+ * timeout: { minutes: 15 },
109
+ * initialDelay: { minutes: 1 },
110
+ * }
111
+ * ```
112
+ */
40
113
  schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
41
114
  };
42
115
  };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const AGGREGATION_KPIS_CONFIG_PATH = "scorecard.aggregationKPIs";
4
+
5
+ exports.AGGREGATION_KPIS_CONFIG_PATH = AGGREGATION_KPIS_CONFIG_PATH;
6
+ //# sourceMappingURL=aggregationKPIs.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n"],"names":[],"mappings":";;AAgBO,MAAM,4BACX,GAAA;;;;"}
@@ -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
@@ -33,39 +34,102 @@ class DatabaseMetricValues {
33
34
  * Get aggregated metrics by status for multiple entities and metrics.
34
35
  */
35
36
  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);
37
+ const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("catalog_entity_ref");
38
+ 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");
39
+ if (!statusRows || statusRows.length === 0) {
40
+ return void 0;
41
+ }
42
+ const normalizeTimestamp = (timestamp) => {
43
+ if (timestamp instanceof Date) {
44
+ return timestamp;
45
+ } else if (typeof timestamp === "number" || typeof timestamp === "string") {
46
+ return new Date(timestamp);
47
+ }
48
+ return /* @__PURE__ */ new Date();
49
+ };
50
+ let maxTimestamp = /* @__PURE__ */ new Date(0);
51
+ let total = 0;
52
+ const statusCounts = {};
53
+ for (const row of statusRows) {
54
+ const rowTimestamp = normalizeTimestamp(row.max_timestamp);
55
+ if (rowTimestamp > maxTimestamp) {
56
+ maxTimestamp = rowTimestamp;
57
+ }
58
+ const name = row.status;
59
+ const count = Number(row.count);
60
+ statusCounts[name] = count;
61
+ total += count;
62
+ }
63
+ return {
64
+ metric_id,
65
+ total,
66
+ max_timestamp: maxTimestamp,
67
+ statusCounts
68
+ };
69
+ }
70
+ /**
71
+ * Fetch the latest entity metric values for a given metric, with optional filtering
72
+ * by status, name, kind, namespace, or owner, plus sorting and pagination.
73
+ */
74
+ async readEntityMetricsWithFilters(metric_id, options) {
75
+ const clientName = this.dbClient.client?.config?.client ?? "";
76
+ const isPostgres = clientName === "pg" || clientName.includes("postgres");
77
+ const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).groupBy("catalog_entity_ref");
78
+ const query = this.dbClient(this.tableName).select("*").whereIn("id", latestIdsSubquery);
79
+ const sortColumnMap = {
80
+ entityName: "catalog_entity_ref",
81
+ owner: "entity_owner",
82
+ entityKind: "entity_kind",
83
+ timestamp: "timestamp",
84
+ metricValue: "value",
85
+ namespace: "entity_namespace",
86
+ status: "status"
87
+ };
88
+ const column = (options.sortBy && sortColumnMap[options.sortBy]) ?? "timestamp";
89
+ const direction = options.sortOrder === "asc" ? "asc" : "desc";
90
+ this.applySort(query, options.sortBy, column, direction, isPostgres);
91
+ if (options.status) {
92
+ query.where("status", options.status);
93
+ }
94
+ if (options.entityName) {
95
+ const escaped = options.entityName.replace(/[%_\\]/g, "\\$&");
96
+ query.whereRaw("catalog_entity_ref LIKE ? ESCAPE '\\'", [`%${escaped}%`]);
97
+ }
98
+ if (options.entityKind) {
99
+ query.where("entity_kind", options.entityKind);
100
+ }
101
+ if (options.entityNamespace) {
102
+ query.where("entity_namespace", options.entityNamespace);
103
+ }
104
+ if (options.entityOwner && options.entityOwner.length > 0) {
105
+ query.whereIn("entity_owner", options.entityOwner);
106
+ }
107
+ if (options.pagination) {
108
+ query.limit(options.pagination.limit).offset(options.pagination.offset);
109
+ }
110
+ return await query;
111
+ }
112
+ applySort(query, sortBy, column, direction, isPostgres) {
113
+ if (sortBy === "metricValue") {
114
+ if (isPostgres) {
115
+ query.orderByRaw(
116
+ `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`
117
+ );
118
+ } else {
119
+ query.orderByRaw(
120
+ `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`
121
+ );
122
+ }
123
+ } else if (sortBy === "status") {
124
+ if (isPostgres) {
125
+ query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);
56
126
  } else {
57
- maxTimestamp = /* @__PURE__ */ new Date();
127
+ query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);
58
128
  }
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
- };
129
+ } else {
130
+ query.orderBy(column, direction);
131
+ query.orderBy("id", "asc");
67
132
  }
68
- return void 0;
69
133
  }
70
134
  }
71
135
 
@@ -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\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 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 /**\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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"}
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var errors = require('@backstage/errors');
5
+
6
+ function validateAggregationIdParam(req, _res, next) {
7
+ const schema = zod.z.object({
8
+ aggregationId: zod.z.string().min(1).max(255)
9
+ });
10
+ const parsed = schema.safeParse(req.params);
11
+ if (!parsed.success) {
12
+ throw new errors.InputError(
13
+ `Invalid aggregationId parameter: ${parsed.error.message}`
14
+ );
15
+ }
16
+ next();
17
+ }
18
+
19
+ exports.validateAggregationIdParam = validateAggregationIdParam;
20
+ //# sourceMappingURL=validateAggregationIdParam.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateAggregationIdParam.cjs.js","sources":["../../src/middlewares/validateAggregationIdParam.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 { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateAggregationIdParam(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n aggregationId: z.string().min(1).max(255),\n });\n\n const parsed = schema.safeParse(req.params);\n\n if (!parsed.success) {\n throw new InputError(\n `Invalid aggregationId parameter: ${parsed.error.message}`,\n );\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,0BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,aAAA,EAAeA,MAAE,MAAO,EAAA,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,GAAG;AAAA,GACzC,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,MAAM,CAAA;AAE1C,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,CAAA,iCAAA,EAAoC,MAAO,CAAA,KAAA,CAAM,OAAO,CAAA;AAAA,KAC1D;AAAA;AAGF,EAAK,IAAA,EAAA;AACP;;;;"}
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var errors = require('@backstage/errors');
5
+
6
+ function validateDatasourceQueryParams(req, _res, next) {
7
+ const schema = zod.z.object({
8
+ datasource: zod.z.string().min(1).max(255).optional()
9
+ });
10
+ const parsed = schema.safeParse(req.query);
11
+ if (!parsed.success) {
12
+ throw new errors.InputError(`Invalid query parameters: ${parsed.error.message}`);
13
+ }
14
+ next();
15
+ }
16
+
17
+ exports.validateDatasourceQueryParams = validateDatasourceQueryParams;
18
+ //# sourceMappingURL=validateDatasourceQueryParams.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateDatasourceQueryParams.cjs.js","sources":["../../src/middlewares/validateDatasourceQueryParams.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 { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateDatasourceQueryParams(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n datasource: z.string().min(1).max(255).optional(),\n });\n\n const parsed = schema.safeParse(req.query);\n\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,6BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,UAAA,EAAYA,KAAE,CAAA,MAAA,EAAS,CAAA,GAAA,CAAI,CAAC,CAAE,CAAA,GAAA,CAAI,GAAG,CAAA,CAAE,QAAS;AAAA,GACjD,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,KAAK,CAAA;AAEzC,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAW,CAAA,CAAA,0BAAA,EAA6B,MAAO,CAAA,KAAA,CAAM,OAAO,CAAE,CAAA,CAAA;AAAA;AAG1E,EAAK,IAAA,EAAA;AACP;;;;"}
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var errors = require('@backstage/errors');
5
+
6
+ function validateMetricIdsQueryParams(req, _res, next) {
7
+ const schema = zod.z.object({
8
+ metricIds: zod.z.string().min(1).max(255).optional()
9
+ });
10
+ const parsed = schema.safeParse(req.query);
11
+ if (!parsed.success) {
12
+ throw new errors.InputError(`Invalid query parameters: ${parsed.error.message}`);
13
+ }
14
+ next();
15
+ }
16
+
17
+ exports.validateMetricIdsQueryParams = validateMetricIdsQueryParams;
18
+ //# sourceMappingURL=validateMetricIdsQueryParams.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateMetricIdsQueryParams.cjs.js","sources":["../../src/middlewares/validateMetricIdsQueryParams.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 { z } from 'zod';\nimport { InputError } from '@backstage/errors';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport function validateMetricIdsQueryParams(\n req: Request,\n _res: Response,\n next: NextFunction,\n): void {\n const schema = z.object({\n metricIds: z.string().min(1).max(255).optional(),\n });\n\n const parsed = schema.safeParse(req.query);\n\n if (!parsed.success) {\n throw new InputError(`Invalid query parameters: ${parsed.error.message}`);\n }\n\n next();\n}\n"],"names":["z","InputError"],"mappings":";;;;;AAoBgB,SAAA,4BAAA,CACd,GACA,EAAA,IAAA,EACA,IACM,EAAA;AACN,EAAM,MAAA,MAAA,GAASA,MAAE,MAAO,CAAA;AAAA,IACtB,SAAA,EAAWA,KAAE,CAAA,MAAA,EAAS,CAAA,GAAA,CAAI,CAAC,CAAE,CAAA,GAAA,CAAI,GAAG,CAAA,CAAE,QAAS;AAAA,GAChD,CAAA;AAED,EAAA,MAAM,MAAS,GAAA,MAAA,CAAO,SAAU,CAAA,GAAA,CAAI,KAAK,CAAA;AAEzC,EAAI,IAAA,CAAC,OAAO,OAAS,EAAA;AACnB,IAAA,MAAM,IAAIC,iBAAW,CAAA,CAAA,0BAAA,EAA6B,MAAO,CAAA,KAAA,CAAM,OAAO,CAAE,CAAA,CAAA;AAAA;AAG1E,EAAK,IAAA,EAAA;AACP;;;;"}
@@ -5,6 +5,30 @@ var errors = require('@backstage/errors');
5
5
  var alpha = require('@backstage/plugin-catalog-common/alpha');
6
6
  var rules = require('./rules.cjs.js');
7
7
 
8
+ const getUserEntityRef = async (credentials) => {
9
+ const userEntityRef = credentials?.principal?.userEntityRef;
10
+ if (!userEntityRef) {
11
+ throw new errors.AuthenticationError("User entity reference not found");
12
+ }
13
+ return userEntityRef;
14
+ };
15
+ const authorizeConditional = async (credentials, permissions, permission) => {
16
+ const scorecardPermission = {};
17
+ if (permission.type === "resource") {
18
+ scorecardPermission.decision = (await permissions.authorizeConditional([{ permission }], {
19
+ credentials
20
+ }))[0];
21
+ } else {
22
+ scorecardPermission.decision = (await permissions.authorize([{ permission }], {
23
+ credentials
24
+ }))[0];
25
+ }
26
+ if (scorecardPermission.decision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
27
+ throw new errors.NotAllowedError();
28
+ }
29
+ scorecardPermission.conditions = scorecardPermission.decision.result === pluginPermissionCommon.AuthorizeResult.CONDITIONAL ? scorecardPermission.decision.conditions : void 0;
30
+ return scorecardPermission;
31
+ };
8
32
  const checkEntityAccess = async (entityRef, req, permissions, httpAuth) => {
9
33
  const entityAccessDecision = await permissions.authorize(
10
34
  [{ permission: alpha.catalogEntityReadPermission, resourceRef: entityRef }],
@@ -36,7 +60,9 @@ const filterAuthorizedMetrics = (metrics, filter) => {
36
60
  return metrics.filter((metric) => matches(metric, filter));
37
61
  };
38
62
 
63
+ exports.authorizeConditional = authorizeConditional;
39
64
  exports.checkEntityAccess = checkEntityAccess;
40
65
  exports.filterAuthorizedMetrics = filterAuthorizedMetrics;
66
+ exports.getUserEntityRef = getUserEntityRef;
41
67
  exports.matches = matches;
42
68
  //# sourceMappingURL=permissionUtils.cjs.js.map