@red-hat-developer-hub/backstage-plugin-scorecard-backend 4.0.0 → 4.2.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 +30 -0
- package/README.md +66 -12
- package/config.d.ts +62 -24
- package/dist/database/DatabaseMetricValues.cjs.js +147 -46
- package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
- package/dist/database/utils/getAggregateExpression.cjs.js +21 -0
- package/dist/database/utils/getAggregateExpression.cjs.js.map +1 -0
- package/dist/database/utils/mapMetricValueRow.cjs.js +33 -0
- package/dist/database/utils/mapMetricValueRow.cjs.js.map +1 -0
- package/dist/plugin.cjs.js +4 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +24 -24
- package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +10 -10
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js +12 -0
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js.map +1 -1
- package/dist/service/aggregations/{AggregationService.cjs.js → AggregationsService.cjs.js} +25 -9
- package/dist/service/aggregations/AggregationsService.cjs.js.map +1 -0
- package/dist/service/aggregations/strategies/ScalarAggregationStrategy.cjs.js +51 -0
- package/dist/service/aggregations/strategies/ScalarAggregationStrategy.cjs.js.map +1 -0
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js +6 -0
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js.map +1 -1
- package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js +10 -12
- package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js.map +1 -1
- package/dist/service/aggregations/strategies/registerStrategies.cjs.js +21 -0
- package/dist/service/aggregations/strategies/registerStrategies.cjs.js.map +1 -1
- package/dist/service/mappers.cjs.js +26 -9
- package/dist/service/mappers.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +14 -5
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/utils/aggregation/buildAggregationConfig.cjs.js +33 -0
- package/dist/utils/aggregation/buildAggregationConfig.cjs.js.map +1 -0
- package/dist/utils/aggregation/buildAggregationConfigFilter.cjs.js +16 -0
- package/dist/utils/aggregation/buildAggregationConfigFilter.cjs.js.map +1 -0
- package/dist/utils/aggregation/buildAggregationConfigThresholds.cjs.js +18 -0
- package/dist/utils/aggregation/buildAggregationConfigThresholds.cjs.js.map +1 -0
- package/dist/utils/aggregation/buildAggregationStatusScores.cjs.js +13 -0
- package/dist/utils/aggregation/buildAggregationStatusScores.cjs.js.map +1 -0
- package/dist/utils/aggregation/isScalarAggregationConfig.cjs.js +10 -0
- package/dist/utils/aggregation/isScalarAggregationConfig.cjs.js.map +1 -0
- package/dist/utils/aggregation/isScalarAggregationType.cjs.js +10 -0
- package/dist/utils/aggregation/isScalarAggregationType.cjs.js.map +1 -0
- package/dist/utils/aggregation/parseValidatedAggregationConfig.cjs.js +21 -0
- package/dist/utils/aggregation/parseValidatedAggregationConfig.cjs.js.map +1 -0
- package/dist/utils/mergeMaxTimestamp.cjs.js +8 -0
- package/dist/utils/mergeMaxTimestamp.cjs.js.map +1 -0
- package/dist/utils/metricCalculationError.cjs.js +1 -1
- package/dist/utils/metricCalculationError.cjs.js.map +1 -1
- package/dist/utils/normalizeTimestamp.cjs.js +17 -0
- package/dist/utils/normalizeTimestamp.cjs.js.map +1 -0
- package/dist/validation/schemas/aggregationConfigSchemas.cjs.js +19 -10
- package/dist/validation/schemas/aggregationConfigSchemas.cjs.js.map +1 -1
- package/dist/validation/schemas/aggregationFilterSchema.cjs.js +10 -0
- package/dist/validation/schemas/aggregationFilterSchema.cjs.js.map +1 -0
- package/dist/validation/schemas/aggregationThresholdsConfigSchema.cjs.js +16 -0
- package/dist/validation/schemas/aggregationThresholdsConfigSchema.cjs.js.map +1 -0
- package/dist/validation/validateAggregationConfig.cjs.js +19 -21
- package/dist/validation/validateAggregationConfig.cjs.js.map +1 -1
- package/dist/validation/validateScalarAggregationConfig.cjs.js +24 -0
- package/dist/validation/validateScalarAggregationConfig.cjs.js.map +1 -0
- package/dist/validation/validateScalarFilterStatus.cjs.js +22 -0
- package/dist/validation/validateScalarFilterStatus.cjs.js.map +1 -0
- package/package.json +3 -3
- package/dist/service/aggregations/AggregationService.cjs.js.map +0 -1
- package/dist/utils/buildAggregationConfig.cjs.js +0 -45
- package/dist/utils/buildAggregationConfig.cjs.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e486f80: Implemented filter by `status` for scalar aggregation types (`sum`, `average`, `count`, `min`, `max`).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [e486f80]
|
|
12
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@4.2.0
|
|
13
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@4.2.0
|
|
14
|
+
|
|
15
|
+
## 4.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 3af0fb2: This update introduces new scalar aggregation KPIs in the scorecard configuration, including:
|
|
20
|
+
|
|
21
|
+
- **`sum`**: Single numeric total of latest metric values across owned entities
|
|
22
|
+
- **`average`**: Mean of latest metric values across owned entities
|
|
23
|
+
- **`max`**: Maximum latest metric value across owned entities
|
|
24
|
+
- **`min`**: Minimum latest metric value across owned entities
|
|
25
|
+
- **`count`**: Number of entities with a non-null latest stored value
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [3af0fb2]
|
|
30
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@4.1.0
|
|
31
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@4.1.0
|
|
32
|
+
|
|
3
33
|
## 4.0.0
|
|
4
34
|
|
|
5
35
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -122,11 +122,23 @@ Thresholds define conditions to assign metric values to specific visual categori
|
|
|
122
122
|
- **App Configuration**: Override defaults through `app-config.yaml`
|
|
123
123
|
- **Entity Annotations**: Override specific thresholds per entity using catalog annotations
|
|
124
124
|
|
|
125
|
-
Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`). For **number** metrics, configurations loaded through validated paths must cover the **entire real line** when two or more rules are defined (no gaps between intervals)
|
|
125
|
+
Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`). For **number** metrics, configurations loaded through validated paths must cover the **entire real line** when two or more rules are defined (no gaps between intervals).
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Thresholds can also be configured for some aggregation homepage cards. To configure card thresholds, use the `app-config.yaml` file and provide configuration under `scorecard.aggregationKPIs.<aggregationId>.options.thresholds`. Check the table below to see which aggregation types support thresholds:
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
| Aggregation Type | Type ID | Is configuration supported |
|
|
130
|
+
| --------------------- | --------------------- | -------------------------- |
|
|
131
|
+
| Status Grouped | `statusGrouped` | **NO** |
|
|
132
|
+
| Weighted Status Score | `weightedStatusScore` | **YES** |
|
|
133
|
+
| Sum | `sum` | **YES** |
|
|
134
|
+
| Average | `average` | **YES** |
|
|
135
|
+
| Max | `max` | **YES** |
|
|
136
|
+
| Min | `min` | **YES** |
|
|
137
|
+
| Count | `count` | **YES** |
|
|
138
|
+
|
|
139
|
+
For a comprehensive threshold configuration guide, examples, best practices, interval validation, and **aggregation KPI result thresholds**, see [thresholds.md](./docs/thresholds.md).
|
|
140
|
+
|
|
141
|
+
## Aggregation KPIs
|
|
130
142
|
|
|
131
143
|
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**.
|
|
132
144
|
|
|
@@ -165,19 +177,59 @@ scorecard:
|
|
|
165
177
|
- key: error
|
|
166
178
|
expression: '<10'
|
|
167
179
|
color: error.main
|
|
180
|
+
totalCriticalPRs:
|
|
181
|
+
title: Total Critical PRs
|
|
182
|
+
description: Sum of open PRs for entities in error status
|
|
183
|
+
type: sum
|
|
184
|
+
metricId: github.openPRs
|
|
185
|
+
filter:
|
|
186
|
+
status: error
|
|
187
|
+
options:
|
|
188
|
+
thresholds:
|
|
189
|
+
rules:
|
|
190
|
+
- key: success
|
|
191
|
+
expression: '<30'
|
|
192
|
+
color: '#6bb300'
|
|
193
|
+
- key: warning
|
|
194
|
+
expression: '30-80'
|
|
195
|
+
color: 'rgb(224, 189, 108)'
|
|
196
|
+
- key: error
|
|
197
|
+
expression: '>80'
|
|
198
|
+
color: '#be1ec7'
|
|
199
|
+
avgOpenIssues:
|
|
200
|
+
title: Average Open Issues
|
|
201
|
+
description: Mean open issues count per entity
|
|
202
|
+
type: average
|
|
203
|
+
metricId: jira.openIssues
|
|
204
|
+
entitiesWithOpenPrs:
|
|
205
|
+
title: Entities with Open PRs
|
|
206
|
+
description: Count of entities with a stored open-prs value
|
|
207
|
+
type: count
|
|
208
|
+
metricId: github.openPRs
|
|
209
|
+
maxOpenPrs:
|
|
210
|
+
title: Maximum Open PRs
|
|
211
|
+
description: Maximum open PR count per entity
|
|
212
|
+
type: max
|
|
213
|
+
metricId: github.openPRs
|
|
214
|
+
minOpenIssues:
|
|
215
|
+
title: Minimum Open Issues
|
|
216
|
+
description: Minimum open issues count per entity
|
|
217
|
+
type: min
|
|
218
|
+
metricId: jira.openIssues
|
|
168
219
|
```
|
|
169
220
|
|
|
170
|
-
| Field | Description
|
|
171
|
-
| ------------- |
|
|
172
|
-
| `title` | Display title for this aggregation (returned in API metadata).
|
|
173
|
-
| `description` | Display description for this aggregation.
|
|
174
|
-
| `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status)
|
|
175
|
-
| `metricId` | Metric provider id used to load thresholds and compute counts.
|
|
176
|
-
| `
|
|
221
|
+
| Field | Description |
|
|
222
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `title` | Display title for this aggregation (returned in API metadata). |
|
|
224
|
+
| `description` | Display description for this aggregation. |
|
|
225
|
+
| `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status), `weightedStatusScore` (normalized weighted score), or a **scalar** type — `sum`, `average`, `max`, `min`, `count` (rollup of latest numeric metric values; see [aggregation.md](./docs/aggregation.md#aggregation-types)). Scalar types require a **number** metric. |
|
|
226
|
+
| `metricId` | Metric provider id used to load thresholds and compute counts or values. |
|
|
227
|
+
| `filter` | Optional for **scalar types** only (silently ignored on **`statusGrouped`** / **`weightedStatusScore`**). **`filter.status`**: threshold status key (case-sensitive). Only latest rows with that status contribute to **`value`** and **`total`**. Must match a threshold rule **`key`** for **`metricId`** from provider defaults or app-config overrides at **`scorecard.metricProviders.<datasource>.<providerName>.metrics.<metricName>.thresholds`** (or provider-level **`scorecard.metricProviders.<datasource>.<providerName>.thresholds`**) (validated at startup for scalar types). **`entitiesConsidered`** and **`calculationErrorCount`** stay full-portfolio. Returned in API **`metadata.filter`** when set. |
|
|
228
|
+
| `options` | **Optional:** extra configuration attributes required to further configure the aggregated card for a specific type |
|
|
177
229
|
|
|
178
230
|
- **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
|
|
179
231
|
- 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.openPRs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
|
|
180
|
-
- **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`weightedStatusScore`** KPIs without **`options.statusScores`**, bad expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation).
|
|
232
|
+
- **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`weightedStatusScore`** KPIs without **`options.statusScores`**, non-count scalar types on boolean metrics, invalid **`filter.status`** keys on scalar types, bad threshold expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation).
|
|
181
233
|
|
|
182
234
|
**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).
|
|
183
235
|
|
|
@@ -250,6 +302,8 @@ curl -X GET "{{url}}/api/scorecard/metrics/catalog/component/default/my-service?
|
|
|
250
302
|
|
|
251
303
|
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)).
|
|
252
304
|
|
|
305
|
+
Response **`result`** shape depends on **`metadata.aggregationType`**: status counts for **`statusGrouped`**, weighted score fields for **`weightedStatusScore`**, or scalar fields for **`sum`** / **`average`** / **`max`** / **`min`** / **`count`** — see [Scalar result fields](./docs/aggregation.md#scalar-result-fields). Scalar KPIs may also return **`metadata.filter`** when **`filter.status`** is configured.
|
|
306
|
+
|
|
253
307
|
The **`aggregationId`** is either:
|
|
254
308
|
|
|
255
309
|
- A key under **`scorecard.aggregationKPIs`** in app-config (KPI-specific title, description, type, and `metricId`), or
|
|
@@ -278,7 +332,7 @@ curl -X GET "{{url}}/api/scorecard/aggregations/github.openPRs" \
|
|
|
278
332
|
|
|
279
333
|
### `GET /aggregations/:aggregationId/metadata`
|
|
280
334
|
|
|
281
|
-
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).
|
|
335
|
+
Returns **title**, **description**, **type**, **history**, and **aggregationType** for the aggregation without computing full aggregate counts. Includes **`filter`** when the KPI is a scalar type with **`filter.status`** configured. Uses the same resolution rules as `GET /aggregations/:aggregationId` (KPI config vs metric id fallback).
|
|
282
336
|
|
|
283
337
|
```bash
|
|
284
338
|
curl -X GET "{{url}}/api/scorecard/aggregations/openIssuesKpi/metadata" \
|
package/config.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
aggregationTypes,
|
|
20
20
|
ThresholdConfig,
|
|
21
21
|
AggregationThresholdRule,
|
|
22
22
|
} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
|
|
@@ -27,30 +27,68 @@ export interface Config {
|
|
|
27
27
|
/** Configuration for scorecard aggregation KPIs */
|
|
28
28
|
aggregationKPIs?: {
|
|
29
29
|
/** Unique identifier for scorecard aggregation KPIs */
|
|
30
|
-
[aggregationId: string]:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
[aggregationId: string]:
|
|
31
|
+
| {
|
|
32
|
+
/** Title of the aggregation */
|
|
33
|
+
title: string;
|
|
34
|
+
/** Description of the aggregation */
|
|
35
|
+
description: string;
|
|
36
|
+
/** Metric ID for which the aggregation is calculated */
|
|
37
|
+
metricId: string;
|
|
38
|
+
/** Status grouped aggregation type */
|
|
39
|
+
type: typeof aggregationTypes.statusGrouped;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
/** Title of the aggregation */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Description of the aggregation */
|
|
45
|
+
description: string;
|
|
46
|
+
/** Metric ID for which the aggregation is calculated */
|
|
47
|
+
metricId: string;
|
|
48
|
+
/** Weighted status score aggregation type */
|
|
49
|
+
type: typeof aggregationTypes.weightedStatusScore;
|
|
50
|
+
/** Options specific to the weighted status score aggregation type */
|
|
51
|
+
options: {
|
|
52
|
+
/** Required: Status scores for the aggregation */
|
|
53
|
+
statusScores: {
|
|
54
|
+
[thresholdRuleKey: string]: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Optional: threshold rules for coloring the KPI headline value from the aggregation result
|
|
58
|
+
* (e.g. weighted status score percentage 0–100).
|
|
59
|
+
*/
|
|
60
|
+
thresholds?: {
|
|
61
|
+
rules: AggregationThresholdRule[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
/** Title of the aggregation */
|
|
67
|
+
title: string;
|
|
68
|
+
/** Description of the aggregation */
|
|
69
|
+
description: string;
|
|
70
|
+
/** Metric ID for which the aggregation is calculated */
|
|
71
|
+
metricId: string;
|
|
72
|
+
/** Scalar aggregation type */
|
|
73
|
+
type:
|
|
74
|
+
| typeof aggregationTypes.sum
|
|
75
|
+
| typeof aggregationTypes.average
|
|
76
|
+
| typeof aggregationTypes.max
|
|
77
|
+
| typeof aggregationTypes.min
|
|
78
|
+
| typeof aggregationTypes.count;
|
|
79
|
+
/** Optional: filter applied when aggregating scalar KPI values */
|
|
80
|
+
filter?: {
|
|
81
|
+
/** Threshold status key to include (e.g. success, warning, error) */
|
|
82
|
+
status?: string;
|
|
83
|
+
};
|
|
84
|
+
/** Options specific to the scalar aggregation type */
|
|
85
|
+
options?: {
|
|
86
|
+
/** Optional: threshold rules for coloring the KPI headline value from the aggregation result */
|
|
87
|
+
thresholds?: {
|
|
88
|
+
rules: AggregationThresholdRule[];
|
|
89
|
+
};
|
|
90
|
+
};
|
|
51
91
|
};
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
92
|
};
|
|
55
93
|
/** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
|
|
56
94
|
dataRetentionDays?: number;
|
|
@@ -1,11 +1,93 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var normalizeTimestamp = require('../utils/normalizeTimestamp.cjs.js');
|
|
4
|
+
var mergeMaxTimestamp = require('../utils/mergeMaxTimestamp.cjs.js');
|
|
5
|
+
var getAggregateExpression = require('./utils/getAggregateExpression.cjs.js');
|
|
6
|
+
var mapMetricValueRow = require('./utils/mapMetricValueRow.cjs.js');
|
|
7
|
+
|
|
3
8
|
class DatabaseMetricValues {
|
|
4
9
|
constructor(dbClient) {
|
|
5
10
|
this.dbClient = dbClient;
|
|
6
11
|
}
|
|
7
12
|
dbClient;
|
|
8
13
|
tableName = "metric_values";
|
|
14
|
+
/**
|
|
15
|
+
* `value` is a JSON column. Depending on database/driver, a "missing" metric value can
|
|
16
|
+
* arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).
|
|
17
|
+
*/
|
|
18
|
+
static metricValueIsMissingExpr = "(value IS NULL OR CAST(value AS TEXT) = 'null')";
|
|
19
|
+
/**
|
|
20
|
+
* Get the latest ids subquery for a given metric and catalog entity refs
|
|
21
|
+
*/
|
|
22
|
+
getLatestIdsSubquery(metricId, catalogEntityRefs) {
|
|
23
|
+
return this.dbClient(this.tableName).max("id").where("metric_id", metricId).whereIn("catalog_entity_ref", catalogEntityRefs).groupBy("catalog_entity_ref");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the stats row for a given latest ids subquery
|
|
27
|
+
*/
|
|
28
|
+
async readStatsRowByLatestIdsSubquery(latestIdsSubquery) {
|
|
29
|
+
const statsRow = await this.dbClient(this.tableName).whereIn("id", latestIdsSubquery).select(
|
|
30
|
+
this.dbClient.raw("COUNT(*) as latest_row_count"),
|
|
31
|
+
this.dbClient.raw(
|
|
32
|
+
`SUM(CASE WHEN error_message IS NOT NULL AND ${DatabaseMetricValues.metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`
|
|
33
|
+
),
|
|
34
|
+
this.dbClient.raw("MAX(timestamp) as max_timestamp")
|
|
35
|
+
).first();
|
|
36
|
+
const latestRowCount = Number(
|
|
37
|
+
statsRow?.latest_row_count ?? 0
|
|
38
|
+
);
|
|
39
|
+
const calculationErrorCount = Number(
|
|
40
|
+
statsRow?.calculation_error_count ?? 0
|
|
41
|
+
);
|
|
42
|
+
const maxTimestampAllLatest = normalizeTimestamp.normalizeTimestamp(
|
|
43
|
+
statsRow?.max_timestamp
|
|
44
|
+
);
|
|
45
|
+
return {
|
|
46
|
+
latestIdsSubquery,
|
|
47
|
+
latestRowCount,
|
|
48
|
+
calculationErrorCount,
|
|
49
|
+
maxTimestampAllLatest
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async readScalarAggregationByLatestIdsSubquery(latestIdsSubquery, aggregationFn, filter) {
|
|
53
|
+
const clientName = this.dbClient.client?.config?.client ?? "";
|
|
54
|
+
const isPostgres = clientName === "pg" || clientName.includes("postgres");
|
|
55
|
+
const numericValueExpr = isPostgres ? "CAST(value::text AS DOUBLE PRECISION)" : "CAST(CAST(value AS TEXT) AS REAL)";
|
|
56
|
+
const aggregateExpression = getAggregateExpression.getAggregateExpression(
|
|
57
|
+
aggregationFn,
|
|
58
|
+
numericValueExpr
|
|
59
|
+
);
|
|
60
|
+
const aggregateQuery = this.dbClient(this.tableName).whereIn("id", latestIdsSubquery).whereRaw(`NOT ${DatabaseMetricValues.metricValueIsMissingExpr}`);
|
|
61
|
+
if (filter?.status && filter.status !== "") {
|
|
62
|
+
aggregateQuery.where("status", filter.status);
|
|
63
|
+
}
|
|
64
|
+
const aggregateRow = await aggregateQuery.select(
|
|
65
|
+
this.dbClient.raw(`${aggregateExpression} as value`),
|
|
66
|
+
this.dbClient.raw("COUNT(*) as total"),
|
|
67
|
+
this.dbClient.raw("MAX(timestamp) as max_timestamp")
|
|
68
|
+
).first();
|
|
69
|
+
const aggregateResult = {
|
|
70
|
+
value: 0,
|
|
71
|
+
total: 0,
|
|
72
|
+
maxTimestamp: /* @__PURE__ */ new Date(0)
|
|
73
|
+
};
|
|
74
|
+
if (aggregateRow) {
|
|
75
|
+
const rawValue = Number(aggregateRow.value);
|
|
76
|
+
const rawTotal = Number(aggregateRow.total);
|
|
77
|
+
aggregateResult.value = Number.isFinite(rawValue) ? rawValue : 0;
|
|
78
|
+
aggregateResult.total = Number.isFinite(rawTotal) ? rawTotal : 0;
|
|
79
|
+
if (aggregateResult.total > 0 && aggregateRow.max_timestamp !== null && aggregateRow.max_timestamp !== "") {
|
|
80
|
+
aggregateResult.maxTimestamp = normalizeTimestamp.normalizeTimestamp(
|
|
81
|
+
aggregateRow.max_timestamp
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
value: aggregateResult.value,
|
|
87
|
+
total: aggregateResult.total,
|
|
88
|
+
maxTimestamp: aggregateResult.maxTimestamp
|
|
89
|
+
};
|
|
90
|
+
}
|
|
9
91
|
/**
|
|
10
92
|
* Insert multiple metric values
|
|
11
93
|
*/
|
|
@@ -13,16 +95,19 @@ class DatabaseMetricValues {
|
|
|
13
95
|
if (metricValues.length === 0) {
|
|
14
96
|
return;
|
|
15
97
|
}
|
|
16
|
-
await this.dbClient(this.tableName).insert(
|
|
98
|
+
await this.dbClient(this.tableName).insert(
|
|
99
|
+
metricValues.map(mapMetricValueRow.toMetricValueRow)
|
|
100
|
+
);
|
|
17
101
|
}
|
|
18
102
|
/**
|
|
19
103
|
* Get the latest metric values for a specific entity and metrics
|
|
20
104
|
*/
|
|
21
|
-
async readLatestEntityMetricValues(
|
|
22
|
-
|
|
105
|
+
async readLatestEntityMetricValues(catalogEntityRef, metricIds) {
|
|
106
|
+
const rows = await this.dbClient(this.tableName).select("*").whereIn(
|
|
23
107
|
"id",
|
|
24
|
-
this.dbClient(this.tableName).max("id").whereIn("metric_id",
|
|
108
|
+
this.dbClient(this.tableName).max("id").whereIn("metric_id", metricIds).where("catalog_entity_ref", catalogEntityRef).groupBy("metric_id")
|
|
25
109
|
);
|
|
110
|
+
return rows.map(mapMetricValueRow.fromMetricValueRow);
|
|
26
111
|
}
|
|
27
112
|
/**
|
|
28
113
|
* Delete metric values that are older than the given date
|
|
@@ -33,56 +118,34 @@ class DatabaseMetricValues {
|
|
|
33
118
|
/**
|
|
34
119
|
* Get aggregated metrics by status for multiple entities and metrics.
|
|
35
120
|
*/
|
|
36
|
-
async readAggregatedMetricByEntityRefs(
|
|
37
|
-
if (
|
|
121
|
+
async readAggregatedMetricByEntityRefs(catalogEntityRefs, metricId) {
|
|
122
|
+
if (catalogEntityRefs.length === 0) {
|
|
38
123
|
return void 0;
|
|
39
124
|
}
|
|
40
|
-
const latestIdsSubquery = this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.dbClient.raw("COUNT(*) as latest_row_count"),
|
|
44
|
-
this.dbClient.raw(
|
|
45
|
-
`SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`
|
|
46
|
-
),
|
|
47
|
-
this.dbClient.raw("MAX(timestamp) as max_timestamp")
|
|
48
|
-
).first();
|
|
49
|
-
const latestRowCount = Number(
|
|
50
|
-
statsRow?.latest_row_count ?? 0
|
|
125
|
+
const latestIdsSubquery = this.getLatestIdsSubquery(
|
|
126
|
+
metricId,
|
|
127
|
+
catalogEntityRefs
|
|
51
128
|
);
|
|
129
|
+
const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } = await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);
|
|
52
130
|
if (latestRowCount === 0) {
|
|
53
131
|
return void 0;
|
|
54
132
|
}
|
|
55
|
-
const
|
|
56
|
-
statsRow?.calculation_error_count ?? 0
|
|
57
|
-
);
|
|
58
|
-
const normalizeTimestamp = (timestamp) => {
|
|
59
|
-
if (timestamp instanceof Date) {
|
|
60
|
-
return timestamp;
|
|
61
|
-
}
|
|
62
|
-
if (typeof timestamp === "number" || typeof timestamp === "string") {
|
|
63
|
-
return new Date(timestamp);
|
|
64
|
-
}
|
|
65
|
-
return /* @__PURE__ */ new Date();
|
|
66
|
-
};
|
|
67
|
-
const maxTimestampAllLatest = normalizeTimestamp(
|
|
68
|
-
statsRow?.max_timestamp
|
|
69
|
-
);
|
|
70
|
-
const statusRows = await this.dbClient(this.tableName).select("status").count("* as count").max("timestamp as max_timestamp").whereIn("id", latestIdsSubquery).whereNotNull("status").whereRaw(`NOT ${metricValueIsMissingExpr}`).groupBy("status");
|
|
133
|
+
const statusRows = await this.dbClient(this.tableName).select("status").count("* as count").max("timestamp as max_timestamp").whereIn("id", latestIdsSubquery).whereNotNull("status").whereRaw(`NOT ${DatabaseMetricValues.metricValueIsMissingExpr}`).groupBy("status");
|
|
71
134
|
if (!statusRows || statusRows.length === 0) {
|
|
72
135
|
return {
|
|
73
|
-
|
|
136
|
+
metricId,
|
|
74
137
|
total: 0,
|
|
75
|
-
|
|
138
|
+
maxTimestamp: maxTimestampAllLatest,
|
|
76
139
|
statusCounts: {},
|
|
77
|
-
|
|
78
|
-
|
|
140
|
+
calculationErrorCount,
|
|
141
|
+
latestEntityCount: latestRowCount
|
|
79
142
|
};
|
|
80
143
|
}
|
|
81
144
|
let maxTimestamp = /* @__PURE__ */ new Date(0);
|
|
82
145
|
let total = 0;
|
|
83
146
|
const statusCounts = {};
|
|
84
147
|
for (const row of statusRows) {
|
|
85
|
-
const rowTimestamp = normalizeTimestamp(row.max_timestamp);
|
|
148
|
+
const rowTimestamp = normalizeTimestamp.normalizeTimestamp(row.max_timestamp);
|
|
86
149
|
if (rowTimestamp > maxTimestamp) {
|
|
87
150
|
maxTimestamp = rowTimestamp;
|
|
88
151
|
}
|
|
@@ -91,24 +154,61 @@ class DatabaseMetricValues {
|
|
|
91
154
|
statusCounts[name] = count;
|
|
92
155
|
total += count;
|
|
93
156
|
}
|
|
94
|
-
const mergedMax =
|
|
157
|
+
const mergedMax = mergeMaxTimestamp.mergeMaxTimestamp(maxTimestampAllLatest, maxTimestamp);
|
|
95
158
|
return {
|
|
96
|
-
|
|
159
|
+
metricId,
|
|
97
160
|
total,
|
|
98
|
-
|
|
161
|
+
maxTimestamp: mergedMax,
|
|
99
162
|
statusCounts,
|
|
100
|
-
|
|
101
|
-
|
|
163
|
+
calculationErrorCount,
|
|
164
|
+
latestEntityCount: latestRowCount
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Aggregate raw metric values across latest rows for multiple entities.
|
|
169
|
+
*/
|
|
170
|
+
async readScalarAggregatedMetricByEntityRefs(catalogEntityRefs, metricId, aggregationFn, filter) {
|
|
171
|
+
if (catalogEntityRefs.length === 0) {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
const latestIdsSubquery = this.getLatestIdsSubquery(
|
|
175
|
+
metricId,
|
|
176
|
+
catalogEntityRefs
|
|
177
|
+
);
|
|
178
|
+
const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } = await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);
|
|
179
|
+
if (latestRowCount === 0) {
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
const {
|
|
183
|
+
value,
|
|
184
|
+
total,
|
|
185
|
+
maxTimestamp: aggregateMaxTimestamp
|
|
186
|
+
} = await this.readScalarAggregationByLatestIdsSubquery(
|
|
187
|
+
latestIdsSubquery,
|
|
188
|
+
aggregationFn,
|
|
189
|
+
filter
|
|
190
|
+
);
|
|
191
|
+
const mergedMax = mergeMaxTimestamp.mergeMaxTimestamp(
|
|
192
|
+
maxTimestampAllLatest,
|
|
193
|
+
aggregateMaxTimestamp
|
|
194
|
+
);
|
|
195
|
+
return {
|
|
196
|
+
metricId,
|
|
197
|
+
total,
|
|
198
|
+
maxTimestamp: mergedMax,
|
|
199
|
+
value,
|
|
200
|
+
calculationErrorCount,
|
|
201
|
+
latestEntityCount: latestRowCount
|
|
102
202
|
};
|
|
103
203
|
}
|
|
104
204
|
/**
|
|
105
205
|
* Fetch the latest entity metric values for a given metric, with optional filtering
|
|
106
206
|
* by status, name, kind, namespace, or owner, plus sorting and pagination.
|
|
107
207
|
*/
|
|
108
|
-
async readEntityMetricsWithFilters(
|
|
208
|
+
async readEntityMetricsWithFilters(metricId, options) {
|
|
109
209
|
const clientName = this.dbClient.client?.config?.client ?? "";
|
|
110
210
|
const isPostgres = clientName === "pg" || clientName.includes("postgres");
|
|
111
|
-
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id",
|
|
211
|
+
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metricId).groupBy("catalog_entity_ref");
|
|
112
212
|
const query = this.dbClient(this.tableName).select("*").whereIn("id", latestIdsSubquery);
|
|
113
213
|
const sortColumnMap = {
|
|
114
214
|
entityName: "catalog_entity_ref",
|
|
@@ -141,7 +241,8 @@ class DatabaseMetricValues {
|
|
|
141
241
|
if (options.pagination) {
|
|
142
242
|
query.limit(options.pagination.limit).offset(options.pagination.offset);
|
|
143
243
|
}
|
|
144
|
-
|
|
244
|
+
const rows = await query;
|
|
245
|
+
return rows.map(mapMetricValueRow.fromMetricValueRow);
|
|
145
246
|
}
|
|
146
247
|
applySort(query, sortBy, column, direction, isPostgres) {
|
|
147
248
|
if (sortBy === "metricValue") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n if (catalog_entity_refs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n // `value` is a JSON column. Depending on database/driver, a \"missing\" metric value can\n // arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).\n const metricValueIsMissingExpr =\n \"(value IS NULL OR CAST(value AS TEXT) = 'null')\";\n\n // One round-trip for latest-row count, calculation-error count, and max timestamp\n // (same latest-id set as the status breakdown query below).\n const statsRow = await this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .select(\n this.dbClient.raw('COUNT(*) as latest_row_count'),\n this.dbClient.raw(\n `SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`,\n ),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const latestRowCount = Number(\n (statsRow as { latest_row_count?: string | number } | undefined)\n ?.latest_row_count ?? 0,\n );\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const calculation_error_count = Number(\n (statsRow as { calculation_error_count?: string | number } | undefined)\n ?.calculation_error_count ?? 0,\n );\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: unknown): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n }\n if (typeof timestamp === 'number' || typeof timestamp === 'string') {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n const maxTimestampAllLatest = normalizeTimestamp(\n (statsRow as { max_timestamp?: unknown })?.max_timestamp,\n );\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereRaw(`NOT ${metricValueIsMissingExpr}`)\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return {\n metric_id,\n total: 0,\n max_timestamp: maxTimestampAllLatest,\n statusCounts: {},\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n const mergedMax =\n maxTimestampAllLatest.getTime() >= maxTimestamp.getTime()\n ? maxTimestampAllLatest\n : maxTimestamp;\n\n return {\n metric_id,\n total,\n max_timestamp: mergedMax,\n statusCounts,\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport type { AggregationConfigFilter } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n DbScalarAggregatedMetric,\n ScalarAggregationFn,\n} from './types';\nimport { normalizeTimestamp } from '../utils/normalizeTimestamp';\nimport { mergeMaxTimestamp } from '../utils/mergeMaxTimestamp';\nimport { getAggregateExpression } from './utils/getAggregateExpression';\nimport {\n fromMetricValueRow,\n toMetricValueRow,\n type MetricValueRowWithId,\n} from './utils/mapMetricValueRow';\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\ntype StatsRowResult = {\n latestIdsSubquery: Knex.QueryBuilder;\n latestRowCount: number;\n calculationErrorCount: number;\n maxTimestampAllLatest: Date;\n};\n\ntype ScalarAggregationRowResult = {\n value: number;\n total: number;\n maxTimestamp: Date;\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n /**\n * `value` is a JSON column. Depending on database/driver, a \"missing\" metric value can\n * arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).\n */\n private static readonly metricValueIsMissingExpr =\n \"(value IS NULL OR CAST(value AS TEXT) = 'null')\";\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Get the latest ids subquery for a given metric and catalog entity refs\n */\n private getLatestIdsSubquery(\n metricId: string,\n catalogEntityRefs: string[],\n ): Knex.QueryBuilder {\n return this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metricId)\n .whereIn('catalog_entity_ref', catalogEntityRefs)\n .groupBy('catalog_entity_ref');\n }\n\n /**\n * Get the stats row for a given latest ids subquery\n */\n private async readStatsRowByLatestIdsSubquery(\n latestIdsSubquery: Knex.QueryBuilder,\n ): Promise<StatsRowResult> {\n // One round-trip for latest-row count, calculation-error count, and max timestamp\n // (same latest-id set as the status breakdown query below).\n const statsRow = await this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .select(\n this.dbClient.raw('COUNT(*) as latest_row_count'),\n this.dbClient.raw(\n `SUM(CASE WHEN error_message IS NOT NULL AND ${DatabaseMetricValues.metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`,\n ),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const latestRowCount = Number(\n (statsRow as { latest_row_count?: string | number } | undefined)\n ?.latest_row_count ?? 0,\n );\n\n const calculationErrorCount = Number(\n (statsRow as { calculation_error_count?: string | number } | undefined)\n ?.calculation_error_count ?? 0,\n );\n\n const maxTimestampAllLatest = normalizeTimestamp(\n (statsRow as { max_timestamp?: unknown })?.max_timestamp,\n );\n\n return {\n latestIdsSubquery,\n latestRowCount,\n calculationErrorCount,\n maxTimestampAllLatest,\n };\n }\n\n private async readScalarAggregationByLatestIdsSubquery(\n latestIdsSubquery: Knex.QueryBuilder,\n aggregationFn: ScalarAggregationFn,\n filter?: AggregationConfigFilter,\n ): Promise<ScalarAggregationRowResult> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const numericValueExpr = isPostgres\n ? 'CAST(value::text AS DOUBLE PRECISION)'\n : 'CAST(CAST(value AS TEXT) AS REAL)';\n\n const aggregateExpression = getAggregateExpression(\n aggregationFn,\n numericValueExpr,\n );\n\n const aggregateQuery = this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .whereRaw(`NOT ${DatabaseMetricValues.metricValueIsMissingExpr}`);\n\n if (filter?.status && filter.status !== '') {\n aggregateQuery.where('status', filter.status);\n }\n\n const aggregateRow = await aggregateQuery\n .select(\n this.dbClient.raw(`${aggregateExpression} as value`),\n this.dbClient.raw('COUNT(*) as total'),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const aggregateResult = {\n value: 0,\n total: 0,\n maxTimestamp: new Date(0),\n };\n\n if (aggregateRow) {\n const rawValue = Number(aggregateRow.value);\n const rawTotal = Number(aggregateRow.total);\n aggregateResult.value = Number.isFinite(rawValue) ? rawValue : 0;\n aggregateResult.total = Number.isFinite(rawTotal) ? rawTotal : 0;\n // MAX(timestamp) is null when no rows contribute (e.g. filter matches nothing).\n // Keep epoch so mergeMaxTimestamp prefers the portfolio latest-row timestamp.\n if (\n aggregateResult.total > 0 &&\n aggregateRow.max_timestamp !== null &&\n aggregateRow.max_timestamp !== ''\n ) {\n aggregateResult.maxTimestamp = normalizeTimestamp(\n aggregateRow.max_timestamp,\n );\n }\n }\n\n return {\n value: aggregateResult.value,\n total: aggregateResult.total,\n maxTimestamp: aggregateResult.maxTimestamp,\n };\n }\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(\n metricValues.map(toMetricValueRow),\n );\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalogEntityRef: string,\n metricIds: string[],\n ): Promise<DbMetricValue[]> {\n const rows = await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metricIds)\n .where('catalog_entity_ref', catalogEntityRef)\n .groupBy('metric_id'),\n );\n\n return (rows as MetricValueRowWithId[]).map(fromMetricValueRow);\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 catalogEntityRefs: string[],\n metricId: string,\n ): Promise<DbAggregatedMetric | undefined> {\n if (catalogEntityRefs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.getLatestIdsSubquery(\n metricId,\n catalogEntityRefs,\n );\n const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } =\n await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);\n\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereRaw(`NOT ${DatabaseMetricValues.metricValueIsMissingExpr}`)\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return {\n metricId,\n total: 0,\n maxTimestamp: maxTimestampAllLatest,\n statusCounts: {},\n calculationErrorCount,\n latestEntityCount: latestRowCount,\n };\n }\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n const mergedMax = mergeMaxTimestamp(maxTimestampAllLatest, maxTimestamp);\n\n return {\n metricId,\n total,\n maxTimestamp: mergedMax,\n statusCounts,\n calculationErrorCount,\n latestEntityCount: latestRowCount,\n };\n }\n\n /**\n * Aggregate raw metric values across latest rows for multiple entities.\n */\n async readScalarAggregatedMetricByEntityRefs(\n catalogEntityRefs: string[],\n metricId: string,\n aggregationFn: ScalarAggregationFn,\n filter?: AggregationConfigFilter,\n ): Promise<DbScalarAggregatedMetric | undefined> {\n if (catalogEntityRefs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.getLatestIdsSubquery(\n metricId,\n catalogEntityRefs,\n );\n const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } =\n await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);\n\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const {\n value,\n total,\n maxTimestamp: aggregateMaxTimestamp,\n } = await this.readScalarAggregationByLatestIdsSubquery(\n latestIdsSubquery,\n aggregationFn,\n filter,\n );\n\n const mergedMax = mergeMaxTimestamp(\n maxTimestampAllLatest,\n aggregateMaxTimestamp,\n );\n\n return {\n metricId,\n total,\n maxTimestamp: mergedMax,\n value,\n calculationErrorCount,\n latestEntityCount: latestRowCount,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metricId: 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', metricId)\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 const rows = await query;\n return (rows as MetricValueRowWithId[]).map(fromMetricValueRow);\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":["normalizeTimestamp","getAggregateExpression","toMetricValueRow","fromMetricValueRow","mergeMaxTimestamp"],"mappings":"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"}
|