@oneuptime/common 12.0.10 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/package.json +1 -1
|
@@ -154,6 +154,18 @@ const toScalar: ToScalarFunction = (value: unknown): ScalarValue => {
|
|
|
154
154
|
return value.toString();
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/*
|
|
158
|
+
* A Date only reaches here from a query built server-side: one that arrived
|
|
159
|
+
* over HTTP has already been through JSON, which renders a Date as its ISO
|
|
160
|
+
* string. Both paths must produce the same text, because the ordering
|
|
161
|
+
* comparisons below are lexicographic and only ISO-8601 makes that
|
|
162
|
+
* chronological. The `String(value)` fallback would yield
|
|
163
|
+
* "Sun Aug 17 2026 00:00:00 GMT+0000", which sorts by weekday name.
|
|
164
|
+
*/
|
|
165
|
+
if (value instanceof Date) {
|
|
166
|
+
return value.toISOString();
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
if (value === null || value === undefined) {
|
|
158
170
|
return "";
|
|
159
171
|
}
|
|
@@ -313,6 +325,32 @@ class KeyPredicateBuilder {
|
|
|
313
325
|
};
|
|
314
326
|
}
|
|
315
327
|
|
|
328
|
+
/**
|
|
329
|
+
* An ordering comparison against a value whose type we only learn at runtime.
|
|
330
|
+
*
|
|
331
|
+
* A jsonb key holds whatever the project put there, so `>` has two readings.
|
|
332
|
+
* Numbers compare numerically — "10" must be greater than "9", which text
|
|
333
|
+
* comparison gets backwards. Everything else compares as text, and the case
|
|
334
|
+
* that matters is a date: custom field dates are stored as ISO-8601 strings,
|
|
335
|
+
* and ISO-8601 is designed so that lexicographic order IS chronological
|
|
336
|
+
* order, so `>=` over the raw text is already the right question.
|
|
337
|
+
*
|
|
338
|
+
* Routing dates through compareNumeric — which is what these operators used
|
|
339
|
+
* to do unconditionally — was silently wrong rather than loudly wrong.
|
|
340
|
+
* `Number("2026-08-17T00:00:00.000Z")` is NaN, so the bound parameter became
|
|
341
|
+
* `CAST(NaN AS NUMERIC)`, and `numericExpression` independently returns NULL
|
|
342
|
+
* for text that does not look like a number. The predicate was therefore
|
|
343
|
+
* `NULL > NaN` on every row: no error, no rows, and a lit filter chip over an
|
|
344
|
+
* empty table.
|
|
345
|
+
*
|
|
346
|
+
* InBetween has always branched this way; these four now agree with it.
|
|
347
|
+
*/
|
|
348
|
+
private compareOrdered(operator: string, value: unknown): PredicateFragment {
|
|
349
|
+
return isNumericValue(toScalar(value))
|
|
350
|
+
? this.compareNumeric(operator, value)
|
|
351
|
+
: this.compareText(operator, value);
|
|
352
|
+
}
|
|
353
|
+
|
|
316
354
|
/**
|
|
317
355
|
* "No value here" — the key is absent, explicitly JSON null, an empty
|
|
318
356
|
* string, or an empty array. A multi-select cleared in the UI leaves `[]`
|
|
@@ -466,19 +504,19 @@ class KeyPredicateBuilder {
|
|
|
466
504
|
}
|
|
467
505
|
|
|
468
506
|
if (value instanceof GreaterThanOrEqual) {
|
|
469
|
-
return this.
|
|
507
|
+
return this.compareOrdered(">=", value.value);
|
|
470
508
|
}
|
|
471
509
|
|
|
472
510
|
if (value instanceof GreaterThan) {
|
|
473
|
-
return this.
|
|
511
|
+
return this.compareOrdered(">", value.value);
|
|
474
512
|
}
|
|
475
513
|
|
|
476
514
|
if (value instanceof LessThanOrEqual) {
|
|
477
|
-
return this.
|
|
515
|
+
return this.compareOrdered("<=", value.value);
|
|
478
516
|
}
|
|
479
517
|
|
|
480
518
|
if (value instanceof LessThan) {
|
|
481
|
-
return this.
|
|
519
|
+
return this.compareOrdered("<", value.value);
|
|
482
520
|
}
|
|
483
521
|
|
|
484
522
|
/*
|
|
@@ -353,8 +353,8 @@ function defineInventorySource<TModel extends BaseModel>(
|
|
|
353
353
|
|
|
354
354
|
/*
|
|
355
355
|
* Archived rows are excluded from every source that has the concept: the
|
|
356
|
-
* explorer is a view of the live estate. IoTDevice
|
|
357
|
-
* archive flag, so
|
|
356
|
+
* explorer is a view of the live estate. IoTDevice is the only source without
|
|
357
|
+
* an archive flag, so it is the only unrestricted query.
|
|
358
358
|
*/
|
|
359
359
|
export const INVENTORY_SOURCES: ReadonlyArray<ErasedInventorySource> = [
|
|
360
360
|
defineInventorySource<NetworkDevice>({
|
|
@@ -362,7 +362,7 @@ export const INVENTORY_SOURCES: ReadonlyArray<ErasedInventorySource> = [
|
|
|
362
362
|
resourceType: "NetworkDevice",
|
|
363
363
|
service: NetworkDeviceService,
|
|
364
364
|
select: { hostname: true },
|
|
365
|
-
query: {},
|
|
365
|
+
query: { isArchived: false },
|
|
366
366
|
describe: (row: NetworkDevice): Dictionary<string> => {
|
|
367
367
|
return compactAttributes({ "net.device.hostname": row.hostname });
|
|
368
368
|
},
|
|
@@ -469,6 +469,12 @@ export interface InventorySyncResult {
|
|
|
469
469
|
created: number;
|
|
470
470
|
updated: number;
|
|
471
471
|
deleted: number;
|
|
472
|
+
/**
|
|
473
|
+
* Orphans that were archived rather than deleted because they carried custom
|
|
474
|
+
* field values. Counted apart from `deleted` so the job log distinguishes
|
|
475
|
+
* "the projection went away" from "we kept someone's data".
|
|
476
|
+
*/
|
|
477
|
+
archived: number;
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
/**
|
|
@@ -579,15 +585,133 @@ async function mirrorRows(
|
|
|
579
585
|
}
|
|
580
586
|
|
|
581
587
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
588
|
+
* Does this row carry anything a person typed?
|
|
589
|
+
*
|
|
590
|
+
* The `customFields` bag is the only user-authored content a mirrored row can
|
|
591
|
+
* hold — every other column on it is rewritten from the owning table on each
|
|
592
|
+
* pass — so it is the whole test for "deleting this loses work".
|
|
593
|
+
*
|
|
594
|
+
* An empty bag counts as nothing: the UI leaves `{}` behind when the last
|
|
595
|
+
* value is cleared, and a row whose fields were all emptied is not one anyone
|
|
596
|
+
* is keeping. A key explicitly set to null or "" is likewise not a value.
|
|
597
|
+
*/
|
|
598
|
+
export type HasCustomFieldValuesFunction = (row: InventoryItem) => boolean;
|
|
599
|
+
|
|
600
|
+
export const hasCustomFieldValues: HasCustomFieldValuesFunction = (
|
|
601
|
+
row: InventoryItem,
|
|
602
|
+
): boolean => {
|
|
603
|
+
const bag: JSONObject | undefined = row.customFields;
|
|
604
|
+
|
|
605
|
+
if (!bag || typeof bag !== "object") {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return Object.keys(bag).some((key: string) => {
|
|
610
|
+
const value: unknown = (bag as JSONObject)[key];
|
|
611
|
+
|
|
612
|
+
if (value === undefined || value === null || value === "") {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return !(Array.isArray(value) && value.length === 0);
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export interface OrphanPartition {
|
|
621
|
+
/** Orphans safe to remove outright — pure projections, nothing typed. */
|
|
622
|
+
deletableIds: Array<ObjectID>;
|
|
623
|
+
/** Orphans that must be kept, because they carry user-authored values. */
|
|
624
|
+
archivableIds: Array<ObjectID>;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export type PartitionOrphanRowsFunction = (data: {
|
|
628
|
+
rows: Array<InventoryItem>;
|
|
629
|
+
/** Ids still present in the owning table, from `findLiveIds`. */
|
|
630
|
+
liveResourceIds: Set<string>;
|
|
631
|
+
}) => OrphanPartition;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The whole decision the reverse pass makes, as a pure function of one page of
|
|
635
|
+
* registry rows and the set of owning-table ids that still exist.
|
|
636
|
+
*
|
|
637
|
+
* Split out from the IO because this is where a mistake destroys data and
|
|
638
|
+
* where a test can actually reach: getting it wrong either hard-deletes rows
|
|
639
|
+
* holding the only copy of someone's serial numbers, or lets the registry
|
|
640
|
+
* accumulate projections of devices that no longer exist.
|
|
641
|
+
*/
|
|
642
|
+
export const partitionOrphanRows: PartitionOrphanRowsFunction = (data: {
|
|
643
|
+
rows: Array<InventoryItem>;
|
|
644
|
+
liveResourceIds: Set<string>;
|
|
645
|
+
}): OrphanPartition => {
|
|
646
|
+
const deletableIds: Array<ObjectID> = [];
|
|
647
|
+
const archivableIds: Array<ObjectID> = [];
|
|
648
|
+
|
|
649
|
+
for (const row of data.rows || []) {
|
|
650
|
+
if (!row.id) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/*
|
|
655
|
+
* A mirrored row with no pointer cannot be matched back to anything, so
|
|
656
|
+
* it is orphaned by definition.
|
|
657
|
+
*/
|
|
658
|
+
const isOrphan: boolean =
|
|
659
|
+
!row.resourceId || !data.liveResourceIds.has(row.resourceId.toString());
|
|
660
|
+
|
|
661
|
+
if (!isOrphan) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!hasCustomFieldValues(row)) {
|
|
666
|
+
deletableIds.push(row.id);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/*
|
|
671
|
+
* Already archived on an earlier sweep. Re-archiving would rewrite
|
|
672
|
+
* `archivedAt` on every 15-minute pass, so the timestamp would report the
|
|
673
|
+
* most recent sweep rather than when the device went away.
|
|
674
|
+
*/
|
|
675
|
+
if (row.isArchived) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
archivableIds.push(row.id);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return { deletableIds, archivableIds };
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Reverse pass: a mirrored row whose owning inventory row is gone stops being
|
|
687
|
+
* part of the live estate. This is what catches cascade deletes, which fire no
|
|
688
|
+
* service hook.
|
|
689
|
+
*
|
|
690
|
+
* Orphans split two ways, on whether the row holds anything a person typed:
|
|
691
|
+
*
|
|
692
|
+
* - nothing typed: hard-deleted, as before. The row was a pure projection of
|
|
693
|
+
* a table row that no longer exists, so there is nothing to keep.
|
|
694
|
+
*
|
|
695
|
+
* - custom field values present: ARCHIVED, not deleted. Those values are the
|
|
696
|
+
* only copy — serial numbers, warranty dates, owning team — and they live
|
|
697
|
+
* on the registry row rather than on the device, so deleting the projection
|
|
698
|
+
* destroys them with no undo and no audit trail (`hardDeleteBy` bypasses
|
|
699
|
+
* the audit hook). Archiving takes the row out of the default list, which
|
|
700
|
+
* is the visible behaviour the delete was there to produce, and leaves the
|
|
701
|
+
* data recoverable from the Archived tab.
|
|
702
|
+
*
|
|
703
|
+
* The cost of being wrong differs by orders of magnitude between the two, which
|
|
704
|
+
* is why the split is here and not left to an operator's retention policy.
|
|
584
705
|
*/
|
|
585
|
-
async function removeOrphans(
|
|
586
|
-
|
|
706
|
+
async function removeOrphans(
|
|
707
|
+
source: ErasedInventorySource,
|
|
708
|
+
): Promise<{ deleted: number; archived: number }> {
|
|
709
|
+
const deletableIds: Array<ObjectID> = [];
|
|
710
|
+
const archivableIds: Array<ObjectID> = [];
|
|
587
711
|
let skip: number = 0;
|
|
588
712
|
|
|
589
713
|
/*
|
|
590
|
-
* Collected across a read-only pass and
|
|
714
|
+
* Collected across a read-only pass and written afterwards. Writing while
|
|
591
715
|
* paging would shift subsequent offsets and skip rows.
|
|
592
716
|
*/
|
|
593
717
|
for (;;) {
|
|
@@ -596,7 +720,12 @@ async function removeOrphans(source: ErasedInventorySource): Promise<number> {
|
|
|
596
720
|
entityType: source.entityType,
|
|
597
721
|
source: EntitySource.Inventory,
|
|
598
722
|
},
|
|
599
|
-
select: {
|
|
723
|
+
select: {
|
|
724
|
+
_id: true,
|
|
725
|
+
resourceId: true,
|
|
726
|
+
customFields: true,
|
|
727
|
+
isArchived: true,
|
|
728
|
+
},
|
|
600
729
|
skip,
|
|
601
730
|
limit: INVENTORY_SYNC_PAGE_SIZE,
|
|
602
731
|
props: { isRoot: true },
|
|
@@ -614,16 +743,13 @@ async function removeOrphans(source: ErasedInventorySource): Promise<number> {
|
|
|
614
743
|
}
|
|
615
744
|
|
|
616
745
|
const liveIds: Set<string> = await source.findLiveIds(resourceIds);
|
|
746
|
+
const partition: OrphanPartition = partitionOrphanRows({
|
|
747
|
+
rows: rows,
|
|
748
|
+
liveResourceIds: liveIds,
|
|
749
|
+
});
|
|
617
750
|
|
|
618
|
-
|
|
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
|
-
}
|
|
751
|
+
deletableIds.push(...partition.deletableIds);
|
|
752
|
+
archivableIds.push(...partition.archivableIds);
|
|
627
753
|
|
|
628
754
|
skip += rows.length;
|
|
629
755
|
|
|
@@ -632,26 +758,42 @@ async function removeOrphans(source: ErasedInventorySource): Promise<number> {
|
|
|
632
758
|
}
|
|
633
759
|
}
|
|
634
760
|
|
|
635
|
-
if (
|
|
636
|
-
|
|
761
|
+
if (deletableIds.length > 0) {
|
|
762
|
+
await InventoryItemService.hardDeleteBy({
|
|
763
|
+
query: {
|
|
764
|
+
_id: QueryHelper.any(
|
|
765
|
+
deletableIds.map((id: ObjectID) => {
|
|
766
|
+
return id.toString();
|
|
767
|
+
}),
|
|
768
|
+
),
|
|
769
|
+
// Re-asserted so a bug upstream can never widen this into a broad delete.
|
|
770
|
+
source: EntitySource.Inventory,
|
|
771
|
+
},
|
|
772
|
+
limit: deletableIds.length,
|
|
773
|
+
skip: 0,
|
|
774
|
+
props: { isRoot: true },
|
|
775
|
+
});
|
|
637
776
|
}
|
|
638
777
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
778
|
+
if (archivableIds.length > 0) {
|
|
779
|
+
await InventoryItemService.updateBy({
|
|
780
|
+
query: {
|
|
781
|
+
_id: QueryHelper.any(
|
|
782
|
+
archivableIds.map((id: ObjectID) => {
|
|
783
|
+
return id.toString();
|
|
784
|
+
}),
|
|
785
|
+
),
|
|
786
|
+
// Same reason as the delete above: never widen past mirrored rows.
|
|
787
|
+
source: EntitySource.Inventory,
|
|
788
|
+
},
|
|
789
|
+
data: { isArchived: true },
|
|
790
|
+
limit: archivableIds.length,
|
|
791
|
+
skip: 0,
|
|
792
|
+
props: { isRoot: true },
|
|
793
|
+
});
|
|
794
|
+
}
|
|
653
795
|
|
|
654
|
-
return
|
|
796
|
+
return { deleted: deletableIds.length, archived: archivableIds.length };
|
|
655
797
|
}
|
|
656
798
|
|
|
657
799
|
/** Reconciles one inventory table into the registry. */
|
|
@@ -659,8 +801,8 @@ export async function syncInventorySource(
|
|
|
659
801
|
source: ErasedInventorySource,
|
|
660
802
|
): Promise<InventorySyncResult> {
|
|
661
803
|
const { created, updated } = await mirrorRows(source);
|
|
662
|
-
const deleted
|
|
663
|
-
return { created, updated, deleted };
|
|
804
|
+
const { deleted, archived } = await removeOrphans(source);
|
|
805
|
+
return { created, updated, deleted, archived };
|
|
664
806
|
}
|
|
665
807
|
|
|
666
808
|
/**
|
|
@@ -669,7 +811,12 @@ export async function syncInventorySource(
|
|
|
669
811
|
* should not stop network devices being mirrored.
|
|
670
812
|
*/
|
|
671
813
|
export async function syncAllInventorySources(): Promise<InventorySyncResult> {
|
|
672
|
-
const total: InventorySyncResult = {
|
|
814
|
+
const total: InventorySyncResult = {
|
|
815
|
+
created: 0,
|
|
816
|
+
updated: 0,
|
|
817
|
+
deleted: 0,
|
|
818
|
+
archived: 0,
|
|
819
|
+
};
|
|
673
820
|
|
|
674
821
|
for (const source of INVENTORY_SOURCES) {
|
|
675
822
|
try {
|
|
@@ -677,6 +824,7 @@ export async function syncAllInventorySources(): Promise<InventorySyncResult> {
|
|
|
677
824
|
total.created += result.created;
|
|
678
825
|
total.updated += result.updated;
|
|
679
826
|
total.deleted += result.deleted;
|
|
827
|
+
total.archived += result.archived;
|
|
680
828
|
} catch (err) {
|
|
681
829
|
logger.error(
|
|
682
830
|
`InventoryEntityRegistry: sync failed for ${source.resourceType}:`,
|