@red-hat-developer-hub/backstage-plugin-scorecard-backend 3.0.1 → 4.1.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 (70) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +24 -10
  3. package/config.d.ts +83 -43
  4. package/dist/database/DatabaseMetricValues.cjs.js +139 -46
  5. package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
  6. package/dist/database/utils/getAggregateExpression.cjs.js +21 -0
  7. package/dist/database/utils/getAggregateExpression.cjs.js.map +1 -0
  8. package/dist/database/utils/mapMetricValueRow.cjs.js +33 -0
  9. package/dist/database/utils/mapMetricValueRow.cjs.js.map +1 -0
  10. package/dist/plugin.cjs.js +2 -2
  11. package/dist/plugin.cjs.js.map +1 -1
  12. package/dist/providers/MetricProvidersRegistry.cjs.js +19 -35
  13. package/dist/providers/MetricProvidersRegistry.cjs.js.map +1 -1
  14. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js +32 -39
  15. package/dist/scheduler/tasks/PullMetricsByProviderTask.cjs.js.map +1 -1
  16. package/dist/service/CatalogMetricService.cjs.js +11 -13
  17. package/dist/service/CatalogMetricService.cjs.js.map +1 -1
  18. package/dist/service/aggregations/AggregatedMetricLoader.cjs.js +11 -0
  19. package/dist/service/aggregations/AggregatedMetricLoader.cjs.js.map +1 -1
  20. package/dist/service/aggregations/{AggregationService.cjs.js → AggregationsService.cjs.js} +22 -8
  21. package/dist/service/aggregations/AggregationsService.cjs.js.map +1 -0
  22. package/dist/service/aggregations/strategies/ScalarAggregationStrategy.cjs.js +50 -0
  23. package/dist/service/aggregations/strategies/ScalarAggregationStrategy.cjs.js.map +1 -0
  24. package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js +6 -0
  25. package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js.map +1 -1
  26. package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js +10 -12
  27. package/dist/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.cjs.js.map +1 -1
  28. package/dist/service/aggregations/strategies/registerStrategies.cjs.js +21 -0
  29. package/dist/service/aggregations/strategies/registerStrategies.cjs.js.map +1 -1
  30. package/dist/service/mappers.cjs.js +17 -8
  31. package/dist/service/mappers.cjs.js.map +1 -1
  32. package/dist/service/router.cjs.js +15 -16
  33. package/dist/service/router.cjs.js.map +1 -1
  34. package/dist/threshold/ThresholdResolver.cjs.js +27 -17
  35. package/dist/threshold/ThresholdResolver.cjs.js.map +1 -1
  36. package/dist/utils/aggregation/buildAggregationConfig.cjs.js +28 -0
  37. package/dist/utils/aggregation/buildAggregationConfig.cjs.js.map +1 -0
  38. package/dist/utils/aggregation/buildAggregationConfigThresholds.cjs.js +18 -0
  39. package/dist/utils/aggregation/buildAggregationConfigThresholds.cjs.js.map +1 -0
  40. package/dist/utils/aggregation/buildAggregationStatusScores.cjs.js +13 -0
  41. package/dist/utils/aggregation/buildAggregationStatusScores.cjs.js.map +1 -0
  42. package/dist/utils/aggregation/isScalarAggregationConfig.cjs.js +10 -0
  43. package/dist/utils/aggregation/isScalarAggregationConfig.cjs.js.map +1 -0
  44. package/dist/utils/aggregation/isScalarAggregationType.cjs.js +10 -0
  45. package/dist/utils/aggregation/isScalarAggregationType.cjs.js.map +1 -0
  46. package/dist/utils/aggregation/parseValidatedAggregationConfig.cjs.js +21 -0
  47. package/dist/utils/aggregation/parseValidatedAggregationConfig.cjs.js.map +1 -0
  48. package/dist/utils/{mergeEntityAndProviderThresholds.cjs.js → mergeEntityAndMetricThresholds.cjs.js} +12 -11
  49. package/dist/utils/mergeEntityAndMetricThresholds.cjs.js.map +1 -0
  50. package/dist/utils/mergeMaxTimestamp.cjs.js +8 -0
  51. package/dist/utils/mergeMaxTimestamp.cjs.js.map +1 -0
  52. package/dist/utils/metricCalculationError.cjs.js +1 -1
  53. package/dist/utils/metricCalculationError.cjs.js.map +1 -1
  54. package/dist/utils/metricProviderConfigKeys.cjs.js +48 -0
  55. package/dist/utils/metricProviderConfigKeys.cjs.js.map +1 -0
  56. package/dist/utils/normalizeTimestamp.cjs.js +14 -0
  57. package/dist/utils/normalizeTimestamp.cjs.js.map +1 -0
  58. package/dist/utils/toIsoTimestamp.cjs.js +8 -0
  59. package/dist/utils/toIsoTimestamp.cjs.js.map +1 -0
  60. package/dist/validation/schemas/aggregationConfigSchemas.cjs.js +25 -10
  61. package/dist/validation/schemas/aggregationConfigSchemas.cjs.js.map +1 -1
  62. package/dist/validation/validateAggregationConfig.cjs.js +14 -20
  63. package/dist/validation/validateAggregationConfig.cjs.js.map +1 -1
  64. package/dist/validation/validateMetricProviderIds.cjs.js +22 -0
  65. package/dist/validation/validateMetricProviderIds.cjs.js.map +1 -0
  66. package/package.json +3 -3
  67. package/dist/service/aggregations/AggregationService.cjs.js.map +0 -1
  68. package/dist/utils/buildAggregationConfig.cjs.js +0 -45
  69. package/dist/utils/buildAggregationConfig.cjs.js.map +0 -1
  70. package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-scorecard-backend
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3af0fb2: This update introduces new scalar aggregation KPIs in the scorecard configuration, including:
8
+
9
+ - **`sum`**: Single numeric total of latest metric values across owned entities
10
+ - **`average`**: Mean of latest metric values across owned entities
11
+ - **`max`**: Maximum latest metric value across owned entities
12
+ - **`min`**: Minimum latest metric value across owned entities
13
+ - **`count`**: Number of entities with a non-null latest stored value
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [3af0fb2]
18
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@4.1.0
19
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@4.1.0
20
+
21
+ ## 4.0.0
22
+
23
+ ### Major Changes
24
+
25
+ - 8c14679: **BREAKING**: Scorecard provider configuration now lives under top-level `scorecard.metricProviders` instead of `scorecard.plugins`. Provider IDs must be `<datasource>.<providerName>` (no longer equal to the datasource alone). Entity annotations for thresholds use now the full metric ID instead of provider ID.
26
+
27
+ Thresholds from configuration are determined by the most specific setting (**metric > provider**):
28
+
29
+ 1. `metricProviders.<datasource>.<providerName>.metrics.<metricName>.thresholds`
30
+ 2. `metricProviders.<datasource>.<providerName>.thresholds`
31
+
32
+ Config keys are local names (no datasource prefix). Entity annotations use the full metric ID:
33
+ `scorecard.io/<metricId>.thresholds.rules.<key>`.
34
+
35
+ Filecheck provider ID is now `filecheck.fileExistence`; files move under `options`:
36
+
37
+ ```diff
38
+ scorecard:
39
+ - plugins:
40
+ - filecheck:
41
+ - files:
42
+ - license: LICENSE
43
+ - codeowners: CODEOWNERS
44
+ - thresholds: ...
45
+ - schedule: ...
46
+ + metricProviders:
47
+ + filecheck:
48
+ + fileExistence:
49
+ + options:
50
+ + files:
51
+ + license: LICENSE
52
+ + codeowners: CODEOWNERS
53
+ + thresholds: ...
54
+ + schedule: ...
55
+ ```
56
+
57
+ Migration from the previous `scorecard.plugins` layout:
58
+
59
+ ```diff
60
+ scorecard:
61
+ - plugins:
62
+ + metricProviders:
63
+ github:
64
+ openPRs:
65
+ schedule: ...
66
+ thresholds: ...
67
+ ```
68
+
69
+ ### Patch Changes
70
+
71
+ - Updated dependencies [8c14679]
72
+ - @red-hat-developer-hub/backstage-plugin-scorecard-common@4.0.0
73
+ - @red-hat-developer-hub/backstage-plugin-scorecard-node@4.0.0
74
+
3
75
  ## 3.0.1
