@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
|
@@ -646,6 +646,50 @@ export default class NetworkDevice extends BaseModel {
|
|
|
646
646
|
})
|
|
647
647
|
public monitoringMethod?: string = undefined;
|
|
648
648
|
|
|
649
|
+
@ColumnAccessControl({
|
|
650
|
+
create: [
|
|
651
|
+
Permission.ProjectOwner,
|
|
652
|
+
Permission.ProjectAdmin,
|
|
653
|
+
Permission.ProjectMember,
|
|
654
|
+
Permission.SettingsAdmin,
|
|
655
|
+
Permission.SettingsMember,
|
|
656
|
+
Permission.CreateNetworkDevice,
|
|
657
|
+
],
|
|
658
|
+
read: [
|
|
659
|
+
Permission.ProjectOwner,
|
|
660
|
+
Permission.ProjectAdmin,
|
|
661
|
+
Permission.ProjectMember,
|
|
662
|
+
Permission.Viewer,
|
|
663
|
+
Permission.SettingsAdmin,
|
|
664
|
+
Permission.SettingsMember,
|
|
665
|
+
Permission.SettingsViewer,
|
|
666
|
+
Permission.ReadNetworkDevice,
|
|
667
|
+
],
|
|
668
|
+
update: [
|
|
669
|
+
Permission.ProjectOwner,
|
|
670
|
+
Permission.ProjectAdmin,
|
|
671
|
+
Permission.ProjectMember,
|
|
672
|
+
Permission.SettingsAdmin,
|
|
673
|
+
Permission.SettingsMember,
|
|
674
|
+
Permission.EditNetworkDevice,
|
|
675
|
+
],
|
|
676
|
+
})
|
|
677
|
+
@TableColumn({
|
|
678
|
+
required: false,
|
|
679
|
+
type: TableColumnType.ShortText,
|
|
680
|
+
canReadOnRelationQuery: true,
|
|
681
|
+
title: "Device Role",
|
|
682
|
+
description:
|
|
683
|
+
"What this device does on the network — router, switch, access point and so on. Left empty, the role is worked out from the device's own SNMP identity. Set it when there is no SNMP to read: a ping-only device has no identity to classify, and the role decides both the shape it is drawn with and where it sits in the topology hierarchy.",
|
|
684
|
+
example: "switch",
|
|
685
|
+
})
|
|
686
|
+
@Column({
|
|
687
|
+
nullable: true,
|
|
688
|
+
type: ColumnType.ShortText,
|
|
689
|
+
length: ColumnLength.ShortText,
|
|
690
|
+
})
|
|
691
|
+
public deviceRole?: string = undefined;
|
|
692
|
+
|
|
649
693
|
@ColumnAccessControl({
|
|
650
694
|
create: [
|
|
651
695
|
Permission.ProjectOwner,
|
|
@@ -376,6 +376,122 @@ export default class NetworkDeviceLink extends BaseModel {
|
|
|
376
376
|
})
|
|
377
377
|
public toDeviceId?: ObjectID = undefined;
|
|
378
378
|
|
|
379
|
+
/*
|
|
380
|
+
* --- Which end is up ---
|
|
381
|
+
*
|
|
382
|
+
* A link says two devices are cabled together. It does not say which one
|
|
383
|
+
* the other hangs off, and the topology map has to decide: the
|
|
384
|
+
* parent-child view draws an actual tree, so somebody is the parent.
|
|
385
|
+
* Left to itself it infers one from device roles and connection counts,
|
|
386
|
+
* which is a good guess on a network that speaks SNMP and no guess at
|
|
387
|
+
* all on one that does not — the case issue #3192 is about, where the
|
|
388
|
+
* child is a ping-only device whose role nothing can classify.
|
|
389
|
+
*
|
|
390
|
+
* This column is the operator answering that question outright. It is
|
|
391
|
+
* NULL by default and on every row written before it existed, and NULL
|
|
392
|
+
* means exactly what the map did then: a peer link, direction inferred.
|
|
393
|
+
* So no existing map moves, and a hierarchy only becomes declared when
|
|
394
|
+
* somebody declares it.
|
|
395
|
+
*
|
|
396
|
+
* The value must be one of this link's own two ends — a parent that is
|
|
397
|
+
* not on the link is not a statement about the link — which the service
|
|
398
|
+
* enforces on create and on update, including when an update moves an
|
|
399
|
+
* end out from under a parent that was already set.
|
|
400
|
+
*/
|
|
401
|
+
@ColumnAccessControl({
|
|
402
|
+
create: [
|
|
403
|
+
Permission.ProjectOwner,
|
|
404
|
+
Permission.ProjectAdmin,
|
|
405
|
+
Permission.ProjectMember,
|
|
406
|
+
Permission.SettingsAdmin,
|
|
407
|
+
Permission.SettingsMember,
|
|
408
|
+
Permission.CreateNetworkDeviceLink,
|
|
409
|
+
],
|
|
410
|
+
read: [
|
|
411
|
+
Permission.ProjectOwner,
|
|
412
|
+
Permission.ProjectAdmin,
|
|
413
|
+
Permission.ProjectMember,
|
|
414
|
+
Permission.Viewer,
|
|
415
|
+
Permission.SettingsAdmin,
|
|
416
|
+
Permission.SettingsMember,
|
|
417
|
+
Permission.SettingsViewer,
|
|
418
|
+
Permission.ReadNetworkDeviceLink,
|
|
419
|
+
],
|
|
420
|
+
update: [
|
|
421
|
+
Permission.ProjectOwner,
|
|
422
|
+
Permission.ProjectAdmin,
|
|
423
|
+
Permission.ProjectMember,
|
|
424
|
+
Permission.SettingsAdmin,
|
|
425
|
+
Permission.SettingsMember,
|
|
426
|
+
Permission.EditNetworkDeviceLink,
|
|
427
|
+
],
|
|
428
|
+
})
|
|
429
|
+
@TableColumn({
|
|
430
|
+
manyToOneRelationColumn: "parentDeviceId",
|
|
431
|
+
type: TableColumnType.Entity,
|
|
432
|
+
modelType: NetworkDevice,
|
|
433
|
+
title: "Parent Device",
|
|
434
|
+
description:
|
|
435
|
+
"Relation to whichever end of this link is the parent, if the hierarchy is declared rather than inferred",
|
|
436
|
+
})
|
|
437
|
+
@ManyToOne(
|
|
438
|
+
() => {
|
|
439
|
+
return NetworkDevice;
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
eager: false,
|
|
443
|
+
nullable: true,
|
|
444
|
+
onDelete: "CASCADE",
|
|
445
|
+
orphanedRowAction: "nullify",
|
|
446
|
+
},
|
|
447
|
+
)
|
|
448
|
+
@JoinColumn({ name: "parentDeviceId" })
|
|
449
|
+
public parentDevice?: NetworkDevice = undefined;
|
|
450
|
+
|
|
451
|
+
@ColumnAccessControl({
|
|
452
|
+
create: [
|
|
453
|
+
Permission.ProjectOwner,
|
|
454
|
+
Permission.ProjectAdmin,
|
|
455
|
+
Permission.ProjectMember,
|
|
456
|
+
Permission.SettingsAdmin,
|
|
457
|
+
Permission.SettingsMember,
|
|
458
|
+
Permission.CreateNetworkDeviceLink,
|
|
459
|
+
],
|
|
460
|
+
read: [
|
|
461
|
+
Permission.ProjectOwner,
|
|
462
|
+
Permission.ProjectAdmin,
|
|
463
|
+
Permission.ProjectMember,
|
|
464
|
+
Permission.Viewer,
|
|
465
|
+
Permission.SettingsAdmin,
|
|
466
|
+
Permission.SettingsMember,
|
|
467
|
+
Permission.SettingsViewer,
|
|
468
|
+
Permission.ReadNetworkDeviceLink,
|
|
469
|
+
],
|
|
470
|
+
update: [
|
|
471
|
+
Permission.ProjectOwner,
|
|
472
|
+
Permission.ProjectAdmin,
|
|
473
|
+
Permission.ProjectMember,
|
|
474
|
+
Permission.SettingsAdmin,
|
|
475
|
+
Permission.SettingsMember,
|
|
476
|
+
Permission.EditNetworkDeviceLink,
|
|
477
|
+
],
|
|
478
|
+
})
|
|
479
|
+
@Index()
|
|
480
|
+
@TableColumn({
|
|
481
|
+
type: TableColumnType.ObjectID,
|
|
482
|
+
required: false,
|
|
483
|
+
canReadOnRelationQuery: true,
|
|
484
|
+
title: "Parent Device ID",
|
|
485
|
+
description:
|
|
486
|
+
"ID of whichever end of this link is the parent. Must be the From Device or the To Device. Empty means the two are peers and the map infers the hierarchy.",
|
|
487
|
+
})
|
|
488
|
+
@Column({
|
|
489
|
+
type: ColumnType.ObjectID,
|
|
490
|
+
nullable: true,
|
|
491
|
+
transformer: ObjectID.getDatabaseTransformer(),
|
|
492
|
+
})
|
|
493
|
+
public parentDeviceId?: ObjectID = undefined;
|
|
494
|
+
|
|
379
495
|
@ColumnAccessControl({
|
|
380
496
|
create: [
|
|
381
497
|
Permission.ProjectOwner,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Two nullable columns, both of them an operator stating something the
|
|
5
|
+
* network cannot be asked:
|
|
6
|
+
*
|
|
7
|
+
* NetworkDevice.deviceRole — what this box actually is, for devices
|
|
8
|
+
* with no SNMP identity to classify.
|
|
9
|
+
* NetworkDeviceLink.parentDeviceId — which end of a declared link is up.
|
|
10
|
+
*
|
|
11
|
+
* Nullable on purpose, with no backfill. NULL is a meaningful value in
|
|
12
|
+
* both cases and it is the value every existing row should hold: no role
|
|
13
|
+
* override (so the classifier keeps deciding) and no declared hierarchy
|
|
14
|
+
* (so the map keeps inferring one). Defaulting either would silently
|
|
15
|
+
* restate every device and every link in every project as something an
|
|
16
|
+
* operator had chosen, which is the one thing neither column may mean.
|
|
17
|
+
*/
|
|
18
|
+
export class AddDeviceRoleAndDeclaredLinkParent1787400000000
|
|
19
|
+
implements MigrationInterface
|
|
20
|
+
{
|
|
21
|
+
public name = "AddDeviceRoleAndDeclaredLinkParent1787400000000";
|
|
22
|
+
|
|
23
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
24
|
+
await queryRunner.query(
|
|
25
|
+
`ALTER TABLE "NetworkDevice" ADD "deviceRole" character varying(100)`,
|
|
26
|
+
);
|
|
27
|
+
await queryRunner.query(
|
|
28
|
+
`ALTER TABLE "NetworkDeviceLink" ADD "parentDeviceId" uuid`,
|
|
29
|
+
);
|
|
30
|
+
await queryRunner.query(
|
|
31
|
+
`CREATE INDEX "IDX_01752cd47b773df38b4a54ae53" ON "NetworkDeviceLink" ("parentDeviceId") `,
|
|
32
|
+
);
|
|
33
|
+
await queryRunner.query(
|
|
34
|
+
`ALTER TABLE "NetworkDeviceLink" ADD CONSTRAINT "FK_01752cd47b773df38b4a54ae539" FOREIGN KEY ("parentDeviceId") REFERENCES "NetworkDevice"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
39
|
+
await queryRunner.query(
|
|
40
|
+
`ALTER TABLE "NetworkDeviceLink" DROP CONSTRAINT "FK_01752cd47b773df38b4a54ae539"`,
|
|
41
|
+
);
|
|
42
|
+
await queryRunner.query(
|
|
43
|
+
`DROP INDEX "public"."IDX_01752cd47b773df38b4a54ae53"`,
|
|
44
|
+
);
|
|
45
|
+
await queryRunner.query(
|
|
46
|
+
`ALTER TABLE "NetworkDeviceLink" DROP COLUMN "parentDeviceId"`,
|
|
47
|
+
);
|
|
48
|
+
await queryRunner.query(
|
|
49
|
+
`ALTER TABLE "NetworkDevice" DROP COLUMN "deviceRole"`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -527,6 +527,7 @@ import { AddOnCallNotificationFallbackColumns1787000000000 } from "./17870000000
|
|
|
527
527
|
import { AddAIConversationPageContext1787100000000 } from "./1787100000000-AddAIConversationPageContext";
|
|
528
528
|
import { AddAIChatMessageFeedback1787200000000 } from "./1787200000000-AddAIChatMessageFeedback";
|
|
529
529
|
import { AddEpisodeMemberNotifyIndexes1787300000000 } from "./1787300000000-AddEpisodeMemberNotifyIndexes";
|
|
530
|
+
import { AddDeviceRoleAndDeclaredLinkParent1787400000000 } from "./1787400000000-AddDeviceRoleAndDeclaredLinkParent";
|
|
530
531
|
|
|
531
532
|
export default [
|
|
532
533
|
InitialMigration,
|
|
@@ -1058,4 +1059,5 @@ export default [
|
|
|
1058
1059
|
AddAIConversationPageContext1787100000000,
|
|
1059
1060
|
AddAIChatMessageFeedback1787200000000,
|
|
1060
1061
|
AddEpisodeMemberNotifyIndexes1787300000000,
|
|
1062
|
+
AddDeviceRoleAndDeclaredLinkParent1787400000000,
|
|
1061
1063
|
];
|
|
@@ -2,24 +2,140 @@ import DatabaseService from "./DatabaseService";
|
|
|
2
2
|
import Model from "../../Models/DatabaseModels/NetworkDeviceLink";
|
|
3
3
|
import NetworkDeviceService from "./NetworkDeviceService";
|
|
4
4
|
import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
|
|
5
|
-
import { OnCreate } from "../Types/Database/Hooks";
|
|
5
|
+
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
7
8
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
8
9
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
10
|
import ObjectID from "../../Types/ObjectID";
|
|
10
11
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
11
12
|
import RelationIdUtil from "../Utils/Database/RelationIdUtil";
|
|
12
13
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
14
|
+
import Query from "../Types/Database/Query";
|
|
15
|
+
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
13
16
|
|
|
14
17
|
// Both spellings of each end, for the same reason RelationIdUtil exists.
|
|
15
18
|
const FROM_DEVICE_KEYS: Array<string> = ["fromDeviceId", "fromDevice"];
|
|
16
19
|
const TO_DEVICE_KEYS: Array<string> = ["toDeviceId", "toDevice"];
|
|
20
|
+
const PARENT_DEVICE_KEYS: Array<string> = ["parentDeviceId", "parentDevice"];
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* A device id, compared the way Postgres compares it.
|
|
24
|
+
*
|
|
25
|
+
* These columns are `uuid`. Postgres normalises and matches them without
|
|
26
|
+
* regard to case, and ObjectID.isValidUUID accepts either case, so an API
|
|
27
|
+
* client may legitimately send `AAAAAAAA-...` for a row stored as
|
|
28
|
+
* `aaaaaaaa-...`. A raw JS !== between the two says "different device" and
|
|
29
|
+
* refuses a parent that is in fact an end of the link. Note that the
|
|
30
|
+
* device-existence check below cannot disagree: it runs in Postgres.
|
|
31
|
+
*/
|
|
32
|
+
const sameDeviceId: (a: ObjectID, b: ObjectID) => boolean = (
|
|
33
|
+
a: ObjectID,
|
|
34
|
+
b: ObjectID,
|
|
35
|
+
): boolean => {
|
|
36
|
+
return a.toString().toLowerCase() === b.toString().toLowerCase();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* A declared parent has to be one of the two devices the link connects.
|
|
41
|
+
* Anything else is not a statement about this link at all, and the map
|
|
42
|
+
* would silently ignore it — the operator would have declared a hierarchy
|
|
43
|
+
* and watched nothing change.
|
|
44
|
+
*/
|
|
45
|
+
const assertParentIsAnEnd: (
|
|
46
|
+
parentDeviceId: ObjectID | null,
|
|
47
|
+
fromDeviceId: ObjectID,
|
|
48
|
+
toDeviceId: ObjectID,
|
|
49
|
+
) => void = (
|
|
50
|
+
parentDeviceId: ObjectID | null,
|
|
51
|
+
fromDeviceId: ObjectID,
|
|
52
|
+
toDeviceId: ObjectID,
|
|
53
|
+
): void => {
|
|
54
|
+
if (!parentDeviceId) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!sameDeviceId(parentDeviceId, fromDeviceId) &&
|
|
59
|
+
!sameDeviceId(parentDeviceId, toDeviceId)
|
|
60
|
+
) {
|
|
61
|
+
throw new BadDataException(
|
|
62
|
+
"The parent device must be one of the two devices this link connects.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Which of `keys` the payload actually WRITES.
|
|
69
|
+
*
|
|
70
|
+
* Not the same question as which keys are present. TypeORM drops
|
|
71
|
+
* undefined-valued keys before it builds the SET list — "it doesn't make
|
|
72
|
+
* sense to update undefined properties" — so a key holding undefined
|
|
73
|
+
* changes nothing in the database. Treating it as a write makes this hook
|
|
74
|
+
* validate a row that is never going to exist: model the parent as
|
|
75
|
+
* cleared, accept the write, and leave the stored parent stranded on a
|
|
76
|
+
* link whose end just moved. Null and "" are different and really do
|
|
77
|
+
* clear the column, which is why the test is against undefined alone.
|
|
78
|
+
*/
|
|
79
|
+
const writtenKeys: (
|
|
80
|
+
data: Record<string, unknown>,
|
|
81
|
+
keys: Array<string>,
|
|
82
|
+
) => Array<string> = (
|
|
83
|
+
data: Record<string, unknown>,
|
|
84
|
+
keys: Array<string>,
|
|
85
|
+
): Array<string> => {
|
|
86
|
+
return keys.filter((key: string) => {
|
|
87
|
+
return key in data && data[key] !== undefined;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* An update payload is a QueryDeepPartialEntity, so a column may hold a
|
|
93
|
+
* raw SQL-expression function instead of a value. RelationIdUtil reads one
|
|
94
|
+
* as null — indistinguishable from "cleared" — and the whole invariant is
|
|
95
|
+
* then checked against a row that bears no relation to what will be
|
|
96
|
+
* written. Nothing in the tree writes this model that way today; refusing
|
|
97
|
+
* is what keeps that true, and a caller who needs it can resolve the id
|
|
98
|
+
* before calling rather than have it silently mis-validated.
|
|
99
|
+
*/
|
|
100
|
+
const assertNoSqlExpression: (
|
|
101
|
+
data: Record<string, unknown>,
|
|
102
|
+
keys: Array<string>,
|
|
103
|
+
) => void = (data: Record<string, unknown>, keys: Array<string>): void => {
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
if (typeof data[key] === "function") {
|
|
106
|
+
throw new BadDataException(
|
|
107
|
+
`${key} cannot be set to a raw SQL expression on a network device link — the link's ends and its declared parent have to be checked against each other, which needs actual ids.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
17
112
|
|
|
18
113
|
export class Service extends DatabaseService<Model> {
|
|
19
114
|
public constructor() {
|
|
20
115
|
super(Model);
|
|
21
116
|
}
|
|
22
117
|
|
|
118
|
+
/*
|
|
119
|
+
* onBeforeUpdate runs before DatabaseService permission-checks the query,
|
|
120
|
+
* so reading the raw client query as root would hand the hook rows from
|
|
121
|
+
* other projects. Re-apply the caller's tenant here. (Same helper as
|
|
122
|
+
* NetworkDeviceService and NetworkSiteService — private per service,
|
|
123
|
+
* which is the shape the codebase already settled on.)
|
|
124
|
+
*/
|
|
125
|
+
private scopeQueryToCallerTenant(
|
|
126
|
+
query: Query<Model>,
|
|
127
|
+
props: DatabaseCommonInteractionProps,
|
|
128
|
+
): Query<Model> {
|
|
129
|
+
if (props.isRoot || !props.tenantId) {
|
|
130
|
+
return query;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...query,
|
|
135
|
+
projectId: props.tenantId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
23
139
|
/*
|
|
24
140
|
* Two guards, both about links that would be nonsense on the map:
|
|
25
141
|
*
|
|
@@ -39,6 +155,12 @@ export class Service extends DatabaseService<Model> {
|
|
|
39
155
|
unknown
|
|
40
156
|
>;
|
|
41
157
|
|
|
158
|
+
assertNoSqlExpression(data, [
|
|
159
|
+
...FROM_DEVICE_KEYS,
|
|
160
|
+
...TO_DEVICE_KEYS,
|
|
161
|
+
...PARENT_DEVICE_KEYS,
|
|
162
|
+
]);
|
|
163
|
+
|
|
42
164
|
const fromDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
43
165
|
data,
|
|
44
166
|
FROM_DEVICE_KEYS,
|
|
@@ -52,7 +174,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
52
174
|
throw new BadDataException("A link needs a device at each end.");
|
|
53
175
|
}
|
|
54
176
|
|
|
55
|
-
if (fromDeviceId
|
|
177
|
+
if (sameDeviceId(fromDeviceId, toDeviceId)) {
|
|
56
178
|
throw new BadDataException("A device cannot be linked to itself.");
|
|
57
179
|
}
|
|
58
180
|
|
|
@@ -77,8 +199,222 @@ export class Service extends DatabaseService<Model> {
|
|
|
77
199
|
);
|
|
78
200
|
}
|
|
79
201
|
|
|
202
|
+
assertParentIsAnEnd(
|
|
203
|
+
RelationIdUtil.read(data, PARENT_DEVICE_KEYS),
|
|
204
|
+
fromDeviceId,
|
|
205
|
+
toDeviceId,
|
|
206
|
+
);
|
|
207
|
+
|
|
80
208
|
return { createBy, carryForward: null };
|
|
81
209
|
}
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* The same invariant, on the other write path.
|
|
213
|
+
*
|
|
214
|
+
* An update can break it two ways, and only one of them is obvious. The
|
|
215
|
+
* obvious one is setting a parent that is not on the link. The other is
|
|
216
|
+
* moving an END: re-point `toDevice` at a different switch and a parent
|
|
217
|
+
* that was valid a moment ago now names a device the link no longer
|
|
218
|
+
* touches. So the post-update state is what gets checked, assembled from
|
|
219
|
+
* the payload where it says something and from the stored row where it
|
|
220
|
+
* does not.
|
|
221
|
+
*
|
|
222
|
+
* An update matches a QUERY, not one row, and each matched row may have
|
|
223
|
+
* different ends — so every one of them is validated, and one bad row
|
|
224
|
+
* rejects the whole write. That is the conservative direction: a partial
|
|
225
|
+
* update that fixed some rows and left others contradicting themselves
|
|
226
|
+
* would be far harder to notice than a refusal.
|
|
227
|
+
*
|
|
228
|
+
* It also re-runs onBeforeCreate's TENANCY check on any end that moves.
|
|
229
|
+
* That check used to exist only on create, which left the update path
|
|
230
|
+
* able to re-point an end at another project's device — the exact threat
|
|
231
|
+
* onBeforeCreate's own comment describes, since the FK is global and the
|
|
232
|
+
* column is member-writable.
|
|
233
|
+
*
|
|
234
|
+
* Only costs a read when the payload touches an end or the parent at all;
|
|
235
|
+
* an update of ports or the bound monitor takes the early return.
|
|
236
|
+
*/
|
|
237
|
+
@CaptureSpan()
|
|
238
|
+
protected override async onBeforeUpdate(
|
|
239
|
+
updateBy: UpdateBy<Model>,
|
|
240
|
+
): Promise<OnUpdate<Model>> {
|
|
241
|
+
const data: Record<string, unknown> = (updateBy.data ||
|
|
242
|
+
{}) as unknown as Record<string, unknown>;
|
|
243
|
+
|
|
244
|
+
const hierarchyKeys: Array<string> = [
|
|
245
|
+
...FROM_DEVICE_KEYS,
|
|
246
|
+
...TO_DEVICE_KEYS,
|
|
247
|
+
...PARENT_DEVICE_KEYS,
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
if (writtenKeys(data, hierarchyKeys).length === 0) {
|
|
251
|
+
return { updateBy, carryForward: null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
assertNoSqlExpression(data, hierarchyKeys);
|
|
255
|
+
|
|
256
|
+
const writtenFromDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
257
|
+
data,
|
|
258
|
+
FROM_DEVICE_KEYS,
|
|
259
|
+
);
|
|
260
|
+
const writtenToDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
261
|
+
data,
|
|
262
|
+
TO_DEVICE_KEYS,
|
|
263
|
+
);
|
|
264
|
+
const writtenParentDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
265
|
+
data,
|
|
266
|
+
PARENT_DEVICE_KEYS,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Writing the parent key with an empty value is how a hierarchy is
|
|
271
|
+
* cleared back to a peer link. RelationIdUtil reads that as null, which
|
|
272
|
+
* is indistinguishable from "not written" — so the presence of the key
|
|
273
|
+
* (with a value that will actually be written; see writtenKeys), not
|
|
274
|
+
* the truthiness of its value, is what decides whether the stored
|
|
275
|
+
* parent still applies.
|
|
276
|
+
*/
|
|
277
|
+
const isParentWritten: boolean =
|
|
278
|
+
writtenKeys(data, PARENT_DEVICE_KEYS).length > 0;
|
|
279
|
+
|
|
280
|
+
const existingLinks: Array<Model> = await this.findBy({
|
|
281
|
+
query: this.scopeQueryToCallerTenant(updateBy.query, updateBy.props),
|
|
282
|
+
select: {
|
|
283
|
+
_id: true,
|
|
284
|
+
projectId: true,
|
|
285
|
+
fromDeviceId: true,
|
|
286
|
+
toDeviceId: true,
|
|
287
|
+
parentDeviceId: true,
|
|
288
|
+
},
|
|
289
|
+
limit: LIMIT_MAX,
|
|
290
|
+
skip: 0,
|
|
291
|
+
props: {
|
|
292
|
+
isRoot: true,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* An end that MOVED has to be re-checked for tenancy, exactly as
|
|
298
|
+
* onBeforeCreate checks it. The FK only requires the device row to
|
|
299
|
+
* exist — it is global — so without this a project member could
|
|
300
|
+
* re-point an end of their own link at another project's device and
|
|
301
|
+
* read its name straight off the map, which is the threat
|
|
302
|
+
* onBeforeCreate's own comment describes. The declared parent inherits
|
|
303
|
+
* the check for free: it has to be one of the two ends.
|
|
304
|
+
*
|
|
305
|
+
* Grouped by project because an update may in principle match links in
|
|
306
|
+
* more than one, and a device is only legitimate within its own.
|
|
307
|
+
*/
|
|
308
|
+
/*
|
|
309
|
+
* Deduped: writing the same id to both ends is a self-link, and that
|
|
310
|
+
* has its own error below — it must not surface as "device not found".
|
|
311
|
+
*/
|
|
312
|
+
const movedEndIds: Array<string> = [
|
|
313
|
+
...new Set<string>(
|
|
314
|
+
[writtenFromDeviceId, writtenToDeviceId]
|
|
315
|
+
.filter((id: ObjectID | null): id is ObjectID => {
|
|
316
|
+
return Boolean(id);
|
|
317
|
+
})
|
|
318
|
+
.map((id: ObjectID) => {
|
|
319
|
+
return id.toString();
|
|
320
|
+
}),
|
|
321
|
+
),
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
if (movedEndIds.length > 0 && existingLinks.length > 0) {
|
|
325
|
+
/*
|
|
326
|
+
* Every project this write touches: the caller's own tenant, plus
|
|
327
|
+
* the project of each row the query matched (a root caller has no
|
|
328
|
+
* tenant, and only root can match rows across more than one). The
|
|
329
|
+
* new end has to be legitimate in all of them.
|
|
330
|
+
*
|
|
331
|
+
* Guarded on there being matched rows at all: a query that matched
|
|
332
|
+
* nothing writes nothing, and there is no link for a foreign device
|
|
333
|
+
* to end up attached to.
|
|
334
|
+
*/
|
|
335
|
+
const projectIds: Set<string> = new Set<string>();
|
|
336
|
+
if (updateBy.props.tenantId) {
|
|
337
|
+
projectIds.add(updateBy.props.tenantId.toString());
|
|
338
|
+
}
|
|
339
|
+
for (const link of existingLinks) {
|
|
340
|
+
if (link.projectId) {
|
|
341
|
+
projectIds.add(link.projectId.toString());
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* Fail closed. No project to check against and an end on the move
|
|
347
|
+
* is not a safe update to wave through — it is the one shape where
|
|
348
|
+
* a device from anywhere could be attached to this link.
|
|
349
|
+
*/
|
|
350
|
+
if (projectIds.size === 0) {
|
|
351
|
+
throw new BadDataException(
|
|
352
|
+
"Both devices must exist and belong to this project.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const projectId of projectIds) {
|
|
357
|
+
const devices: Array<NetworkDevice> = await NetworkDeviceService.findBy(
|
|
358
|
+
{
|
|
359
|
+
query: {
|
|
360
|
+
_id: QueryHelper.any(movedEndIds),
|
|
361
|
+
projectId: new ObjectID(projectId),
|
|
362
|
+
},
|
|
363
|
+
select: { _id: true },
|
|
364
|
+
limit: LIMIT_MAX,
|
|
365
|
+
skip: 0,
|
|
366
|
+
props: { isRoot: true },
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* Compared as a SET of ids rather than as a count. A count says
|
|
372
|
+
* "as many rows came back as I asked about", which a duplicate row
|
|
373
|
+
* satisfies just as well as the right rows do — and this is the
|
|
374
|
+
* check standing between one project's map and another project's
|
|
375
|
+
* device names, so it should be answering the actual question.
|
|
376
|
+
*/
|
|
377
|
+
const foundIds: Set<string> = new Set<string>(
|
|
378
|
+
devices.map((device: NetworkDevice) => {
|
|
379
|
+
return (device.id || device._id || "").toString().toLowerCase();
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const isMissingAnEnd: boolean = movedEndIds.some((id: string) => {
|
|
384
|
+
return !foundIds.has(id.toLowerCase());
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (isMissingAnEnd) {
|
|
388
|
+
throw new BadDataException(
|
|
389
|
+
"Both devices must exist and belong to this project.",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const link of existingLinks) {
|
|
396
|
+
const fromDeviceId: ObjectID | undefined =
|
|
397
|
+
writtenFromDeviceId || link.fromDeviceId;
|
|
398
|
+
const toDeviceId: ObjectID | undefined =
|
|
399
|
+
writtenToDeviceId || link.toDeviceId;
|
|
400
|
+
const parentDeviceId: ObjectID | null = isParentWritten
|
|
401
|
+
? writtenParentDeviceId
|
|
402
|
+
: link.parentDeviceId || null;
|
|
403
|
+
|
|
404
|
+
// A row mid-migration with a missing end has no invariant to check.
|
|
405
|
+
if (!fromDeviceId || !toDeviceId) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (sameDeviceId(fromDeviceId, toDeviceId)) {
|
|
410
|
+
throw new BadDataException("A device cannot be linked to itself.");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
assertParentIsAnEnd(parentDeviceId, fromDeviceId, toDeviceId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { updateBy, carryForward: null };
|
|
417
|
+
}
|
|
82
418
|
}
|
|
83
419
|
|
|
84
420
|
export default new Service();
|
|
@@ -4,6 +4,19 @@ import ServerMeteredPlan from "./ServerMeteredPlan";
|
|
|
4
4
|
import TelemetryMeteredPlanType from "./TelemetryMeteredPlan";
|
|
5
5
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
6
6
|
import ProductType from "../../../../Types/MeteredPlan/ProductType";
|
|
7
|
+
import {
|
|
8
|
+
SESSION_REPLAY_PRICE_IN_USD_PER_GB,
|
|
9
|
+
TELEMETRY_PRICE_IN_USD_PER_GB,
|
|
10
|
+
TELEMETRY_PRICE_RETENTION_IN_DAYS,
|
|
11
|
+
} from "../../../../Types/Billing/PayAsYouGoPricing";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Per GB per day, derived from the advertised per-GB-per-15-days rate so the
|
|
15
|
+
* price the dashboard quotes a Free plan user and the price we actually meter
|
|
16
|
+
* cannot drift apart.
|
|
17
|
+
*/
|
|
18
|
+
const TELEMETRY_UNIT_COST_IN_USD: number =
|
|
19
|
+
TELEMETRY_PRICE_IN_USD_PER_GB / TELEMETRY_PRICE_RETENTION_IN_DAYS;
|
|
7
20
|
|
|
8
21
|
export const ActiveMonitoringMeteredPlan: ActiveMonitoringMeteredPlanType =
|
|
9
22
|
new ActiveMonitoringMeteredPlanType();
|
|
@@ -11,25 +24,25 @@ export const ActiveMonitoringMeteredPlan: ActiveMonitoringMeteredPlanType =
|
|
|
11
24
|
export const LogDataIngestMeteredPlan: TelemetryMeteredPlanType =
|
|
12
25
|
new TelemetryMeteredPlanType({
|
|
13
26
|
productType: ProductType.Logs,
|
|
14
|
-
unitCostInUSD:
|
|
27
|
+
unitCostInUSD: TELEMETRY_UNIT_COST_IN_USD, // 0.10 per 15 days per GB
|
|
15
28
|
});
|
|
16
29
|
|
|
17
30
|
export const MetricsDataIngestMeteredPlan: TelemetryMeteredPlanType =
|
|
18
31
|
new TelemetryMeteredPlanType({
|
|
19
32
|
productType: ProductType.Metrics,
|
|
20
|
-
unitCostInUSD:
|
|
33
|
+
unitCostInUSD: TELEMETRY_UNIT_COST_IN_USD, // 0.10 per 15 days per GB
|
|
21
34
|
});
|
|
22
35
|
|
|
23
36
|
export const TracesDataIngestMetredPlan: TelemetryMeteredPlanType =
|
|
24
37
|
new TelemetryMeteredPlanType({
|
|
25
38
|
productType: ProductType.Traces,
|
|
26
|
-
unitCostInUSD:
|
|
39
|
+
unitCostInUSD: TELEMETRY_UNIT_COST_IN_USD, // 0.10 per 15 days per GB
|
|
27
40
|
});
|
|
28
41
|
|
|
29
42
|
export const ProfilesDataIngestMeteredPlan: TelemetryMeteredPlanType =
|
|
30
43
|
new TelemetryMeteredPlanType({
|
|
31
44
|
productType: ProductType.Profiles,
|
|
32
|
-
unitCostInUSD:
|
|
45
|
+
unitCostInUSD: TELEMETRY_UNIT_COST_IN_USD, // 0.10 per 15 days per GB
|
|
33
46
|
});
|
|
34
47
|
|
|
35
48
|
/*
|
|
@@ -43,7 +56,8 @@ export const ProfilesDataIngestMeteredPlan: TelemetryMeteredPlanType =
|
|
|
43
56
|
export const SessionReplayDataIngestMeteredPlan: TelemetryMeteredPlanType =
|
|
44
57
|
new TelemetryMeteredPlanType({
|
|
45
58
|
productType: ProductType.SessionReplay,
|
|
46
|
-
unitCostInUSD:
|
|
59
|
+
unitCostInUSD:
|
|
60
|
+
SESSION_REPLAY_PRICE_IN_USD_PER_GB / TELEMETRY_PRICE_RETENTION_IN_DAYS, // 2.00 per 15 days per GB
|
|
47
61
|
});
|
|
48
62
|
|
|
49
63
|
const AllMeteredPlans: Array<ServerMeteredPlan> = [
|