@oneuptime/common 14.0.5 → 14.0.7

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 (192) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/MetricService.ts +1 -1
  10. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  11. package/Server/Services/UserNotificationRuleService.ts +55 -53
  12. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  13. package/Server/Types/Database/QueryHelper.ts +54 -1
  14. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  15. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  16. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  17. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  18. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  19. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  20. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  21. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  22. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  23. package/Server/Utils/Response.ts +31 -0
  24. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  25. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  26. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  27. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  28. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  29. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  30. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  31. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  32. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  33. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  34. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  35. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  36. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  37. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  38. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  39. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  40. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  41. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  42. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  43. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  44. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  45. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  46. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  47. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  48. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  49. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  50. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  51. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  52. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  53. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  54. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  55. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  56. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  57. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  58. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  59. package/Tests/Server/Services/DomainService.test.ts +525 -0
  60. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  61. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  62. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  63. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  64. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  65. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  66. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  67. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  68. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  69. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  70. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  71. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  72. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  73. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  74. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  75. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  76. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  77. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  78. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  79. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  80. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  81. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  82. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  83. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  84. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  85. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  86. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  87. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  88. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  89. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  90. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  91. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  92. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  93. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  94. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  95. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  96. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  97. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  98. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  99. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  100. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  101. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  102. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  103. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  104. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  105. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  106. package/Types/BaseDatabase/AggregationType.ts +7 -0
  107. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  108. package/Types/Incident/IncidentAlertLink.ts +19 -0
  109. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  110. package/Types/Rum/WebVitals.ts +13 -0
  111. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  112. package/Types/Topology/TopologyApi.ts +428 -0
  113. package/Types/Topology/TopologyTypeRules.ts +191 -0
  114. package/UI/Components/Input/Input.tsx +11 -0
  115. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  116. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  117. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  118. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AlertService.js +21 -33
  124. package/build/dist/Server/Services/AlertService.js.map +1 -1
  125. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  126. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  127. package/build/dist/Server/Services/IncidentService.js +96 -42
  128. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  129. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  130. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  131. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  132. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  133. package/build/dist/Server/Services/MetricService.js +1 -1
  134. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  135. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  136. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  137. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  138. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  139. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  140. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  141. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  142. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  143. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  144. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  145. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  146. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  147. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  148. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  149. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  150. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  151. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  152. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  153. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  154. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  156. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  159. package/build/dist/Server/Utils/Response.js +30 -0
  160. package/build/dist/Server/Utils/Response.js.map +1 -1
  161. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  162. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  163. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  164. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  165. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  166. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  167. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  168. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  169. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  170. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  171. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  172. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  173. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  174. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  175. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  176. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  177. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  178. package/build/dist/Types/Rum/WebVitals.js +12 -0
  179. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  180. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  181. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  182. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  183. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  184. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  185. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  186. package/build/dist/UI/Components/Input/Input.js +1 -1
  187. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  188. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  189. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  190. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  191. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  192. package/package.json +1 -1
