@oneuptime/common 12.0.7 → 12.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
- package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Server/API/UserAPI.ts +16 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
- package/Server/Services/TelemetryEntityService.ts +114 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
- package/Tests/Types/Workflow/Templates.test.ts +403 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +27 -0
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +7 -3
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +499 -0
- package/Types/Workflow/Templates.ts +316 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +23 -6
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Input/Input.tsx +31 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
- package/UI/Components/Workflow/GraphLint.ts +591 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
- package/UI/Components/Workflow/ModelSchema.ts +196 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/Workflow.tsx +77 -4
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
- package/Utils/Telemetry/EntityKey.ts +66 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +27 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +218 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +12 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import TelemetryEntity from "../../../Models/DatabaseModels/TelemetryEntity";
|
|
3
|
+
import CloudResource from "../../../Models/DatabaseModels/CloudResource";
|
|
4
|
+
import DockerHost from "../../../Models/DatabaseModels/DockerHost";
|
|
5
|
+
import IoTDevice from "../../../Models/DatabaseModels/IoTDevice";
|
|
6
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
7
|
+
import PodmanHost from "../../../Models/DatabaseModels/PodmanHost";
|
|
8
|
+
import RumApplication from "../../../Models/DatabaseModels/RumApplication";
|
|
9
|
+
import ServerlessFunction from "../../../Models/DatabaseModels/ServerlessFunction";
|
|
10
|
+
import DatabaseService from "../../Services/DatabaseService";
|
|
11
|
+
import CloudResourceService from "../../Services/CloudResourceService";
|
|
12
|
+
import DockerHostService from "../../Services/DockerHostService";
|
|
13
|
+
import IoTDeviceService from "../../Services/IoTDeviceService";
|
|
14
|
+
import NetworkDeviceService from "../../Services/NetworkDeviceService";
|
|
15
|
+
import PodmanHostService from "../../Services/PodmanHostService";
|
|
16
|
+
import RumApplicationService from "../../Services/RumApplicationService";
|
|
17
|
+
import ServerlessFunctionService from "../../Services/ServerlessFunctionService";
|
|
18
|
+
import TelemetryEntityService from "../../Services/TelemetryEntityService";
|
|
19
|
+
import Query from "../../Types/Database/Query";
|
|
20
|
+
import QueryHelper from "../../Types/Database/QueryHelper";
|
|
21
|
+
import Select from "../../Types/Database/Select";
|
|
22
|
+
import QueryDeepPartialEntity from "../../../Types/Database/PartialEntity";
|
|
23
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
24
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
25
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
26
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
27
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
28
|
+
import EntitySource from "../../../Types/Telemetry/EntitySource";
|
|
29
|
+
import EntityType from "../../../Types/Telemetry/EntityType";
|
|
30
|
+
import {
|
|
31
|
+
INVENTORY_ENTITY_IDENTITY_ATTRIBUTE,
|
|
32
|
+
keyForInventoryEntity,
|
|
33
|
+
} from "../../../Utils/Telemetry/EntityKey";
|
|
34
|
+
import logger from "../Logger";
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Mirrors OneUptime's inventory tables into the TelemetryEntity registry.
|
|
38
|
+
*
|
|
39
|
+
* The registry began as a projection of OTLP resource attributes, which is
|
|
40
|
+
* why the entity explorer shows a Kubernetes pod but not the switch next to
|
|
41
|
+
* it: half of what OneUptime inventories is collected by pollers (SNMP,
|
|
42
|
+
* cloud APIs, MQTT) that never produce a resource for ingest to extract
|
|
43
|
+
* entities from. Those rows already exist in rich typed tables — they were
|
|
44
|
+
* simply never projected. This module projects them, so "everything we know
|
|
45
|
+
* about the estate" is answerable in one query instead of eight.
|
|
46
|
+
*
|
|
47
|
+
* WHY A RECONCILE RATHER THAN CREATE/DELETE HOOKS ON EACH TABLE
|
|
48
|
+
*
|
|
49
|
+
* Hooks were the obvious approach and are the wrong one here, for three
|
|
50
|
+
* reasons that all bite the same way — a registry that silently disagrees
|
|
51
|
+
* with the inventory it mirrors:
|
|
52
|
+
*
|
|
53
|
+
* 1. Backfill. Hooks only fire on future writes, so every device
|
|
54
|
+
* registered before this shipped would stay invisible forever.
|
|
55
|
+
* 2. Cascade deletes. Deleting a project (or an IoT fleet) removes its
|
|
56
|
+
* devices by FK cascade, which fires no per-row service hook — the
|
|
57
|
+
* mirrored rows would outlive their owners.
|
|
58
|
+
* 3. Seven tables x two hooks is seven chances for a future edit to drop
|
|
59
|
+
* one, and the resulting drift is invisible until someone notices a
|
|
60
|
+
* device missing from the explorer.
|
|
61
|
+
*
|
|
62
|
+
* A periodic full reconcile is level-triggered: whatever the inventory
|
|
63
|
+
* says now is what the registry converges to, no matter how it got there.
|
|
64
|
+
* The cost is latency — a newly registered device appears within one cron
|
|
65
|
+
* period rather than instantly — which is an easy trade for inventory that
|
|
66
|
+
* changes on a human timescale.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/** Registry rows are read and written in pages of this size. */
|
|
70
|
+
export const INVENTORY_SYNC_PAGE_SIZE: number = 1000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The projection of one inventory row, as the registry stores it. Pure data
|
|
74
|
+
* so the mapping can be tested without a database.
|
|
75
|
+
*/
|
|
76
|
+
export interface InventoryRowProjection {
|
|
77
|
+
id: ObjectID;
|
|
78
|
+
projectId: ObjectID;
|
|
79
|
+
displayName: string;
|
|
80
|
+
descriptiveAttributes: Dictionary<string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One inventory table, type-erased at the boundary so the seven specs can
|
|
85
|
+
* live in a single array. `defineInventorySource` below preserves full type
|
|
86
|
+
* safety at each definition site; only the resulting closures are untyped.
|
|
87
|
+
*/
|
|
88
|
+
export interface ErasedInventorySource {
|
|
89
|
+
entityType: EntityType;
|
|
90
|
+
/** Model name stored in TelemetryEntity.resourceType. */
|
|
91
|
+
resourceType: string;
|
|
92
|
+
fetchPage(data: {
|
|
93
|
+
skip: number;
|
|
94
|
+
limit: number;
|
|
95
|
+
}): Promise<Array<InventoryRowProjection>>;
|
|
96
|
+
/** Which of these ids still exist in the owning table. */
|
|
97
|
+
findLiveIds(ids: Array<ObjectID>): Promise<Set<string>>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface InventorySourceSpec<TModel extends BaseModel> {
|
|
101
|
+
entityType: EntityType;
|
|
102
|
+
resourceType: string;
|
|
103
|
+
service: DatabaseService<TModel>;
|
|
104
|
+
/**
|
|
105
|
+
* Columns needed by `describe`, beyond `_id` / `projectId` / `name` which
|
|
106
|
+
* are always selected.
|
|
107
|
+
*/
|
|
108
|
+
select: Select<TModel>;
|
|
109
|
+
/**
|
|
110
|
+
* Restricts what is mirrored — archived rows are excluded so the explorer
|
|
111
|
+
* reflects the live estate rather than its history.
|
|
112
|
+
*/
|
|
113
|
+
query: Query<TModel>;
|
|
114
|
+
/** Non-identifying metadata worth showing on the entity. */
|
|
115
|
+
describe(row: TModel): Dictionary<string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Drops empty values so the stored attribute bag contains only things worth
|
|
120
|
+
* displaying — an inventory row with no OS recorded should have no `os.type`
|
|
121
|
+
* key rather than an empty one.
|
|
122
|
+
*/
|
|
123
|
+
export function compactAttributes(
|
|
124
|
+
attributes: Dictionary<string | undefined | null>,
|
|
125
|
+
): Dictionary<string> {
|
|
126
|
+
const out: Dictionary<string> = {};
|
|
127
|
+
|
|
128
|
+
for (const key of Object.keys(attributes)) {
|
|
129
|
+
const value: string | undefined | null = attributes[key];
|
|
130
|
+
if (value === undefined || value === null) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const text: string = String(value).trim();
|
|
134
|
+
if (text.length === 0) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
out[key] = text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds the registry row for one inventory row. Pure: same inputs, same
|
|
145
|
+
* output, no database — this is the part worth testing exhaustively, since
|
|
146
|
+
* an identity mistake here is what would mint duplicate entities.
|
|
147
|
+
*/
|
|
148
|
+
export function buildInventoryEntityModel(data: {
|
|
149
|
+
source: Pick<ErasedInventorySource, "entityType" | "resourceType">;
|
|
150
|
+
row: InventoryRowProjection;
|
|
151
|
+
now: Date;
|
|
152
|
+
}): TelemetryEntity {
|
|
153
|
+
const { source, row, now } = data;
|
|
154
|
+
|
|
155
|
+
const model: TelemetryEntity = new TelemetryEntity();
|
|
156
|
+
model.projectId = row.projectId;
|
|
157
|
+
model.entityType = source.entityType;
|
|
158
|
+
model.entityKey = keyForInventoryEntity(
|
|
159
|
+
row.projectId.toString(),
|
|
160
|
+
source.entityType,
|
|
161
|
+
row.id.toString(),
|
|
162
|
+
);
|
|
163
|
+
model.source = EntitySource.Inventory;
|
|
164
|
+
/*
|
|
165
|
+
* Identity is the owning row's id, not its name: renaming a device in the
|
|
166
|
+
* UI must not orphan its registry row and mint a second one beside it.
|
|
167
|
+
*/
|
|
168
|
+
model.identifyingAttributes = {
|
|
169
|
+
[INVENTORY_ENTITY_IDENTITY_ATTRIBUTE]: row.id.toString(),
|
|
170
|
+
} as JSONObject;
|
|
171
|
+
model.displayName = row.displayName.substring(0, ColumnLength.ShortText);
|
|
172
|
+
model.descriptiveAttributes = row.descriptiveAttributes as JSONObject;
|
|
173
|
+
/*
|
|
174
|
+
* The polymorphic pointer back to the rich typed row. Populated here for
|
|
175
|
+
* every mirrored type — previously only `service` entities carried one —
|
|
176
|
+
* so a registry row can be resolved to the page that owns it.
|
|
177
|
+
*/
|
|
178
|
+
model.resourceType = source.resourceType;
|
|
179
|
+
model.resourceId = row.id;
|
|
180
|
+
/*
|
|
181
|
+
* Both stamped at mirror time. `lastSeenAt` never advances for these rows
|
|
182
|
+
* (no telemetry heartbeat exists to advance it), which is precisely why
|
|
183
|
+
* PruneStaleEntities excludes EntitySource.Inventory.
|
|
184
|
+
*/
|
|
185
|
+
model.firstSeenAt = now;
|
|
186
|
+
model.lastSeenAt = now;
|
|
187
|
+
|
|
188
|
+
return model;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* True when a mirrored row has drifted from its owning inventory row and
|
|
193
|
+
* needs rewriting. Compared field by field rather than blindly updating so a
|
|
194
|
+
* steady-state reconcile issues no writes at all.
|
|
195
|
+
*/
|
|
196
|
+
export function inventoryEntityNeedsUpdate(data: {
|
|
197
|
+
existing: TelemetryEntity;
|
|
198
|
+
desired: TelemetryEntity;
|
|
199
|
+
}): boolean {
|
|
200
|
+
const { existing, desired } = data;
|
|
201
|
+
|
|
202
|
+
if ((existing.displayName || "") !== (desired.displayName || "")) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (existing.resourceType !== desired.resourceType) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (existing.resourceId?.toString() !== desired.resourceId?.toString()) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
JSON.stringify(existing.descriptiveAttributes || {}) !==
|
|
216
|
+
JSON.stringify(desired.descriptiveAttributes || {})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The columns a drifted mirror rewrites.
|
|
222
|
+
*
|
|
223
|
+
* Built as a plain shape and cast once, for the same two reasons as
|
|
224
|
+
* TelemetryEntityService.buildDescriptiveUpdate: assigning into
|
|
225
|
+
* `QueryDeepPartialEntity<TelemetryEntity>` directly makes TS instantiate the
|
|
226
|
+
* recursive JSONObject mapping (TS2589), and under this project's
|
|
227
|
+
* `exactOptionalPropertyTypes` an optional model field typed `string |
|
|
228
|
+
* undefined` is not assignable to the update type at all. Keys are therefore
|
|
229
|
+
* assigned only when they have a value rather than being set to undefined —
|
|
230
|
+
* which is also the correct write semantics: absent means "leave it alone",
|
|
231
|
+
* not "null it out".
|
|
232
|
+
*/
|
|
233
|
+
function buildMirrorUpdate(
|
|
234
|
+
desired: TelemetryEntity,
|
|
235
|
+
): QueryDeepPartialEntity<TelemetryEntity> {
|
|
236
|
+
const update: {
|
|
237
|
+
displayName?: string;
|
|
238
|
+
descriptiveAttributes?: JSONObject;
|
|
239
|
+
resourceType?: string;
|
|
240
|
+
resourceId?: ObjectID;
|
|
241
|
+
} = {};
|
|
242
|
+
|
|
243
|
+
if (desired.displayName !== undefined) {
|
|
244
|
+
update.displayName = desired.displayName;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (desired.descriptiveAttributes !== undefined) {
|
|
248
|
+
update.descriptiveAttributes = desired.descriptiveAttributes;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (desired.resourceType !== undefined) {
|
|
252
|
+
update.resourceType = desired.resourceType;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (desired.resourceId !== undefined) {
|
|
256
|
+
update.resourceId = desired.resourceId;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return update as unknown as QueryDeepPartialEntity<TelemetryEntity>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function defineInventorySource<TModel extends BaseModel>(
|
|
263
|
+
spec: InventorySourceSpec<TModel>,
|
|
264
|
+
): ErasedInventorySource {
|
|
265
|
+
return {
|
|
266
|
+
entityType: spec.entityType,
|
|
267
|
+
resourceType: spec.resourceType,
|
|
268
|
+
|
|
269
|
+
fetchPage: async (data: {
|
|
270
|
+
skip: number;
|
|
271
|
+
limit: number;
|
|
272
|
+
}): Promise<Array<InventoryRowProjection>> => {
|
|
273
|
+
const rows: Array<TModel> = await spec.service.findBy({
|
|
274
|
+
query: spec.query,
|
|
275
|
+
select: {
|
|
276
|
+
_id: true,
|
|
277
|
+
projectId: true,
|
|
278
|
+
name: true,
|
|
279
|
+
...spec.select,
|
|
280
|
+
} as Select<TModel>,
|
|
281
|
+
skip: data.skip,
|
|
282
|
+
limit: data.limit,
|
|
283
|
+
props: { isRoot: true },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const projections: Array<InventoryRowProjection> = [];
|
|
287
|
+
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
const rowRecord: Record<string, unknown> = row as unknown as Record<
|
|
290
|
+
string,
|
|
291
|
+
unknown
|
|
292
|
+
>;
|
|
293
|
+
const projectId: unknown = rowRecord["projectId"];
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* A row with no project cannot be mirrored — the entity key folds
|
|
297
|
+
* projectId in, and the registry is tenant-scoped. Should not occur;
|
|
298
|
+
* skipped rather than throwing so one bad row cannot stall the sweep.
|
|
299
|
+
*/
|
|
300
|
+
if (!row._id || !projectId) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const name: unknown = rowRecord["name"];
|
|
305
|
+
const displayName: string = String(name || "").trim();
|
|
306
|
+
|
|
307
|
+
projections.push({
|
|
308
|
+
id: new ObjectID(row._id.toString()),
|
|
309
|
+
projectId: new ObjectID(String(projectId)),
|
|
310
|
+
displayName:
|
|
311
|
+
displayName.length > 0 ? displayName : row._id.toString(),
|
|
312
|
+
descriptiveAttributes: spec.describe(row),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return projections;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
findLiveIds: async (ids: Array<ObjectID>): Promise<Set<string>> => {
|
|
320
|
+
if (ids.length === 0) {
|
|
321
|
+
return new Set<string>();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* Deliberately unfiltered by `spec.query`: this answers "does the row
|
|
326
|
+
* still exist", and an archived row still exists. Reusing the mirror
|
|
327
|
+
* query here would make archiving delete the registry row rather than
|
|
328
|
+
* merely stop refreshing it, which is a different (and destructive)
|
|
329
|
+
* behaviour from the one intended.
|
|
330
|
+
*/
|
|
331
|
+
const rows: Array<TModel> = await spec.service.findBy({
|
|
332
|
+
query: {
|
|
333
|
+
_id: QueryHelper.any(
|
|
334
|
+
ids.map((id: ObjectID) => {
|
|
335
|
+
return id.toString();
|
|
336
|
+
}),
|
|
337
|
+
),
|
|
338
|
+
} as Query<TModel>,
|
|
339
|
+
select: { _id: true } as Select<TModel>,
|
|
340
|
+
skip: 0,
|
|
341
|
+
limit: ids.length,
|
|
342
|
+
props: { isRoot: true },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return new Set<string>(
|
|
346
|
+
rows.map((row: TModel) => {
|
|
347
|
+
return row._id!.toString();
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/*
|
|
355
|
+
* Archived rows are excluded from every source that has the concept: the
|
|
356
|
+
* explorer is a view of the live estate. IoTDevice and NetworkDevice have no
|
|
357
|
+
* archive flag, so their queries are unrestricted.
|
|
358
|
+
*/
|
|
359
|
+
export const INVENTORY_SOURCES: ReadonlyArray<ErasedInventorySource> = [
|
|
360
|
+
defineInventorySource<NetworkDevice>({
|
|
361
|
+
entityType: EntityType.NetworkDevice,
|
|
362
|
+
resourceType: "NetworkDevice",
|
|
363
|
+
service: NetworkDeviceService,
|
|
364
|
+
select: { hostname: true },
|
|
365
|
+
query: {},
|
|
366
|
+
describe: (row: NetworkDevice): Dictionary<string> => {
|
|
367
|
+
return compactAttributes({ "net.device.hostname": row.hostname });
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
defineInventorySource<CloudResource>({
|
|
371
|
+
entityType: EntityType.CloudResource,
|
|
372
|
+
resourceType: "CloudResource",
|
|
373
|
+
service: CloudResourceService,
|
|
374
|
+
select: {
|
|
375
|
+
resourceIdentifier: true,
|
|
376
|
+
cloudProvider: true,
|
|
377
|
+
cloudRegion: true,
|
|
378
|
+
cloudAccountId: true,
|
|
379
|
+
},
|
|
380
|
+
query: { isArchived: false },
|
|
381
|
+
describe: (row: CloudResource): Dictionary<string> => {
|
|
382
|
+
return compactAttributes({
|
|
383
|
+
"cloud.resource.id": row.resourceIdentifier,
|
|
384
|
+
"cloud.provider": row.cloudProvider,
|
|
385
|
+
"cloud.region": row.cloudRegion,
|
|
386
|
+
"cloud.account.id": row.cloudAccountId,
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
defineInventorySource<ServerlessFunction>({
|
|
391
|
+
entityType: EntityType.ServerlessFunction,
|
|
392
|
+
resourceType: "ServerlessFunction",
|
|
393
|
+
service: ServerlessFunctionService,
|
|
394
|
+
select: {
|
|
395
|
+
functionIdentifier: true,
|
|
396
|
+
cloudProvider: true,
|
|
397
|
+
cloudRegion: true,
|
|
398
|
+
runtimeName: true,
|
|
399
|
+
},
|
|
400
|
+
query: { isArchived: false },
|
|
401
|
+
describe: (row: ServerlessFunction): Dictionary<string> => {
|
|
402
|
+
return compactAttributes({
|
|
403
|
+
"faas.name": row.functionIdentifier,
|
|
404
|
+
"cloud.provider": row.cloudProvider,
|
|
405
|
+
"cloud.region": row.cloudRegion,
|
|
406
|
+
"process.runtime.name": row.runtimeName,
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
defineInventorySource<RumApplication>({
|
|
411
|
+
entityType: EntityType.RumApplication,
|
|
412
|
+
resourceType: "RumApplication",
|
|
413
|
+
service: RumApplicationService,
|
|
414
|
+
select: { appIdentifier: true, clientType: true, sdkLanguage: true },
|
|
415
|
+
query: { isArchived: false },
|
|
416
|
+
describe: (row: RumApplication): Dictionary<string> => {
|
|
417
|
+
return compactAttributes({
|
|
418
|
+
"rum.app.id": row.appIdentifier,
|
|
419
|
+
"rum.client.type": row.clientType,
|
|
420
|
+
"telemetry.sdk.language": row.sdkLanguage,
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
defineInventorySource<IoTDevice>({
|
|
425
|
+
entityType: EntityType.IoTDevice,
|
|
426
|
+
resourceType: "IoTDevice",
|
|
427
|
+
service: IoTDeviceService,
|
|
428
|
+
select: { externalId: true, deviceType: true, firmwareVersion: true },
|
|
429
|
+
query: {},
|
|
430
|
+
describe: (row: IoTDevice): Dictionary<string> => {
|
|
431
|
+
return compactAttributes({
|
|
432
|
+
"device.id": row.externalId,
|
|
433
|
+
"device.type": row.deviceType,
|
|
434
|
+
"device.firmware.version": row.firmwareVersion,
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
defineInventorySource<DockerHost>({
|
|
439
|
+
entityType: EntityType.DockerHost,
|
|
440
|
+
resourceType: "DockerHost",
|
|
441
|
+
service: DockerHostService,
|
|
442
|
+
select: { hostIdentifier: true, osType: true, osVersion: true },
|
|
443
|
+
query: { isArchived: false },
|
|
444
|
+
describe: (row: DockerHost): Dictionary<string> => {
|
|
445
|
+
return compactAttributes({
|
|
446
|
+
"host.name": row.hostIdentifier,
|
|
447
|
+
"os.type": row.osType,
|
|
448
|
+
"os.version": row.osVersion,
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
defineInventorySource<PodmanHost>({
|
|
453
|
+
entityType: EntityType.PodmanHost,
|
|
454
|
+
resourceType: "PodmanHost",
|
|
455
|
+
service: PodmanHostService,
|
|
456
|
+
select: { hostIdentifier: true, osType: true, osVersion: true },
|
|
457
|
+
query: { isArchived: false },
|
|
458
|
+
describe: (row: PodmanHost): Dictionary<string> => {
|
|
459
|
+
return compactAttributes({
|
|
460
|
+
"host.name": row.hostIdentifier,
|
|
461
|
+
"os.type": row.osType,
|
|
462
|
+
"os.version": row.osVersion,
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
}),
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
export interface InventorySyncResult {
|
|
469
|
+
created: number;
|
|
470
|
+
updated: number;
|
|
471
|
+
deleted: number;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Forward pass: every live inventory row gets a registry row, and one that
|
|
476
|
+
* has drifted (renamed, re-tagged) is rewritten.
|
|
477
|
+
*/
|
|
478
|
+
async function mirrorRows(
|
|
479
|
+
source: ErasedInventorySource,
|
|
480
|
+
): Promise<{ created: number; updated: number }> {
|
|
481
|
+
let created: number = 0;
|
|
482
|
+
let updated: number = 0;
|
|
483
|
+
let skip: number = 0;
|
|
484
|
+
|
|
485
|
+
for (;;) {
|
|
486
|
+
const rows: Array<InventoryRowProjection> = await source.fetchPage({
|
|
487
|
+
skip,
|
|
488
|
+
limit: INVENTORY_SYNC_PAGE_SIZE,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
if (rows.length === 0) {
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const now: Date = OneUptimeDate.getCurrentDate();
|
|
496
|
+
|
|
497
|
+
const desiredByKey: Map<string, TelemetryEntity> = new Map<
|
|
498
|
+
string,
|
|
499
|
+
TelemetryEntity
|
|
500
|
+
>();
|
|
501
|
+
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
504
|
+
source,
|
|
505
|
+
row,
|
|
506
|
+
now,
|
|
507
|
+
});
|
|
508
|
+
desiredByKey.set(model.entityKey!, model);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const existingRows: Array<TelemetryEntity> =
|
|
512
|
+
await TelemetryEntityService.findBy({
|
|
513
|
+
query: {
|
|
514
|
+
entityType: source.entityType,
|
|
515
|
+
entityKey: QueryHelper.any(Array.from(desiredByKey.keys())),
|
|
516
|
+
},
|
|
517
|
+
select: {
|
|
518
|
+
_id: true,
|
|
519
|
+
entityKey: true,
|
|
520
|
+
displayName: true,
|
|
521
|
+
descriptiveAttributes: true,
|
|
522
|
+
resourceType: true,
|
|
523
|
+
resourceId: true,
|
|
524
|
+
},
|
|
525
|
+
skip: 0,
|
|
526
|
+
limit: desiredByKey.size,
|
|
527
|
+
props: { isRoot: true },
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const existingByKey: Map<string, TelemetryEntity> = new Map<
|
|
531
|
+
string,
|
|
532
|
+
TelemetryEntity
|
|
533
|
+
>();
|
|
534
|
+
for (const existing of existingRows) {
|
|
535
|
+
existingByKey.set(existing.entityKey!, existing);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
for (const [key, desired] of desiredByKey.entries()) {
|
|
539
|
+
const existing: TelemetryEntity | undefined = existingByKey.get(key);
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
if (!existing) {
|
|
543
|
+
await TelemetryEntityService.create({
|
|
544
|
+
data: desired,
|
|
545
|
+
props: { isRoot: true },
|
|
546
|
+
});
|
|
547
|
+
created++;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (inventoryEntityNeedsUpdate({ existing, desired })) {
|
|
552
|
+
await TelemetryEntityService.updateOneById({
|
|
553
|
+
id: existing.id!,
|
|
554
|
+
data: buildMirrorUpdate(desired),
|
|
555
|
+
props: { isRoot: true },
|
|
556
|
+
});
|
|
557
|
+
updated++;
|
|
558
|
+
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
/*
|
|
561
|
+
* One unmirrorable row must not stall the rest of the estate. The
|
|
562
|
+
* next run retries it, so a transient failure self-heals.
|
|
563
|
+
*/
|
|
564
|
+
logger.error(
|
|
565
|
+
`InventoryEntityRegistry: failed to mirror ${source.resourceType} row for entity key ${key}:`,
|
|
566
|
+
);
|
|
567
|
+
logger.error(err as Error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
skip += rows.length;
|
|
572
|
+
|
|
573
|
+
if (rows.length < INVENTORY_SYNC_PAGE_SIZE) {
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { created, updated };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Reverse pass: a mirrored row whose owning inventory row is gone is
|
|
583
|
+
* deleted. This is what catches cascade deletes, which fire no service hook.
|
|
584
|
+
*/
|
|
585
|
+
async function removeOrphans(source: ErasedInventorySource): Promise<number> {
|
|
586
|
+
const orphanIds: Array<ObjectID> = [];
|
|
587
|
+
let skip: number = 0;
|
|
588
|
+
|
|
589
|
+
/*
|
|
590
|
+
* Collected across a read-only pass and deleted afterwards. Deleting while
|
|
591
|
+
* paging would shift subsequent offsets and skip rows.
|
|
592
|
+
*/
|
|
593
|
+
for (;;) {
|
|
594
|
+
const rows: Array<TelemetryEntity> = await TelemetryEntityService.findBy({
|
|
595
|
+
query: {
|
|
596
|
+
entityType: source.entityType,
|
|
597
|
+
source: EntitySource.Inventory,
|
|
598
|
+
},
|
|
599
|
+
select: { _id: true, resourceId: true },
|
|
600
|
+
skip,
|
|
601
|
+
limit: INVENTORY_SYNC_PAGE_SIZE,
|
|
602
|
+
props: { isRoot: true },
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (rows.length === 0) {
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const resourceIds: Array<ObjectID> = [];
|
|
610
|
+
for (const row of rows) {
|
|
611
|
+
if (row.resourceId) {
|
|
612
|
+
resourceIds.push(row.resourceId);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const liveIds: Set<string> = await source.findLiveIds(resourceIds);
|
|
617
|
+
|
|
618
|
+
for (const row of rows) {
|
|
619
|
+
/*
|
|
620
|
+
* A mirrored row with no pointer cannot be matched back to anything,
|
|
621
|
+
* so it is orphaned by definition.
|
|
622
|
+
*/
|
|
623
|
+
if (!row.resourceId || !liveIds.has(row.resourceId.toString())) {
|
|
624
|
+
orphanIds.push(row.id!);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
skip += rows.length;
|
|
629
|
+
|
|
630
|
+
if (rows.length < INVENTORY_SYNC_PAGE_SIZE) {
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (orphanIds.length === 0) {
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
await TelemetryEntityService.hardDeleteBy({
|
|
640
|
+
query: {
|
|
641
|
+
_id: QueryHelper.any(
|
|
642
|
+
orphanIds.map((id: ObjectID) => {
|
|
643
|
+
return id.toString();
|
|
644
|
+
}),
|
|
645
|
+
),
|
|
646
|
+
// Re-asserted so a bug upstream can never widen this into a broad delete.
|
|
647
|
+
source: EntitySource.Inventory,
|
|
648
|
+
},
|
|
649
|
+
limit: orphanIds.length,
|
|
650
|
+
skip: 0,
|
|
651
|
+
props: { isRoot: true },
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return orphanIds.length;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Reconciles one inventory table into the registry. */
|
|
658
|
+
export async function syncInventorySource(
|
|
659
|
+
source: ErasedInventorySource,
|
|
660
|
+
): Promise<InventorySyncResult> {
|
|
661
|
+
const { created, updated } = await mirrorRows(source);
|
|
662
|
+
const deleted: number = await removeOrphans(source);
|
|
663
|
+
return { created, updated, deleted };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Reconciles every inventory table. Each source is isolated: a failure in
|
|
668
|
+
* one leaves the others to complete, because a broken cloud integration
|
|
669
|
+
* should not stop network devices being mirrored.
|
|
670
|
+
*/
|
|
671
|
+
export async function syncAllInventorySources(): Promise<InventorySyncResult> {
|
|
672
|
+
const total: InventorySyncResult = { created: 0, updated: 0, deleted: 0 };
|
|
673
|
+
|
|
674
|
+
for (const source of INVENTORY_SOURCES) {
|
|
675
|
+
try {
|
|
676
|
+
const result: InventorySyncResult = await syncInventorySource(source);
|
|
677
|
+
total.created += result.created;
|
|
678
|
+
total.updated += result.updated;
|
|
679
|
+
total.deleted += result.deleted;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
logger.error(
|
|
682
|
+
`InventoryEntityRegistry: sync failed for ${source.resourceType}:`,
|
|
683
|
+
);
|
|
684
|
+
logger.error(err as Error);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return total;
|
|
689
|
+
}
|