4
76
 
5
77
  ### Patch 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); **`weightedStatusScore`** KPI **`options.thresholds`** follow the same rule.
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
- For comprehensive threshold configuration guide, examples, best practices, interval validation, and **aggregation KPI result thresholds** for **`type: weightedStatusScore`**, see [thresholds.md](./docs/thresholds.md).
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
- ## Aggregation KPIs (homepage and `GET /aggregations`)
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
 
@@ -167,13 +179,13 @@ scorecard:
167
179
  color: error.main
168
180
  ```
169
181
 
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) or `weightedStatusScore` (normalized weighted score). |
175
- | `metricId` | Metric provider id used to load thresholds and compute counts. |
176
- | `options` | Optional for `statusGrouped`. **Required** for `weightedStatusScore`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-weightedstatusscore-type)); evaluated on **`weightedStatusScore`** (**0–100** portfolio percentage, **one decimal**); first match sets **`aggregationChartDisplayColor`**. The API includes **`weightedStatusScore`**, **`weightedStatusSum`**, **`weightedStatusMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). |
182
+ | Field | Description |
183
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
184
+ | `title` | Display title for this aggregation (returned in API metadata). |
185
+ | `description` | Display description for this aggregation. |
186
+ | `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. |
187
+ | `metricId` | Metric provider id used to load thresholds and compute counts or scalar values. |
188
+ | `options` | **Optional:** extra configuration attributes required to further configure the aggregated card for a specific type |
177
189
 
178
190
  - **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
179
191
  - 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.
@@ -250,6 +262,8 @@ curl -X GET "{{url}}/api/scorecard/metrics/catalog/component/default/my-service?
250
262
 
251
263
  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
264
 
265
+ 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).
266
+
253
267
  The **`aggregationId`** is either:
254
268
 
255
269
  - A key under **`scorecard.aggregationKPIs`** in app-config (KPI-specific title, description, type, and `metricId`), or
package/config.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
18
18
  import {
19
- AggregationType,
19
+ aggregationTypes,
20
20
  ThresholdConfig,
21
21
  AggregationThresholdRule,
22
22
  } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
@@ -27,30 +27,63 @@ 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
- /** Title of the aggregation */
32
- title: string;
33
- /** Description of the aggregation */
34
- description: string;
35
- /** Type of the aggregation */
36
- type: AggregationType;
37
- /** Metric ID for which the aggregation is calculated */
38
- metricId: string;
39
- /** Type-specific settings */
40
- options?: {
41
- /** Required under `options` when `type` is `weightedStatusScore` */
42
- statusScores?: {
43
- [thresholdRuleKey: string]: number;
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
+ /** Options specific to the scalar aggregation type */
80
+ options?: {
81
+ /** Optional: threshold rules for coloring the KPI headline value from the aggregation result */
82
+ thresholds?: {
83
+ rules: AggregationThresholdRule[];
84
+ };
85
+ };
44
86
  };
45
- /**
46
- * Optional: threshold rules for coloring the KPI headline value from the aggregation result
47
- * (e.g. weighted status score percentage 0–100 for `weightedStatusScore` KPIs).
48
- */
49
- thresholds?: {
50
- rules: AggregationThresholdRule[];
51
- };
52
- };
53
- };
54
87
  };
55
88
  /** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
56
89
  dataRetentionDays?: number;
@@ -66,29 +99,36 @@ export interface Config {
66
99
  except?: string[];
67
100
  };
68
101
  };
69
- /** Configuration for scorecard metric providers */
70
- plugins?: {
71
- /** Configuration for datasource */
102
+ /** Metric providers calculate one or more metrics on a schedule. */
103
+ metricProviders?: {
104
+ /** Datasource ID, matches `getProviderDatasourceId()` of a provider (e.g., `jira`, `github`, `filecheck`). */
72
105
  [datasource: string]: {
73
- /** Configuration for metric providers within the datasource.
74
- * Each key corresponds to the metric name part of the provider ID (datasource.metricName).
106
+ /** Configuration for a specific metric provider.
107
+ * Use the local name without datasource prefix (e.g., `openPRs` instead of `github.openPRs`).
75
108
  */
76
- [metricName: string]: {
77
- /** Threshold configuration for the metric */
78
- thresholds?: ThresholdConfig;
109
+ [providerName: string]: {
110
+ /** How often metrics will be calculated for this provider. */
111
+ schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
79
112
  /**
80
- * Schedule for collecting this metric. If not set, the default hourly schedule is used.
81
- *
82
- * Default schedule:
83
- * ```ts
84
- * {
85
- * frequency: { hours: 1 },
86
- * timeout: { minutes: 15 },
87
- * initialDelay: { minutes: 1 },
88
- * }
89
- * ```
113
+ * How metric values are categorized for all metrics of this provider.
114
+ * Overridden by metric-level thresholds when set.
90
115
  */
91
- schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
116
+ thresholds?: ThresholdConfig;
117
+ /** Per-metric configuration. */
118
+ metrics?: {
119
+ /** Configuration for a specific metric.
120
+ * Use the local name without datasource prefix (e.g., 'openPRs' instead of 'github.openPRs').
121
+ */
122
+ [metricName: string]: {
123
+ /**
124
+ * How metric values are categorized for this metric.
125
+ * Overrides provider-level thresholds.
126
+ */
127
+ thresholds?: ThresholdConfig;
128
+ };
129
+ };
130
+ /** Provider-specific options (shape defined by each module). */
131
+ options?: unknown;
92
132
  };
93
133
  };
94
134
  };
@@ -1,11 +1,86 @@
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) {
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
+ const aggregateRow = await aggregateQuery.select(
62
+ this.dbClient.raw(`${aggregateExpression} as value`),
63
+ this.dbClient.raw("COUNT(*) as total"),
64
+ this.dbClient.raw("MAX(timestamp) as max_timestamp")
65
+ ).first();
66
+ const aggregateResult = {
67
+ value: 0,
68
+ total: 0,
69
+ maxTimestamp: /* @__PURE__ */ new Date(0)
70
+ };
71
+ if (aggregateRow) {
72
+ aggregateResult.value = Number(aggregateRow.value);
73
+ aggregateResult.total = Number(aggregateRow.total);
74
+ aggregateResult.maxTimestamp = normalizeTimestamp.normalizeTimestamp(
75
+ aggregateRow.max_timestamp
76
+ );
77
+ }
78
+ return {
79
+ value: aggregateResult.value,
80
+ total: aggregateResult.total,
81
+ maxTimestamp: aggregateResult.maxTimestamp
82
+ };
83
+ }
9
84
  /**
10
85
  * Insert multiple metric values
11
86
  */
@@ -13,16 +88,19 @@ class DatabaseMetricValues {
13
88
  if (metricValues.length === 0) {
14
89
  return;
15
90
  }
16
- await this.dbClient(this.tableName).insert(metricValues);
91
+ await this.dbClient(this.tableName).insert(
92
+ metricValues.map(mapMetricValueRow.toMetricValueRow)
93
+ );
17
94
  }
18
95
  /**
19
96
  * Get the latest metric values for a specific entity and metrics
20
97
  */
21
- async readLatestEntityMetricValues(catalog_entity_ref, metric_ids) {
22
- return await this.dbClient(this.tableName).select("*").whereIn(
98
+ async readLatestEntityMetricValues(catalogEntityRef, metricIds) {
99
+ const rows = await this.dbClient(this.tableName).select("*").whereIn(
23
100
  "id",
24
- this.dbClient(this.tableName).max("id").whereIn("metric_id", metric_ids).where("catalog_entity_ref", catalog_entity_ref).groupBy("metric_id")
101
+ this.dbClient(this.tableName).max("id").whereIn("metric_id", metricIds).where("catalog_entity_ref", catalogEntityRef).groupBy("metric_id")
25
102
  );
103
+ return rows.map(mapMetricValueRow.fromMetricValueRow);
26
104
  }
27
105
  /**
28
106
  * Delete metric values that are older than the given date
@@ -33,56 +111,34 @@ class DatabaseMetricValues {
33
111
  /**
34
112
  * Get aggregated metrics by status for multiple entities and metrics.
35
113
  */
36
- async readAggregatedMetricByEntityRefs(catalog_entity_refs, metric_id) {
37
- if (catalog_entity_refs.length === 0) {
114
+ async readAggregatedMetricByEntityRefs(catalogEntityRefs, metricId) {
115
+ if (catalogEntityRefs.length === 0) {
38
116
  return void 0;
39
117
  }
40
- const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("catalog_entity_ref");
41
- const metricValueIsMissingExpr = "(value IS NULL OR CAST(value AS TEXT) = 'null')";
42
- const statsRow = await this.dbClient(this.tableName).whereIn("id", latestIdsSubquery).select(
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
118
+ const latestIdsSubquery = this.getLatestIdsSubquery(
119
+ metricId,
120
+ catalogEntityRefs
51
121
  );
122
+ const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } = await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);
52
123
  if (latestRowCount === 0) {
53
124
  return void 0;
54
125
  }
55
- const calculation_error_count = Number(
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");
126
+ 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
127
  if (!statusRows || statusRows.length === 0) {
72
128
  return {
73
- metric_id,
129
+ metricId,
74
130
  total: 0,
75
- max_timestamp: maxTimestampAllLatest,
131
+ maxTimestamp: maxTimestampAllLatest,
76
132
  statusCounts: {},
77
- calculation_error_count,
78
- latest_entity_count: latestRowCount
133
+ calculationErrorCount,
134
+ latestEntityCount: latestRowCount
79
135
  };
80
136
  }
81
137
  let maxTimestamp = /* @__PURE__ */ new Date(0);
82
138
  let total = 0;
83
139
  const statusCounts = {};
84
140
  for (const row of statusRows) {
85
- const rowTimestamp = normalizeTimestamp(row.max_timestamp);
141
+ const rowTimestamp = normalizeTimestamp.normalizeTimestamp(row.max_timestamp);
86
142
  if (rowTimestamp > maxTimestamp) {
87
143
  maxTimestamp = rowTimestamp;
88
144
  }
@@ -91,24 +147,60 @@ class DatabaseMetricValues {
91
147
  statusCounts[name] = count;
92
148
  total += count;
93
149
  }
94
- const mergedMax = maxTimestampAllLatest.getTime() >= maxTimestamp.getTime() ? maxTimestampAllLatest : maxTimestamp;
150
+ const mergedMax = mergeMaxTimestamp.mergeMaxTimestamp(maxTimestampAllLatest, maxTimestamp);
95
151
  return {
96
- metric_id,
152
+ metricId,
97
153
  total,
98
- max_timestamp: mergedMax,
154
+ maxTimestamp: mergedMax,
99
155
  statusCounts,
100
- calculation_error_count,
101
- latest_entity_count: latestRowCount
156
+ calculationErrorCount,
157
+ latestEntityCount: latestRowCount
158
+ };
159
+ }
160
+ /**
161
+ * Aggregate raw metric values across latest rows for multiple entities.
162
+ */
163
+ async readScalarAggregatedMetricByEntityRefs(catalogEntityRefs, metricId, aggregationFn) {
164
+ if (catalogEntityRefs.length === 0) {
165
+ return void 0;
166
+ }
167
+ const latestIdsSubquery = this.getLatestIdsSubquery(
168
+ metricId,
169
+ catalogEntityRefs
170
+ );
171
+ const { latestRowCount, calculationErrorCount, maxTimestampAllLatest } = await this.readStatsRowByLatestIdsSubquery(latestIdsSubquery);
172
+ if (latestRowCount === 0) {
173
+ return void 0;
174
+ }
175
+ const {
176
+ value,
177
+ total,
178
+ maxTimestamp: aggregateMaxTimestamp
179
+ } = await this.readScalarAggregationByLatestIdsSubquery(
180
+ latestIdsSubquery,
181
+ aggregationFn
182
+ );
183
+ const mergedMax = mergeMaxTimestamp.mergeMaxTimestamp(
184
+ maxTimestampAllLatest,
185
+ aggregateMaxTimestamp
186
+ );
187
+ return {
188
+ metricId,
189
+ total,
190
+ maxTimestamp: mergedMax,
191
+ value,
192
+ calculationErrorCount,
193
+ latestEntityCount: latestRowCount
102
194
  };
103
195
  }
104
196
  /**
105
197
  * Fetch the latest entity metric values for a given metric, with optional filtering
106
198
  * by status, name, kind, namespace, or owner, plus sorting and pagination.
107
199
  */
108
- async readEntityMetricsWithFilters(metric_id, options) {
200
+ async readEntityMetricsWithFilters(metricId, options) {
109
201
  const clientName = this.dbClient.client?.config?.client ?? "";
110
202
  const isPostgres = clientName === "pg" || clientName.includes("postgres");
111
- const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).groupBy("catalog_entity_ref");
203
+ const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metricId).groupBy("catalog_entity_ref");
112
204
  const query = this.dbClient(this.tableName).select("*").whereIn("id", latestIdsSubquery);
113
205
  const sortColumnMap = {
114
206
  entityName: "catalog_entity_ref",
@@ -141,7 +233,8 @@ class DatabaseMetricValues {
141
233
  if (options.pagination) {
142
234
  query.limit(options.pagination.limit).offset(options.pagination.offset);
143
235
  }
144
- return await query;
236
+ const rows = await query;
237
+ return rows.map(mapMetricValueRow.fromMetricValueRow);
145
238
  }
146
239
  applySort(query, sortBy, column, direction, isPostgres) {
147
240
  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 {\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 ): 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 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 aggregateResult.value = Number(aggregateRow.value);\n aggregateResult.total = Number(aggregateRow.total);\n aggregateResult.maxTimestamp = normalizeTimestamp(\n aggregateRow.max_timestamp,\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 ): 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 );\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":";;;;;;;AAgEO,MAAM,oBAAA,CAAqB;AAAA,EAUhC,YAA6B,QAAA,EAA4B;AAA5B,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA,EAA6B;AAAA,EAA7B,QAAA;AAAA,EATZ,SAAA,GAAY,eAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,OAAwB,wBAAA,GACtB,iDAAA;AAAA;AAAA;AAAA;AAAA,EAOM,oBAAA,CACN,UACA,iBAAA,EACmB;AACnB,IAAA,OAAO,KAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CAChC,IAAI,IAAI,CAAA,CACR,KAAA,CAAM,WAAA,EAAa,QAAQ,CAAA,CAC3B,OAAA,CAAQ,sBAAsB,iBAAiB,CAAA,CAC/C,QAAQ,oBAAoB,CAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,gCACZ,iBAAA,EACyB;AAGzB,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CAChD,OAAA,CAAQ,IAAA,EAAM,iBAAiB,CAAA,CAC/B,MAAA;AAAA,MACC,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,8BAA8B,CAAA;AAAA,MAChD,KAAK,QAAA,CAAS,GAAA;AAAA,QACZ,CAAA,4CAAA,EAA+C,qBAAqB,wBAAwB,CAAA,8CAAA;AAAA,OAC9F;AAAA,MACA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,iCAAiC;AAAA,MAEpD,KAAA,EAAM;AAET,IAAA,MAAM,cAAA,GAAiB,MAAA;AAAA,MACpB,UACG,gBAAA,IAAoB;AAAA,KAC1B;AAEA,IAAA,MAAM,qBAAA,GAAwB,MAAA;AAAA,MAC3B,UACG,uBAAA,IAA2B;AAAA,KACjC;AAEA,IAAA,MAAM,qBAAA,GAAwBA,qCAAA;AAAA,MAC3B,QAAA,EAA0C;AAAA,KAC7C;AAEA,IAAA,OAAO;AAAA,MACL,iBAAA;AAAA,MACA,cAAA;AAAA,MACA,qBAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAc,wCAAA,CACZ,iBAAA,EACA,aAAA,EACqC;AACrC,IAAA,MAAM,UAAA,GACH,IAAA,CAAK,QAAA,CAAiB,MAAA,EAAQ,QAAQ,MAAA,IAAU,EAAA;AACnD,IAAA,MAAM,UAAA,GAAa,UAAA,KAAe,IAAA,IAAQ,UAAA,CAAW,SAAS,UAAU,CAAA;AAExE,IAAA,MAAM,gBAAA,GAAmB,aACrB,uCAAA,GACA,mCAAA;AAEJ,IAAA,MAAM,mBAAA,GAAsBC,6CAAA;AAAA,MAC1B,aAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CAChD,OAAA,CAAQ,IAAA,EAAM,iBAAiB,CAAA,CAC/B,QAAA,CAAS,CAAA,IAAA,EAAO,oBAAA,CAAqB,wBAAwB,CAAA,CAAE,CAAA;AAElE,IAAA,MAAM,YAAA,GAAe,MAAM,cAAA,CACxB,MAAA;AAAA,MACC,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,CAAA,EAAG,mBAAmB,CAAA,SAAA,CAAW,CAAA;AAAA,MACnD,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,mBAAmB,CAAA;AAAA,MACrC,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,iCAAiC;AAAA,MAEpD,KAAA,EAAM;AAET,IAAA,MAAM,eAAA,GAAkB;AAAA,MACtB,KAAA,EAAO,CAAA;AAAA,MACP,KAAA,EAAO,CAAA;AAAA,MACP,YAAA,kBAAc,IAAI,IAAA,CAAK,CAAC;AAAA,KAC1B;AAEA,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,eAAA,CAAgB,KAAA,GAAQ,MAAA,CAAO,YAAA,CAAa,KAAK,CAAA;AACjD,MAAA,eAAA,CAAgB,KAAA,GAAQ,MAAA,CAAO,YAAA,CAAa,KAAK,CAAA;AACjD,MAAA,eAAA,CAAgB,YAAA,GAAeD,qCAAA;AAAA,QAC7B,YAAA,CAAa;AAAA,OACf;AAAA,IACF;AAEA,IAAA,OAAO;AAAA,MACL,OAAO,eAAA,CAAgB,KAAA;AAAA,MACvB,OAAO,eAAA,CAAgB,KAAA;AAAA,MACvB,cAAc,eAAA,CAAgB;AAAA,KAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,mBAAmB,YAAA,EAAoD;AAC3E,IAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,MAAA;AAAA,IACF;AACA,IAAA,MAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CAAE,MAAA;AAAA,MAClC,YAAA,CAAa,IAAIE,kCAAgB;AAAA,KACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,4BAAA,CACJ,gBAAA,EACA,SAAA,EAC0B;AAC1B,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,QAAA,CAAS,KAAK,SAAS,CAAA,CAC5C,MAAA,CAAO,GAAG,CAAA,CACV,OAAA;AAAA,MACC,IAAA;AAAA,MACA,KAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CACzB,IAAI,IAAI,CAAA,CACR,OAAA,CAAQ,WAAA,EAAa,SAAS,CAAA,CAC9B,KAAA,CAAM,sBAAsB,gBAAgB,CAAA,CAC5C,QAAQ,WAAW;AAAA,KACxB;AAEF,IAAA,OAAQ,IAAA,CAAgC,IAAIC,oCAAkB,CAAA;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,SAAA,EAAkC;AAC5D,IAAA,OAAO,MAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CACtC,KAAA,CAAM,WAAA,EAAa,GAAA,EAAK,SAAS,CAAA,CACjC,GAAA,EAAI;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gCAAA,CACJ,iBAAA,EACA,QAAA,EACyC;AACzC,IAAA,IAAI,iBAAA,CAAkB,WAAW,CAAA,EAAG;AAClC,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,oBAAoB,IAAA,CAAK,oBAAA;AAAA,MAC7B,QAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,MAAM,EAAE,gBAAgB,qBAAA,EAAuB,qBAAA,KAC7C,MAAM,IAAA,CAAK,gCAAgC,iBAAiB,CAAA;AAE9D,IAAA,IAAI,mBAAmB,CAAA,EAAG;AACxB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CAClD,MAAA,CAAO,QAAQ,CAAA,CACf,KAAA,CAAM,YAAY,EAClB,GAAA,CAAI,4BAA4B,CAAA,CAChC,OAAA,CAAQ,IAAA,EAAM,iBAAiB,CAAA,CAC/B,YAAA,CAAa,QAAQ,CAAA,CACrB,QAAA,CAAS,CAAA,IAAA,EAAO,oBAAA,CAAqB,wBAAwB,CAAA,CAAE,CAAA,CAC/D,QAAQ,QAAQ,CAAA;AAEnB,IAAA,IAAI,CAAC,UAAA,IAAc,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG;AAC1C,MAAA,OAAO;AAAA,QACL,QAAA;AAAA,QACA,KAAA,EAAO,CAAA;AAAA,QACP,YAAA,EAAc,qBAAA;AAAA,QACd,cAAc,EAAC;AAAA,QACf,qBAAA;AAAA,QACA,iBAAA,EAAmB;AAAA,OACrB;AAAA,IACF;AAEA,IAAA,IAAI,YAAA,mBAAe,IAAI,IAAA,CAAK,CAAC,CAAA;AAC7B,IAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,IAAA,MAAM,eAAuC,EAAC;AAC9C,IAAA,KAAA,MAAW,OAAO,UAAA,EAAY;AAC5B,MAAA,MAAM,YAAA,GAAeH,qCAAA,CAAmB,GAAA,CAAI,aAAa,CAAA;AACzD,MAAA,IAAI,eAAe,YAAA,EAAc;AAC/B,QAAA,YAAA,GAAe,YAAA;AAAA,MACjB;AACA,MAAA,MAAM,OAAO,GAAA,CAAI,MAAA;AACjB,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC9B,MAAA,YAAA,CAAa,IAAI,CAAA,GAAI,KAAA;AACrB,MAAA,KAAA,IAAS,KAAA;AAAA,IACX;AAEA,IAAA,MAAM,SAAA,GAAYI,mCAAA,CAAkB,qBAAA,EAAuB,YAAY,CAAA;AAEvE,IAAA,OAAO;AAAA,MACL,QAAA;AAAA,MACA,KAAA;AAAA,MACA,YAAA,EAAc,SAAA;AAAA,MACd,YAAA;AAAA,MACA,qBAAA;AAAA,MACA,iBAAA,EAAmB;AAAA,KACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,sCAAA,CACJ,iBAAA,EACA,QAAA,EACA,aAAA,EAC+C;AAC/C,IAAA,IAAI,iBAAA,CAAkB,WAAW,CAAA,EAAG;AAClC,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,oBAAoB,IAAA,CAAK,oBAAA;AAAA,MAC7B,QAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,MAAM,EAAE,gBAAgB,qBAAA,EAAuB,qBAAA,KAC7C,MAAM,IAAA,CAAK,gCAAgC,iBAAiB,CAAA;AAE9D,IAAA,IAAI,mBAAmB,CAAA,EAAG;AACxB,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM;AAAA,MACJ,KAAA;AAAA,MACA,KAAA;AAAA,MACA,YAAA,EAAc;AAAA,KAChB,GAAI,MAAM,IAAA,CAAK,wCAAA;AAAA,MACb,iBAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,MAAM,SAAA,GAAYA,mCAAA;AAAA,MAChB,qBAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO;AAAA,MACL,QAAA;AAAA,MACA,KAAA;AAAA,MACA,YAAA,EAAc,SAAA;AAAA,MACd,KAAA;AAAA,MACA,qBAAA;AAAA,MACA,iBAAA,EAAmB;AAAA,KACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,4BAAA,CACJ,QAAA,EACA,OAAA,EAC0B;AAC1B,IAAA,MAAM,UAAA,GACH,IAAA,CAAK,QAAA,CAAiB,MAAA,EAAQ,QAAQ,MAAA,IAAU,EAAA;AACnD,IAAA,MAAM,UAAA,GAAa,UAAA,KAAe,IAAA,IAAQ,UAAA,CAAW,SAAS,UAAU,CAAA;AAExE,IAAA,MAAM,iBAAA,GAAoB,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CACnD,GAAA,CAAI,IAAI,CAAA,CACR,KAAA,CAAM,WAAA,EAAa,QAAQ,CAAA,CAC3B,QAAQ,oBAAoB,CAAA;AAE/B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,SAAS,CAAA,CACvC,MAAA,CAAO,GAAG,CAAA,CACV,OAAA,CAAQ,IAAA,EAAM,iBAAiB,CAAA;AAElC,IAAA,MAAM,aAAA,GAAwC;AAAA,MAC5C,UAAA,EAAY,oBAAA;AAAA,MACZ,KAAA,EAAO,cAAA;AAAA,MACP,UAAA,EAAY,aAAA;AAAA,MACZ,SAAA,EAAW,WAAA;AAAA,MACX,WAAA,EAAa,OAAA;AAAA,MACb,SAAA,EAAW,kBAAA;AAAA,MACX,MAAA,EAAQ;AAAA,KACV;AAEA,IAAA,MAAM,UACH,OAAA,CAAQ,MAAA,IAAU,aAAA,CAAc,OAAA,CAAQ,MAAM,CAAA,KAAM,WAAA;AACvD,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,KAAc,KAAA,GAAQ,KAAA,GAAQ,MAAA;AAExD,IAAA,IAAA,CAAK,UAAU,KAAA,EAAO,OAAA,CAAQ,MAAA,EAAQ,MAAA,EAAQ,WAAW,UAAU,CAAA;AAEnE,IAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,MAAA,KAAA,CAAM,KAAA,CAAM,QAAA,EAAU,OAAA,CAAQ,MAAM,CAAA;AAAA,IACtC;AAEA,IAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,MAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,UAAA,CAAW,OAAA,CAAQ,WAAW,MAAM,CAAA;AAC5D,MAAA,KAAA,CAAM,SAAS,uCAAA,EAAyC,CAAC,CAAA,CAAA,EAAI,OAAO,GAAG,CAAC,CAAA;AAAA,IAC1E;AAEA,IAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,MAAA,KAAA,CAAM,KAAA,CAAM,aAAA,EAAe,OAAA,CAAQ,UAAU,CAAA;AAAA,IAC/C;AAEA,IAAA,IAAI,QAAQ,eAAA,EAAiB;AAC3B,MAAA,KAAA,CAAM,KAAA,CAAM,kBAAA,EAAoB,OAAA,CAAQ,eAAe,CAAA;AAAA,IACzD;AAEA,IAAA,IAAI,OAAA,CAAQ,WAAA,IAAe,OAAA,CAAQ,WAAA,CAAY,SAAS,CAAA,EAAG;AACzD,MAAA,KAAA,CAAM,OAAA,CAAQ,cAAA,EAAgB,OAAA,CAAQ,WAAW,CAAA;AAAA,IACnD;AAEA,IAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,MAAA,KAAA,CAAM,KAAA,CAAM,QAAQ,UAAA,CAAW,KAAK,EAAE,MAAA,CAAO,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA,IACxE;AAEA,IAAA,MAAM,OAAO,MAAM,KAAA;AACnB,IAAA,OAAQ,IAAA,CAAgC,IAAID,oCAAkB,CAAA;AAAA,EAChE;AAAA,EAEQ,SAAA,CACN,KAAA,EACA,MAAA,EACA,MAAA,EACA,WACA,UAAA,EACM;AACN,IAAA,IAAI,WAAW,aAAA,EAAe;AAE5B,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,KAAA,CAAM,UAAA;AAAA,UACJ,yCAAyC,SAAS,CAAA,mBAAA;AAAA,SACpD;AAAA,MACF,CAAA,MAAO;AAEL,QAAA,KAAA,CAAM,UAAA;AAAA,UACJ,oDAAoD,SAAS,CAAA,QAAA;AAAA,SAC/D;AAAA,MACF;AAAA,IACF,CAAA,MAAA,IAAW,WAAW,QAAA,EAAU;AAE9B,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,KAAA,CAAM,UAAA,CAAW,CAAA,OAAA,EAAU,SAAS,CAAA,mBAAA,CAAqB,CAAA;AAAA,MAC3D,CAAA,MAAO;AAEL,QAAA,KAAA,CAAM,UAAA,CAAW,CAAA,uBAAA,EAA0B,SAAS,CAAA,QAAA,CAAU,CAAA;AAAA,MAChE;AAAA,IACF,CAAA,MAAO;AACL,MAAA,KAAA,CAAM,OAAA,CAAQ,QAAQ,SAAS,CAAA;AAE/B,MAAA,KAAA,CAAM,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA,IAC3B;AAAA,EACF;AACF;;;;"}