@@ -0,0 +1,688 @@
1
+ import EntityRelationshipType from "../../../Types/Telemetry/EntityRelationshipType";
2
+ import EntitySource from "../../../Types/Telemetry/EntitySource";
3
+ import EntityType from "../../../Types/Telemetry/EntityType";
4
+ import { CONTAINER_SPECIFICITY, NESTABLE_CHILD_TYPES, NESTING_RELATIONSHIP_PRIORITY, PLACEMENT_RELATIONSHIP_TYPES, SERVICE_MAP_DETAIL_ATTRIBUTE_KEYS, } from "../../../Types/Topology/TopologyTypeRules";
5
+ /* Collects bound parameters and hands out their placeholders in order. */
6
+ export class TopologySqlParams {
7
+ constructor() {
8
+ this.values = [];
9
+ }
10
+ add(value) {
11
+ this.values.push(value);
12
+ return `$${this.values.length}`;
13
+ }
14
+ list() {
15
+ return this.values;
16
+ }
17
+ }
18
+ /*
19
+ * A parameter bound the first time a statement's text needs it. Postgres
20
+ * refuses a parameter the text never references ("could not determine data
21
+ * type"), and several are only needed on some paths — the range start when
22
+ * inactive items are filtered out, the project's types when a duplicated key
23
+ * has to be resolved.
24
+ */
25
+ export class TopologyLazyParam {
26
+ constructor(params, value) {
27
+ this.params = params;
28
+ this.value = value;
29
+ this.placeholder = null;
30
+ }
31
+ get() {
32
+ if (!this.placeholder) {
33
+ this.placeholder = this.params.add(this.value);
34
+ }
35
+ return this.placeholder;
36
+ }
37
+ }
38
+ function toRankTable(table) {
39
+ const keys = [];
40
+ const ranks = [];
41
+ for (const key of Object.keys(table).sort()) {
42
+ const rank = table[key];
43
+ if (typeof rank === "number") {
44
+ keys.push(key);
45
+ ranks.push(rank);
46
+ }
47
+ }
48
+ return { keys, ranks };
49
+ }
50
+ export const NESTING_RELATIONSHIP_RANKS = toRankTable(NESTING_RELATIONSHIP_PRIORITY);
51
+ export const CONTAINER_SPECIFICITY_RANKS = toRankTable(CONTAINER_SPECIFICITY);
52
+ export const NESTABLE_CHILD_TYPE_LIST = Array.from(NESTABLE_CHILD_TYPES).sort();
53
+ export const PLACEMENT_RELATIONSHIP_TYPE_LIST = Array.from(PLACEMENT_RELATIONSHIP_TYPES).sort();
54
+ export const SERVICE_MAP_ATTRIBUTE_KEY_LIST = [
55
+ ...SERVICE_MAP_DETAIL_ATTRIBUTE_KEYS,
56
+ ];
57
+ /* Labels the drawer orders its rows by; the browser translates its own. */
58
+ export const UNNAMED_RESOURCE_LABEL = "Unnamed resource";
59
+ export const UNDISCOVERED_RESOURCE_LABEL = "Undiscovered resource";
60
+ // ------------------------------------------------------------- fragments
61
+ /*
62
+ * "Did this resource report in the range?", the SQL twin of the Dashboard's
63
+ * TopologyActivity.isEntityActive. Only discovered rows have a heartbeat:
64
+ * manual and inventory-mirrored rows (any other non-blank source) are always
65
+ * active, and so is a row that has never reported. Change the two together.
66
+ */
67
+ export function activeSql(alias, rangeStart) {
68
+ return (`CASE WHEN ${alias}."source" IS NOT NULL AND ${alias}."source" <> '' ` +
69
+ `AND ${alias}."source" <> '${EntitySource.Discovered}' THEN TRUE ` +
70
+ `WHEN ${alias}."lastSeenAt" IS NULL THEN TRUE ` +
71
+ `ELSE ${alias}."lastSeenAt" >= ${rangeStart} END`);
72
+ }
73
+ /* Epoch milliseconds, floored, as float8 (a JavaScript number). */
74
+ export function epochMsSql(expression) {
75
+ return `floor(extract(epoch from ${expression}) * 1000)::float8`;
76
+ }
77
+ /* A non-archived, non-deleted item of the project. */
78
+ export function liveItemSql(alias, projectId) {
79
+ return (`${alias}."projectId" = ${projectId} AND ${alias}."isArchived" = false ` +
80
+ `AND ${alias}."deletedAt" IS NULL`);
81
+ }
82
+ /*
83
+ * A non-deleted row of the project, for the all-time reads: an item archived
84
+ * or not, or a relationship however long ago it was seen.
85
+ */
86
+ export function storedRowSql(alias, projectId) {
87
+ return `${alias}."projectId" = ${projectId} AND ${alias}."deletedAt" IS NULL`;
88
+ }
89
+ /* A non-deleted relationship of the project that was seen in the range. */
90
+ export function inRangeRelationshipSql(alias, projectId, rangeStart) {
91
+ return (`${alias}."projectId" = ${projectId} AND ${alias}."deletedAt" IS NULL ` +
92
+ `AND ${alias}."lastSeenAt" >= ${rangeStart}`);
93
+ }
94
+ /*
95
+ * The row that represents its entity key. Keys are unique per type (the
96
+ * unique index) and hash the type into their preimage, so two live rows share
97
+ * a key only if something wrote one by hand — but when it happens every
98
+ * endpoint must pick the same row: the first by (createdAt ASC, _id DESC),
99
+ * which is the row the old client-side list kept (it iterated createdAt DESC,
100
+ * _id ASC and let the last write win).
101
+ *
102
+ * The duplicated keys are found once per request (duplicateKeysStatement) and
103
+ * the rule is applied to them alone. For every other key the row is its own
104
+ * winner, so the common case adds no predicate at all — an anti-join against
105
+ * the whole project would cost a full pass over it in every statement that
106
+ * lists items.
107
+ */
108
+ export function winnerSql(alias, projectId, projectTypes, duplicateKeys) {
109
+ return (`(NOT (${alias}."entityKey" = ANY(${duplicateKeys}::text[])) ` +
110
+ `OR NOT EXISTS (SELECT 1 FROM "InventoryItem" dup ` +
111
+ `WHERE dup."projectId" = ${projectId} ` +
112
+ `AND dup."entityType" = ANY(${projectTypes}::text[]) ` +
113
+ `AND dup."entityKey" = ${alias}."entityKey" ` +
114
+ `AND dup."isArchived" = false AND dup."deletedAt" IS NULL ` +
115
+ `AND dup."_id" <> ${alias}."_id" ` +
116
+ `AND (dup."createdAt" < ${alias}."createdAt" ` +
117
+ `OR (dup."createdAt" = ${alias}."createdAt" AND dup."_id" > ${alias}."_id"))))`);
118
+ }
119
+ /* The winner predicate for `alias`, or nothing when no key is duplicated. */
120
+ function winnerPredicates(data) {
121
+ if (data.winner.duplicateKeys.length === 0) {
122
+ return [];
123
+ }
124
+ return [
125
+ winnerSql(data.alias, data.projectId, data.projectTypes.get(), data.params.add(data.winner.duplicateKeys)),
126
+ ];
127
+ }
128
+ /* Escapes LIKE metacharacters so a search term only ever matches literally. */
129
+ export function escapeLikeTerm(term) {
130
+ return term.replace(/[\\%_]/g, (character) => {
131
+ return `\\${character}`;
132
+ });
133
+ }
134
+ /* Every term must appear in the display name (case-insensitively). */
135
+ function nameTermsSql(alias, params, nameTerms) {
136
+ return nameTerms.map((term) => {
137
+ return `${alias}."displayName" ILIKE '%' || ${params.add(escapeLikeTerm(term))} || '%' ESCAPE '\\'`;
138
+ });
139
+ }
140
+ /* The lean entity columns every map row is built from. */
141
+ function leanEntityColumnsSql(alias) {
142
+ return (`${alias}."entityKey" AS "key", ${alias}."entityType" AS "type", ` +
143
+ `${alias}."displayName" AS "name", ${alias}."source" AS "source", ` +
144
+ `${epochMsSql(`${alias}."lastSeenAt"`)} AS "lastSeenAt"`);
145
+ }
146
+ /*
147
+ * Only string-valued detail attributes, per bag: the Service Map's reader
148
+ * needs a non-blank string and falls through to the next key otherwise, so
149
+ * anything else would change which label it shows.
150
+ */
151
+ function detailAttributesSql(alias, column, keys) {
152
+ return (`(SELECT jsonb_object_agg(k.key, ${alias}."${column}" -> k.key) ` +
153
+ `FROM unnest(${keys}::text[]) AS k(key) ` +
154
+ `WHERE jsonb_typeof(${alias}."${column}" -> k.key) = 'string')`);
155
+ }
156
+ // ----------------------------------------------------------- statements
157
+ /*
158
+ * The project's distinct entity types, read by skipping through the
159
+ * (projectId, entityType) index one type at a time (a "loose index scan"), so
160
+ * it costs one index probe per type however many items there are.
161
+ */
162
+ export function projectTypesStatement(data) {
163
+ const params = new TopologySqlParams();
164
+ const projectId = params.add(data.projectId);
165
+ return {
166
+ sql: `WITH RECURSIVE project_types AS (` +
167
+ `(SELECT i."entityType" FROM "InventoryItem" i ` +
168
+ `WHERE i."projectId" = ${projectId} AND i."deletedAt" IS NULL ` +
169
+ `ORDER BY i."entityType" LIMIT 1) ` +
170
+ `UNION ALL ` +
171
+ `SELECT (SELECT i."entityType" FROM "InventoryItem" i ` +
172
+ `WHERE i."projectId" = ${projectId} AND i."deletedAt" IS NULL ` +
173
+ `AND i."entityType" > t."entityType" ORDER BY i."entityType" LIMIT 1) ` +
174
+ `FROM project_types t WHERE t."entityType" IS NOT NULL) ` +
175
+ `SELECT pt."entityType" AS "entityType" FROM project_types pt ` +
176
+ `WHERE pt."entityType" IS NOT NULL`,
177
+ params: params.list(),
178
+ };
179
+ }
180
+ /*
181
+ * Keys that more than one live row of the project carries (normally none).
182
+ * One pass over the project; with the (projectId, entityKey) index it streams
183
+ * in key order without holding the keys in memory.
184
+ */
185
+ export function duplicateKeysStatement(data) {
186
+ const params = new TopologySqlParams();
187
+ const projectId = params.add(data.projectId);
188
+ return {
189
+ sql: `SELECT i."entityKey" AS "key" FROM "InventoryItem" i ` +
190
+ `WHERE ${liveItemSql("i", projectId)} ` +
191
+ `GROUP BY i."entityKey" HAVING COUNT(*) > 1`,
192
+ params: params.list(),
193
+ };
194
+ }
195
+ /*
196
+ * Service Map entities of one kind: every non-archived service
197
+ * (`serviceKeys` absent), or the non-service items with the given keys (the
198
+ * things services call). Ordered by key so a truncated list is stable.
199
+ */
200
+ export function serviceMapEntitiesStatement(data) {
201
+ const params = new TopologySqlParams();
202
+ const projectId = params.add(data.projectId);
203
+ const projectTypes = new TopologyLazyParam(params, data.winner.projectTypes);
204
+ const where = [liveItemSql("i", projectId)];
205
+ if (data.keys) {
206
+ where.push(`i."entityType" = ANY(${projectTypes.get()}::text[])`);
207
+ where.push(`i."entityKey" = ANY(${params.add(data.keys)}::text[])`);
208
+ where.push(`i."entityType" <> ${params.add(EntityType.Service)}`);
209
+ }
210
+ else {
211
+ where.push(`i."entityType" = ${params.add(EntityType.Service)}`);
212
+ }
213
+ where.push(...winnerPredicates({
214
+ params,
215
+ alias: "i",
216
+ projectId,
217
+ projectTypes,
218
+ winner: data.winner,
219
+ }));
220
+ if (data.mode === "count") {
221
+ return {
222
+ sql: `SELECT COUNT(*)::int AS "total" FROM "InventoryItem" i WHERE ${where.join(" AND ")}`,
223
+ params: params.list(),
224
+ };
225
+ }
226
+ const attributeKeys = params.add(SERVICE_MAP_ATTRIBUTE_KEY_LIST);
227
+ return {
228
+ sql: `SELECT ${leanEntityColumnsSql("i")}, ` +
229
+ `${detailAttributesSql("i", "descriptiveAttributes", attributeKeys)} AS "descriptiveAttributes", ` +
230
+ `${detailAttributesSql("i", "identifyingAttributes", attributeKeys)} AS "identifyingAttributes" ` +
231
+ `FROM "InventoryItem" i WHERE ${where.join(" AND ")} ` +
232
+ `ORDER BY i."entityKey" COLLATE "C" ASC ` +
233
+ `LIMIT ${params.add(data.limit)}`,
234
+ params: params.list(),
235
+ };
236
+ }
237
+ /*
238
+ * In-range `depends-on` relationships whose caller is one of the services, in
239
+ * the order the old list API returned them (createdAt DESC, _id ASC) so the
240
+ * browser sums traffic in the same order and averages come out bit-identical.
241
+ * `calleeKeys` narrows them to calls into those keys (Infrastructure asks
242
+ * only for calls between services it can place).
243
+ */
244
+ export function serviceMapDependenciesStatement(data) {
245
+ const params = new TopologySqlParams();
246
+ const projectId = params.add(data.projectId);
247
+ const rangeStart = params.add(data.rangeStart);
248
+ const where = `${inRangeRelationshipSql("r", projectId, rangeStart)} ` +
249
+ `AND r."relationshipType" = ${params.add(EntityRelationshipType.DependsOn)} ` +
250
+ `AND r."fromEntityKey" <> r."toEntityKey" ` +
251
+ `AND r."fromEntityKey" = ANY(${params.add(data.serviceKeys)}::text[])` +
252
+ (data.calleeKeys
253
+ ? ` AND r."toEntityKey" = ANY(${params.add(data.calleeKeys)}::text[])`
254
+ : "");
255
+ if (data.mode === "count") {
256
+ return {
257
+ sql: `SELECT COUNT(*)::int AS "total" FROM "InventoryItemRelationship" r WHERE ${where}`,
258
+ params: params.list(),
259
+ };
260
+ }
261
+ return {
262
+ sql: `SELECT r."fromEntityKey" AS "from", r."toEntityKey" AS "to", ` +
263
+ `r."callCount" AS "callCount", r."errorCount" AS "errorCount", ` +
264
+ `r."avgDurationMs" AS "avgDurationMs" ` +
265
+ `FROM "InventoryItemRelationship" r WHERE ${where} ` +
266
+ `ORDER BY r."createdAt" DESC, r."_id" ASC ` +
267
+ `LIMIT ${params.add(data.limit)}`,
268
+ params: params.list(),
269
+ };
270
+ }
271
+ /*
272
+ * Where services run: distinct (service, resource) over in-range runs-on /
273
+ * hosted-on from the given services. The Service Map counts these per target
274
+ * type; Infrastructure keeps the ones that land on a shipped node.
275
+ */
276
+ export function placementsStatement(data) {
277
+ const params = new TopologySqlParams();
278
+ const projectId = params.add(data.projectId);
279
+ const rangeStart = params.add(data.rangeStart);
280
+ return {
281
+ sql: `SELECT DISTINCT r."fromEntityKey" AS "service", r."toEntityKey" AS "target" ` +
282
+ `FROM "InventoryItemRelationship" r ` +
283
+ `WHERE ${inRangeRelationshipSql("r", projectId, rangeStart)} ` +
284
+ `AND r."relationshipType" = ANY(${params.add(PLACEMENT_RELATIONSHIP_TYPE_LIST)}::text[]) ` +
285
+ `AND r."fromEntityKey" = ANY(${params.add(data.serviceKeys)}::text[])`,
286
+ params: params.list(),
287
+ };
288
+ }
289
+ /*
290
+ * The live items (winning rows) with the given keys: their type and whether
291
+ * they reported in range. The keys are an array rather than a sub-select so
292
+ * the planner knows how many there are — a handful become index probes, tens
293
+ * of thousands one pass over the project (a sub-select over a CTE is
294
+ * estimated at a couple of hundred rows and gets probed key by key, once per
295
+ * project type, which is seconds on a large estate).
296
+ */
297
+ export function itemsByKeysStatement(data) {
298
+ const params = new TopologySqlParams();
299
+ const projectId = params.add(data.projectId);
300
+ const rangeStart = params.add(data.rangeStart);
301
+ const projectTypes = new TopologyLazyParam(params, data.winner.projectTypes);
302
+ const where = [
303
+ liveItemSql("i", projectId),
304
+ `i."entityType" = ANY(${projectTypes.get()}::text[])`,
305
+ `i."entityKey" = ANY(${params.add(data.keys)}::text[])`,
306
+ ...winnerPredicates({
307
+ params,
308
+ alias: "i",
309
+ projectId,
310
+ projectTypes,
311
+ winner: data.winner,
312
+ }),
313
+ ];
314
+ return {
315
+ sql: `SELECT i."entityKey" AS "key", i."entityType" AS "type", ` +
316
+ `${activeSql("i", rangeStart)} AS "active" ` +
317
+ `FROM "InventoryItem" i WHERE ${where.join(" AND ")}`,
318
+ params: params.list(),
319
+ };
320
+ }
321
+ /*
322
+ * Every live service's key and name (Infrastructure resolves placements and
323
+ * service labels from these).
324
+ */
325
+ export function servicesStatement(data) {
326
+ const params = new TopologySqlParams();
327
+ const projectId = params.add(data.projectId);
328
+ const projectTypes = new TopologyLazyParam(params, data.winner.projectTypes);
329
+ const where = [
330
+ liveItemSql("i", projectId),
331
+ `i."entityType" = ${params.add(EntityType.Service)}`,
332
+ ...winnerPredicates({
333
+ params,
334
+ alias: "i",
335
+ projectId,
336
+ projectTypes,
337
+ winner: data.winner,
338
+ }),
339
+ ];
340
+ return {
341
+ sql: `SELECT i."entityKey" AS "key", i."displayName" AS "name" ` +
342
+ `FROM "InventoryItem" i WHERE ${where.join(" AND ")} ` +
343
+ `ORDER BY i."entityKey" COLLATE "C" ASC`,
344
+ params: params.list(),
345
+ };
346
+ }
347
+ /*
348
+ * Exact item counts per infrastructure type, in one pass: how many there
349
+ * are, how many reported in range, and the latest report over each. Decides
350
+ * which flat types become collections and feeds the totals.
351
+ */
352
+ export function infrastructureTypeCountsStatement(data) {
353
+ const params = new TopologySqlParams();
354
+ const projectId = params.add(data.projectId);
355
+ const rangeStart = params.add(data.rangeStart);
356
+ const active = activeSql("i", rangeStart);
357
+ return {
358
+ sql: `SELECT i."entityType" AS "type", COUNT(*)::int AS "total", ` +
359
+ `COUNT(*) FILTER (WHERE ${active})::int AS "active", ` +
360
+ `${epochMsSql(`MAX(i."lastSeenAt")`)} AS "lastSeenAt", ` +
361
+ `${epochMsSql(`MAX(i."lastSeenAt") FILTER (WHERE ${active})`)} AS "activeLastSeenAt" ` +
362
+ `FROM "InventoryItem" i ` +
363
+ `WHERE ${liveItemSql("i", projectId)} ` +
364
+ `AND i."entityType" = ANY(${params.add(data.infrastructureTypes)}::text[]) ` +
365
+ `GROUP BY i."entityType" ` +
366
+ `ORDER BY i."entityType" COLLATE "C" ASC`,
367
+ params: params.list(),
368
+ };
369
+ }
370
+ /* The Infrastructure node set: live winners of the given (node) types. */
371
+ function infrastructureNodesCteSql(data) {
372
+ const where = [
373
+ liveItemSql("i", data.projectId),
374
+ `i."entityType" = ANY(${data.nodeTypes}::text[])`,
375
+ ...winnerPredicates({
376
+ params: data.params,
377
+ alias: "i",
378
+ projectId: data.projectId,
379
+ projectTypes: new TopologyLazyParam(data.params, data.winner.projectTypes),
380
+ winner: data.winner,
381
+ }),
382
+ ];
383
+ return (`nodes AS MATERIALIZED (` +
384
+ `SELECT i."entityKey" AS "key", i."entityType" AS "type", ` +
385
+ `i."displayName" AS "name", i."source" AS "source", ` +
386
+ `i."lastSeenAt" AS "lastSeenAt", ` +
387
+ `${activeSql("i", data.rangeStart)} AS "active" ` +
388
+ `FROM "InventoryItem" i WHERE ${where.join(" AND ")})`);
389
+ }
390
+ /*
391
+ * One row per infrastructure resource with its container chosen here, using
392
+ * the Dashboard's nesting rules over EVERY in-range relationship:
393
+ *
394
+ * candidates: C -> P where C's type nests, the relationship type ranks,
395
+ * P is another node and P's type is a container.
396
+ * rank: relationship priority DESC, container specificity DESC,
397
+ * P's key (code-point order, COLLATE "C") ASC.
398
+ *
399
+ * `parent` is the best candidate over all nodes; `activeParent` the best
400
+ * over active nodes, which the browser falls back to when inactive resources
401
+ * are hidden and `parent` did not report in range. Taking the argmax over a
402
+ * subset that contains the global argmax gives the global argmax, so the
403
+ * browser builds exactly the tree all relationships would give it.
404
+ */
405
+ export function infrastructureNodesStatement(data) {
406
+ const params = new TopologySqlParams();
407
+ const projectId = params.add(data.projectId);
408
+ const rangeStart = params.add(data.rangeStart);
409
+ const nodeTypes = params.add(data.nodeTypes);
410
+ const nodesCte = infrastructureNodesCteSql({
411
+ params,
412
+ projectId,
413
+ rangeStart,
414
+ winner: data.winner,
415
+ nodeTypes,
416
+ });
417
+ if (data.mode === "count") {
418
+ return {
419
+ sql: `WITH ${nodesCte} ` +
420
+ `SELECT COUNT(*)::int AS "total", ` +
421
+ `COUNT(*) FILTER (WHERE n."active")::int AS "active" FROM nodes n`,
422
+ params: params.list(),
423
+ };
424
+ }
425
+ const relationshipTypes = params.add(NESTING_RELATIONSHIP_RANKS.keys);
426
+ const relationshipPriorities = params.add(NESTING_RELATIONSHIP_RANKS.ranks);
427
+ const containerTypes = params.add(CONTAINER_SPECIFICITY_RANKS.keys);
428
+ const containerSpecificities = params.add(CONTAINER_SPECIFICITY_RANKS.ranks);
429
+ const nestableTypes = params.add(NESTABLE_CHILD_TYPE_LIST);
430
+ /*
431
+ * Keys sort with COLLATE "C" (code-point order) here for two reasons: the
432
+ * Dashboard breaks the same tie with a code-point comparator, and a byte
433
+ * comparison is an order of magnitude cheaper than the database collation
434
+ * over a few hundred thousand candidates.
435
+ */
436
+ const ranking = `ORDER BY c."child" COLLATE "C", c."priority" DESC, c."specificity" DESC, ` +
437
+ `c."parent" COLLATE "C" ASC`;
438
+ return {
439
+ sql: `WITH ${nodesCte}, ` +
440
+ `candidates AS MATERIALIZED (` +
441
+ `SELECT r."fromEntityKey" AS "child", r."toEntityKey" AS "parent", ` +
442
+ `r."relationshipType" AS "via", ` +
443
+ `(${relationshipPriorities}::int[])[array_position(${relationshipTypes}::text[], r."relationshipType"::text)] AS "priority", ` +
444
+ `(${containerSpecificities}::int[])[array_position(${containerTypes}::text[], p."type"::text)] AS "specificity", ` +
445
+ `p."active" AS "parentActive" ` +
446
+ `FROM "InventoryItemRelationship" r ` +
447
+ `JOIN nodes child ON child."key" = r."fromEntityKey" ` +
448
+ `JOIN nodes p ON p."key" = r."toEntityKey" ` +
449
+ `WHERE ${inRangeRelationshipSql("r", projectId, rangeStart)} ` +
450
+ `AND r."relationshipType" = ANY(${relationshipTypes}::text[]) ` +
451
+ `AND r."fromEntityKey" <> r."toEntityKey" ` +
452
+ `AND child."type" = ANY(${nestableTypes}::text[]) ` +
453
+ `AND p."type" = ANY(${containerTypes}::text[])), ` +
454
+ `best AS (SELECT DISTINCT ON (c."child" COLLATE "C") c."child", c."parent", c."via", c."parentActive" ` +
455
+ `FROM candidates c ${ranking}), ` +
456
+ `best_active AS (SELECT DISTINCT ON (c."child" COLLATE "C") c."child", c."parent", c."via" ` +
457
+ `FROM candidates c WHERE c."parentActive" ${ranking}) ` +
458
+ `SELECT n."key" AS "key", n."type" AS "type", n."name" AS "name", ` +
459
+ `n."source" AS "source", ${epochMsSql(`n."lastSeenAt"`)} AS "lastSeenAt", ` +
460
+ `n."active" AS "active", ` +
461
+ `b."parent" AS "parent", b."via" AS "parentVia", ` +
462
+ `(b."parent" IS NOT NULL AND NOT b."parentActive") AS "parentInactive", ` +
463
+ `ba."parent" AS "activeParent", ba."via" AS "activeParentVia" ` +
464
+ `FROM nodes n ` +
465
+ `LEFT JOIN best b ON b."child" = n."key" ` +
466
+ `LEFT JOIN best_active ba ON ba."child" = n."key" ` +
467
+ `ORDER BY n."type" COLLATE "C" ASC, n."key" COLLATE "C" ASC ` +
468
+ `LIMIT ${params.add(data.limit)}`,
469
+ params: params.list(),
470
+ };
471
+ }
472
+ /* Filters shared by a collection's page, its total and its search count. */
473
+ function collectionWhereSql(data) {
474
+ const where = [
475
+ liveItemSql("i", data.projectId),
476
+ `i."entityType" = ${data.params.add(data.entityType)}`,
477
+ ];
478
+ if (!data.includeInactive) {
479
+ where.push(`(${activeSql("i", data.rangeStart.get())})`);
480
+ }
481
+ where.push(...nameTermsSql("i", data.params, data.nameTerms));
482
+ return where;
483
+ }
484
+ /*
485
+ * One page of a collection, keyset-paged on (display name, key). The name
486
+ * sorts in the database's collation (what a person expects), the key with
487
+ * COLLATE "C" (code points); both orders are total, so a cursor never skips
488
+ * or repeats a row.
489
+ */
490
+ export function collectionPageStatement(data) {
491
+ const params = new TopologySqlParams();
492
+ const projectId = params.add(data.projectId);
493
+ const rangeStart = new TopologyLazyParam(params, data.rangeStart);
494
+ const where = collectionWhereSql({
495
+ params,
496
+ projectId,
497
+ rangeStart,
498
+ entityType: data.entityType,
499
+ includeInactive: data.includeInactive,
500
+ nameTerms: data.nameTerms,
501
+ });
502
+ if (data.mode === "count") {
503
+ return {
504
+ sql: `SELECT COUNT(*)::int AS "total" FROM "InventoryItem" i WHERE ${where.join(" AND ")}`,
505
+ params: params.list(),
506
+ };
507
+ }
508
+ if (data.cursor) {
509
+ const name = params.add(data.cursor.name);
510
+ const key = params.add(data.cursor.key);
511
+ where.push(`(COALESCE(i."displayName", '') > ${name} ` +
512
+ `OR (COALESCE(i."displayName", '') = ${name} ` +
513
+ `AND i."entityKey" COLLATE "C" > ${key}))`);
514
+ }
515
+ return {
516
+ sql: `SELECT ${leanEntityColumnsSql("i")} ` +
517
+ `FROM "InventoryItem" i WHERE ${where.join(" AND ")} ` +
518
+ `ORDER BY COALESCE(i."displayName", '') ASC, i."entityKey" COLLATE "C" ASC ` +
519
+ `LIMIT ${params.add(data.limit)}`,
520
+ params: params.list(),
521
+ };
522
+ }
523
+ /* How many items of each requested collection match its search terms. */
524
+ export function collectionSearchStatement(data) {
525
+ const params = new TopologySqlParams();
526
+ const projectId = params.add(data.projectId);
527
+ const rangeStart = new TopologyLazyParam(params, data.rangeStart);
528
+ const branches = data.types.map((type) => {
529
+ const where = collectionWhereSql({
530
+ params,
531
+ projectId,
532
+ rangeStart,
533
+ entityType: type.entityType,
534
+ includeInactive: data.includeInactive,
535
+ nameTerms: type.nameTerms,
536
+ });
537
+ return (`(SELECT ${params.add(type.entityType)}::text AS "entityType", ` +
538
+ `COUNT(*)::int AS "count" FROM "InventoryItem" i ` +
539
+ `WHERE ${where.join(" AND ")})`);
540
+ });
541
+ return {
542
+ sql: branches.join(" UNION ALL "),
543
+ params: params.list(),
544
+ };
545
+ }
546
+ /*
547
+ * The item behind a drawer or an inventory read. The key alone identifies it
548
+ * (the winning row); a known type narrows the choice when two rows share the
549
+ * key.
550
+ */
551
+ export function entityStatement(data) {
552
+ const params = new TopologySqlParams();
553
+ const projectId = params.add(data.projectId);
554
+ const projectTypes = params.add(data.projectTypes);
555
+ return {
556
+ sql: `SELECT i."_id"::text AS "id", ${leanEntityColumnsSql("i")}, ` +
557
+ `${epochMsSql(`i."firstSeenAt"`)} AS "firstSeenAt", ` +
558
+ `i."resourceType" AS "resourceType", i."resourceId"::text AS "resourceId", ` +
559
+ `i."identifyingAttributes" AS "identifyingAttributes", ` +
560
+ `i."descriptiveAttributes" AS "descriptiveAttributes" ` +
561
+ `FROM "InventoryItem" i ` +
562
+ `WHERE ${data.includeArchived ? storedRowSql("i", projectId) : liveItemSql("i", projectId)} ` +
563
+ `AND i."entityType" = ANY(${projectTypes}::text[]) ` +
564
+ `AND i."entityKey" = ${params.add(data.entityKey)} ` +
565
+ `ORDER BY (i."entityType" = ${params.add(data.entityType)}::text) DESC NULLS LAST, ` +
566
+ `${data.includeArchived ? `i."isArchived" ASC, ` : ""}` +
567
+ `i."createdAt" ASC, i."_id" DESC ` +
568
+ `LIMIT 1`,
569
+ params: params.list(),
570
+ };
571
+ }
572
+ /*
573
+ * An entity's relationships in `scope`, classified into the drawer's four
574
+ * sections, with exact per-section totals and the requested rows.
575
+ *
576
+ * At most `scanLimit` relationships are read — outbound first, then inbound,
577
+ * each newest first — so a cluster with hundreds of thousands of pods in
578
+ * range answers with lower bounds instead of timing out. A self-loop is read
579
+ * once, as outbound. The other end is looked up among live items (the key's
580
+ * winning row); a relationship to anything else stays in its section as an
581
+ * unknown row, and is counted in `unknownTotal`.
582
+ *
583
+ * All time, the other end is looked up among archived items too (a live row
584
+ * still wins a shared key) and each row carries its `otherId`. A relationship
585
+ * that never reported a lastSeenAt sorts first under DESC, so the scan limit
586
+ * drops the oldest discovered edges before it drops one of those.
587
+ *
588
+ * Returns one row: `scanned` (relationships read), `sections` (JSON array of
589
+ * {section, total, unknownTotal}) and `rows` (JSON array, section then rank).
590
+ */
591
+ export function entitySectionsStatement(data) {
592
+ const params = new TopologySqlParams();
593
+ const projectId = params.add(data.projectId);
594
+ const allTime = data.scope.kind === "allTime";
595
+ const relationshipScope = data.scope.kind === "range"
596
+ ? inRangeRelationshipSql("r", projectId, params.add(data.scope.rangeStart))
597
+ : storedRowSql("r", projectId);
598
+ const projectTypes = params.add(data.projectTypes);
599
+ const entityKey = params.add(data.entityKey);
600
+ const scanLimit = params.add(data.scanLimit);
601
+ const dependsOn = params.add(EntityRelationshipType.DependsOn);
602
+ const isService = params.add(data.isService);
603
+ const placementTypes = params.add(PLACEMENT_RELATIONSHIP_TYPE_LIST);
604
+ const scanColumns = (direction, other) => {
605
+ return (`r."relationshipType" AS "relationshipType", '${direction}'::text AS "direction", ` +
606
+ `r.${other} AS "otherKey", r."callCount" AS "callCount", ` +
607
+ `r."errorCount" AS "errorCount", r."avgDurationMs" AS "avgDurationMs", ` +
608
+ `r."lastSeenAt" AS "lastSeenAt"`);
609
+ };
610
+ let rowWindow;
611
+ if (data.window.kind === "top") {
612
+ rowWindow =
613
+ `r."rank" <= CASE WHEN r."section" IN ('calls', 'calledBy') ` +
614
+ `THEN ${params.add(data.window.dependencyRows)}::int ` +
615
+ `ELSE ${params.add(data.window.otherRows)}::int END`;
616
+ }
617
+ else {
618
+ const offset = params.add(data.window.offset);
619
+ rowWindow =
620
+ `r."section" = ${params.add(data.window.section)}::text ` +
621
+ `AND r."rank" > ${offset}::int ` +
622
+ `AND r."rank" <= ${offset}::int + ${params.add(data.window.limit)}::int`;
623
+ }
624
+ return {
625
+ sql: `WITH scan_out AS MATERIALIZED (` +
626
+ `SELECT ${scanColumns("out", `"toEntityKey"`)} ` +
627
+ `FROM "InventoryItemRelationship" r ` +
628
+ `WHERE ${relationshipScope} ` +
629
+ `AND r."fromEntityKey" = ${entityKey} ` +
630
+ `ORDER BY r."lastSeenAt" DESC, r."_id" ASC LIMIT ${scanLimit}::int), ` +
631
+ `scan_in AS MATERIALIZED (` +
632
+ `SELECT ${scanColumns("in", `"fromEntityKey"`)} ` +
633
+ `FROM "InventoryItemRelationship" r ` +
634
+ `WHERE ${relationshipScope} ` +
635
+ `AND r."toEntityKey" = ${entityKey} AND r."fromEntityKey" <> ${entityKey} ` +
636
+ `ORDER BY r."lastSeenAt" DESC, r."_id" ASC ` +
637
+ `LIMIT GREATEST(0, ${scanLimit}::int - (SELECT COUNT(*)::int FROM scan_out))), ` +
638
+ `scan AS (SELECT * FROM scan_out UNION ALL SELECT * FROM scan_in), ` +
639
+ `others AS MATERIALIZED (` +
640
+ `SELECT DISTINCT ON (i."entityKey") i."entityKey" AS "key", ` +
641
+ `${allTime ? `i."_id"::text AS "id", ` : ""}` +
642
+ `i."entityType" AS "type", i."displayName" AS "name" ` +
643
+ `FROM "InventoryItem" i ` +
644
+ `WHERE ${allTime ? storedRowSql("i", projectId) : liveItemSql("i", projectId)} ` +
645
+ `AND i."entityType" = ANY(${projectTypes}::text[]) ` +
646
+ `AND i."entityKey" IN (SELECT s."otherKey" FROM scan s) ` +
647
+ `ORDER BY i."entityKey", ${allTime ? `i."isArchived" ASC, ` : ""}` +
648
+ `i."createdAt" ASC, i."_id" DESC), ` +
649
+ `classified AS MATERIALIZED (` +
650
+ `SELECT s.*, (o."key" IS NOT NULL) AS "otherKnown", ` +
651
+ `${allTime ? `o."id" AS "otherId", ` : ""}` +
652
+ `o."type" AS "otherType", o."name" AS "otherName", ` +
653
+ `CASE WHEN s."relationshipType" = ${dependsOn} AND s."direction" = 'out' THEN 'calls' ` +
654
+ `WHEN s."relationshipType" = ${dependsOn} THEN 'calledBy' ` +
655
+ `WHEN s."direction" = 'out' AND ${isService}::boolean ` +
656
+ `AND s."relationshipType" = ANY(${placementTypes}::text[]) THEN 'runsOn' ` +
657
+ `ELSE 'related' END AS "section", ` +
658
+ `COALESCE(NULLIF(o."name", ''), CASE WHEN o."key" IS NOT NULL ` +
659
+ `THEN '${UNNAMED_RESOURCE_LABEL}' ELSE '${UNDISCOVERED_RESOURCE_LABEL}' END) AS "label" ` +
660
+ `FROM scan s LEFT JOIN others o ON o."key" = s."otherKey"), ` +
661
+ `ranked AS (` +
662
+ `SELECT c.*, ROW_NUMBER() OVER (PARTITION BY c."section" ORDER BY ` +
663
+ `CASE WHEN c."section" IN ('calls', 'calledBy') THEN c."callCount" END DESC NULLS LAST, ` +
664
+ `CASE WHEN c."section" IN ('runsOn', 'related') THEN c."otherKnown" END DESC NULLS LAST, ` +
665
+ `c."label" ASC, c."otherKey" COLLATE "C" ASC, ` +
666
+ `c."relationshipType" COLLATE "C" ASC, c."direction" ASC) AS "rank" ` +
667
+ `FROM classified c) ` +
668
+ `SELECT (SELECT COUNT(*)::int FROM scan) AS "scanned", ` +
669
+ `(SELECT COALESCE(json_agg(json_build_object(` +
670
+ `'section', t."section", 'total', t."total", 'unknownTotal', t."unknownTotal")), '[]'::json) ` +
671
+ `FROM (SELECT c."section" AS "section", COUNT(*)::int AS "total", ` +
672
+ `COUNT(*) FILTER (WHERE NOT c."otherKnown")::int AS "unknownTotal" ` +
673
+ `FROM classified c GROUP BY c."section") t) AS "sections", ` +
674
+ `(SELECT COALESCE(json_agg(json_build_object(` +
675
+ `'section', r."section", 'relationshipType', r."relationshipType", ` +
676
+ `'direction', r."direction", 'otherKey', r."otherKey", ` +
677
+ `'otherKnown', r."otherKnown", ` +
678
+ `${allTime ? `'otherId', r."otherId", ` : ""}` +
679
+ `'otherName', r."otherName", ` +
680
+ `'otherType', r."otherType", 'callCount', r."callCount", ` +
681
+ `'errorCount', r."errorCount", 'avgDurationMs', r."avgDurationMs", ` +
682
+ `'lastSeenAt', ${epochMsSql(`r."lastSeenAt"`)}) ` +
683
+ `ORDER BY r."section", r."rank"), '[]'::json) ` +
684
+ `FROM ranked r WHERE ${rowWindow}) AS "rows"`,
685
+ params: params.list(),
686
+ };
687
+ }
688
+ //# sourceMappingURL=TopologySql.js.map