@oneuptime/common 11.3.0 → 11.3.1

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.
@@ -62,6 +62,7 @@ import IncidentState from "../../Models/DatabaseModels/IncidentState";
62
62
  import IncidentRole from "../../Models/DatabaseModels/IncidentRole";
63
63
  import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
64
64
  import Model from "../../Models/DatabaseModels/Project";
65
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
65
66
  import PromoCode from "../../Models/DatabaseModels/PromoCode";
66
67
  import ScheduledMaintenanceState from "../../Models/DatabaseModels/ScheduledMaintenanceState";
67
68
  import Team from "../../Models/DatabaseModels/Team";
@@ -638,80 +639,132 @@ export class ProjectService extends DatabaseService<Model> {
638
639
  }
639
640
  }
640
641
 
642
+ /**
643
+ * Names of every row of `service` already scoped to `projectId`.
644
+ *
645
+ * The addDefault* seeders below each create a fixed set of named rows on a
646
+ * column that is unique per project (@UniqueColumnBy("projectId") on `name`).
647
+ * The SAME defaults are created in two places: here, in the project-create
648
+ * hooks, AND by backfill data migrations (e.g.
649
+ * AddDefaultAlertSeverityAndStateToExistingProjects). Creating a name that
650
+ * already exists throws "<X> with the same name already exists" in
651
+ * DatabaseService.checkUniqueColumnBy — and because the DataMigration runner
652
+ * halts the whole chain at the first failure, that single throw freezes every
653
+ * later migration. Each seeder guards its creates against this set so both
654
+ * paths are idempotent / safe to re-run.
655
+ */
656
+ private async getExistingProjectScopedNames<TBaseModel extends BaseModel>(
657
+ service: DatabaseService<TBaseModel>,
658
+ projectId: ObjectID,
659
+ ): Promise<Set<string | undefined>> {
660
+ const items: Array<TBaseModel> = await service.findBy({
661
+ query: {
662
+ projectId: projectId,
663
+ } as Query<TBaseModel>,
664
+ select: {
665
+ name: true,
666
+ } as Select<TBaseModel>,
667
+ skip: 0,
668
+ limit: LIMIT_MAX,
669
+ props: {
670
+ isRoot: true,
671
+ },
672
+ });
673
+
674
+ return new Set(
675
+ items.map((item: TBaseModel): string | undefined => {
676
+ return (item as { name?: string | undefined }).name;
677
+ }),
678
+ );
679
+ }
680
+
641
681
  private async addDefaultScheduledMaintenanceState(
642
682
  createdItem: Model,
643
683
  ): Promise<Model> {
644
- let createdScheduledMaintenanceState: ScheduledMaintenanceState =
645
- new ScheduledMaintenanceState();
646
- createdScheduledMaintenanceState.name = "Scheduled";
647
- createdScheduledMaintenanceState.description =
648
- "When an event is scheduled, it belongs to this state";
649
- createdScheduledMaintenanceState.color = Black;
650
- createdScheduledMaintenanceState.isScheduledState = true;
651
- createdScheduledMaintenanceState.projectId = createdItem.id!;
652
- createdScheduledMaintenanceState.order = 1;
653
-
654
- createdScheduledMaintenanceState =
684
+ const projectId: ObjectID = createdItem.id!;
685
+
686
+ // Idempotent — see getExistingProjectScopedNames.
687
+ const existingNames: Set<string | undefined> =
688
+ await this.getExistingProjectScopedNames(
689
+ ScheduledMaintenanceStateService,
690
+ projectId,
691
+ );
692
+
693
+ if (!existingNames.has("Scheduled")) {
694
+ const createdScheduledMaintenanceState: ScheduledMaintenanceState =
695
+ new ScheduledMaintenanceState();
696
+ createdScheduledMaintenanceState.name = "Scheduled";
697
+ createdScheduledMaintenanceState.description =
698
+ "When an event is scheduled, it belongs to this state";
699
+ createdScheduledMaintenanceState.color = Black;
700
+ createdScheduledMaintenanceState.isScheduledState = true;
701
+ createdScheduledMaintenanceState.projectId = projectId;
702
+ createdScheduledMaintenanceState.order = 1;
703
+
655
704
  await ScheduledMaintenanceStateService.create({
656
705
  data: createdScheduledMaintenanceState,
657
706
  props: {
658
707
  isRoot: true,
659
708
  },
660
709
  });
710
+ }
711
+
712
+ if (!existingNames.has("Ongoing")) {
713
+ const ongoingScheduledMaintenanceState: ScheduledMaintenanceState =
714
+ new ScheduledMaintenanceState();
715
+ ongoingScheduledMaintenanceState.name = "Ongoing";
716
+ ongoingScheduledMaintenanceState.description =
717
+ "When an event is ongoing, it belongs to this state.";
718
+ ongoingScheduledMaintenanceState.color = Yellow;
719
+ ongoingScheduledMaintenanceState.isOngoingState = true;
720
+ ongoingScheduledMaintenanceState.projectId = projectId;
721
+ ongoingScheduledMaintenanceState.order = 2;
661
722
 
662
- let ongoingScheduledMaintenanceState: ScheduledMaintenanceState =
663
- new ScheduledMaintenanceState();
664
- ongoingScheduledMaintenanceState.name = "Ongoing";
665
- ongoingScheduledMaintenanceState.description =
666
- "When an event is ongoing, it belongs to this state.";
667
- ongoingScheduledMaintenanceState.color = Yellow;
668
- ongoingScheduledMaintenanceState.isOngoingState = true;
669
- ongoingScheduledMaintenanceState.projectId = createdItem.id!;
670
- ongoingScheduledMaintenanceState.order = 2;
671
-
672
- ongoingScheduledMaintenanceState =
673
723
  await ScheduledMaintenanceStateService.create({
674
724
  data: ongoingScheduledMaintenanceState,
675
725
  props: {
676
726
  isRoot: true,
677
727
  },
678
728
  });
729
+ }
730
+
731
+ if (!existingNames.has("Ended")) {
732
+ const endedScheduledMaintenanceState: ScheduledMaintenanceState =
733
+ new ScheduledMaintenanceState();
734
+ endedScheduledMaintenanceState.name = "Ended";
735
+ endedScheduledMaintenanceState.description =
736
+ "Scheduled maintenance events switch to this state when they end.";
737
+ endedScheduledMaintenanceState.color = new Color("#4A4A4A");
738
+ endedScheduledMaintenanceState.isEndedState = true;
739
+ endedScheduledMaintenanceState.projectId = projectId;
740
+ endedScheduledMaintenanceState.order = 3;
679
741
 
680
- let endedScheduledMaintenanceState: ScheduledMaintenanceState =
681
- new ScheduledMaintenanceState();
682
- endedScheduledMaintenanceState.name = "Ended";
683
- endedScheduledMaintenanceState.description =
684
- "Scheduled maintenance events switch to this state when they end.";
685
- endedScheduledMaintenanceState.color = new Color("#4A4A4A");
686
- endedScheduledMaintenanceState.isEndedState = true;
687
- endedScheduledMaintenanceState.projectId = createdItem.id!;
688
- endedScheduledMaintenanceState.order = 3;
689
-
690
- endedScheduledMaintenanceState =
691
742
  await ScheduledMaintenanceStateService.create({
692
743
  data: endedScheduledMaintenanceState,
693
744
  props: {
694
745
  isRoot: true,
695
746
  },
696
747
  });
748
+ }
749
+
750
+ if (!existingNames.has("Completed")) {
751
+ const completedScheduledMaintenanceState: ScheduledMaintenanceState =
752
+ new ScheduledMaintenanceState();
753
+ completedScheduledMaintenanceState.name = "Completed";
754
+ completedScheduledMaintenanceState.description =
755
+ "When an event is completed, it belongs to this state.";
756
+ completedScheduledMaintenanceState.color = Green;
757
+ completedScheduledMaintenanceState.isResolvedState = true;
758
+ completedScheduledMaintenanceState.projectId = projectId;
759
+ completedScheduledMaintenanceState.order = 4;
697
760
 
698
- let completedScheduledMaintenanceState: ScheduledMaintenanceState =
699
- new ScheduledMaintenanceState();
700
- completedScheduledMaintenanceState.name = "Completed";
701
- completedScheduledMaintenanceState.description =
702
- "When an event is completed, it belongs to this state.";
703
- completedScheduledMaintenanceState.color = Green;
704
- completedScheduledMaintenanceState.isResolvedState = true;
705
- completedScheduledMaintenanceState.projectId = createdItem.id!;
706
- completedScheduledMaintenanceState.order = 4;
707
-
708
- completedScheduledMaintenanceState =
709
761
  await ScheduledMaintenanceStateService.create({
710
762
  data: completedScheduledMaintenanceState,
711
763
  props: {
712
764
  isRoot: true,
713
765
  },
714
766
  });
767
+ }
715
768
 
716
769
  return createdItem;
717
770
  }
@@ -858,304 +911,380 @@ export class ProjectService extends DatabaseService<Model> {
858
911
  }
859
912
 
860
913
  private async addDefaultIncidentState(createdItem: Model): Promise<Model> {
861
- let createdIncidentState: IncidentState = new IncidentState();
862
- createdIncidentState.name = "Identified";
863
- createdIncidentState.description =
864
- "When an incident is created, it belongs to this state";
865
- createdIncidentState.color = Red;
866
- createdIncidentState.isCreatedState = true;
867
- createdIncidentState.projectId = createdItem.id!;
868
- createdIncidentState.order = 1;
869
-
870
- createdIncidentState = await IncidentStateService.create({
871
- data: createdIncidentState,
872
- props: {
873
- isRoot: true,
874
- },
875
- });
914
+ const projectId: ObjectID = createdItem.id!;
915
+
916
+ // Idempotent — see getExistingProjectScopedNames.
917
+ const existingNames: Set<string | undefined> =
918
+ await this.getExistingProjectScopedNames(IncidentStateService, projectId);
919
+
920
+ if (!existingNames.has("Identified")) {
921
+ const createdIncidentState: IncidentState = new IncidentState();
922
+ createdIncidentState.name = "Identified";
923
+ createdIncidentState.description =
924
+ "When an incident is created, it belongs to this state";
925
+ createdIncidentState.color = Red;
926
+ createdIncidentState.isCreatedState = true;
927
+ createdIncidentState.projectId = projectId;
928
+ createdIncidentState.order = 1;
929
+
930
+ await IncidentStateService.create({
931
+ data: createdIncidentState,
932
+ props: {
933
+ isRoot: true,
934
+ },
935
+ });
936
+ }
876
937
 
877
- let acknowledgedIncidentState: IncidentState = new IncidentState();
878
- acknowledgedIncidentState.name = "Acknowledged";
879
- acknowledgedIncidentState.description =
880
- "When an incident is acknowledged, it belongs to this state.";
881
- acknowledgedIncidentState.color = Yellow;
882
- acknowledgedIncidentState.isAcknowledgedState = true;
883
- acknowledgedIncidentState.projectId = createdItem.id!;
884
- acknowledgedIncidentState.order = 2;
885
-
886
- acknowledgedIncidentState = await IncidentStateService.create({
887
- data: acknowledgedIncidentState,
888
- props: {
889
- isRoot: true,
890
- },
891
- });
938
+ if (!existingNames.has("Acknowledged")) {
939
+ const acknowledgedIncidentState: IncidentState = new IncidentState();
940
+ acknowledgedIncidentState.name = "Acknowledged";
941
+ acknowledgedIncidentState.description =
942
+ "When an incident is acknowledged, it belongs to this state.";
943
+ acknowledgedIncidentState.color = Yellow;
944
+ acknowledgedIncidentState.isAcknowledgedState = true;
945
+ acknowledgedIncidentState.projectId = projectId;
946
+ acknowledgedIncidentState.order = 2;
947
+
948
+ await IncidentStateService.create({
949
+ data: acknowledgedIncidentState,
950
+ props: {
951
+ isRoot: true,
952
+ },
953
+ });
954
+ }
892
955
 
893
- let resolvedIncidentState: IncidentState = new IncidentState();
894
- resolvedIncidentState.name = "Resolved";
895
- resolvedIncidentState.description =
896
- "When an incident is resolved, it belongs to this state.";
897
- resolvedIncidentState.color = Green;
898
- resolvedIncidentState.isResolvedState = true;
899
- resolvedIncidentState.projectId = createdItem.id!;
900
- resolvedIncidentState.order = 3;
901
-
902
- resolvedIncidentState = await IncidentStateService.create({
903
- data: resolvedIncidentState,
904
- props: {
905
- isRoot: true,
906
- },
907
- });
956
+ if (!existingNames.has("Resolved")) {
957
+ const resolvedIncidentState: IncidentState = new IncidentState();
958
+ resolvedIncidentState.name = "Resolved";
959
+ resolvedIncidentState.description =
960
+ "When an incident is resolved, it belongs to this state.";
961
+ resolvedIncidentState.color = Green;
962
+ resolvedIncidentState.isResolvedState = true;
963
+ resolvedIncidentState.projectId = projectId;
964
+ resolvedIncidentState.order = 3;
965
+
966
+ await IncidentStateService.create({
967
+ data: resolvedIncidentState,
968
+ props: {
969
+ isRoot: true,
970
+ },
971
+ });
972
+ }
908
973
 
909
974
  return createdItem;
910
975
  }
911
976
 
912
977
  @CaptureSpan()
913
978
  public async addDefaultAlertState(createdItem: Model): Promise<Model> {
914
- let createdAlertState: AlertState = new AlertState();
915
- createdAlertState.name = "Identified";
916
- createdAlertState.description =
917
- "When an alert is created, it belongs to this state";
918
- createdAlertState.color = Red;
919
- createdAlertState.isCreatedState = true;
920
- createdAlertState.projectId = createdItem.id!;
921
- createdAlertState.order = 1;
922
-
923
- createdAlertState = await AlertStateService.create({
924
- data: createdAlertState,
925
- props: {
926
- isRoot: true,
927
- },
928
- });
979
+ const projectId: ObjectID = createdItem.id!;
980
+
981
+ // Idempotent — see getExistingProjectScopedNames.
982
+ const existingNames: Set<string | undefined> =
983
+ await this.getExistingProjectScopedNames(AlertStateService, projectId);
984
+
985
+ if (!existingNames.has("Identified")) {
986
+ const createdAlertState: AlertState = new AlertState();
987
+ createdAlertState.name = "Identified";
988
+ createdAlertState.description =
989
+ "When an alert is created, it belongs to this state";
990
+ createdAlertState.color = Red;
991
+ createdAlertState.isCreatedState = true;
992
+ createdAlertState.projectId = projectId;
993
+ createdAlertState.order = 1;
994
+
995
+ await AlertStateService.create({
996
+ data: createdAlertState,
997
+ props: {
998
+ isRoot: true,
999
+ },
1000
+ });
1001
+ }
929
1002
 
930
- let acknowledgedAlertState: AlertState = new AlertState();
931
- acknowledgedAlertState.name = "Acknowledged";
932
- acknowledgedAlertState.description =
933
- "When an alert is acknowledged, it belongs to this state.";
934
- acknowledgedAlertState.color = Yellow;
935
- acknowledgedAlertState.isAcknowledgedState = true;
936
- acknowledgedAlertState.projectId = createdItem.id!;
937
- acknowledgedAlertState.order = 2;
938
-
939
- acknowledgedAlertState = await AlertStateService.create({
940
- data: acknowledgedAlertState,
941
- props: {
942
- isRoot: true,
943
- },
944
- });
1003
+ if (!existingNames.has("Acknowledged")) {
1004
+ const acknowledgedAlertState: AlertState = new AlertState();
1005
+ acknowledgedAlertState.name = "Acknowledged";
1006
+ acknowledgedAlertState.description =
1007
+ "When an alert is acknowledged, it belongs to this state.";
1008
+ acknowledgedAlertState.color = Yellow;
1009
+ acknowledgedAlertState.isAcknowledgedState = true;
1010
+ acknowledgedAlertState.projectId = projectId;
1011
+ acknowledgedAlertState.order = 2;
1012
+
1013
+ await AlertStateService.create({
1014
+ data: acknowledgedAlertState,
1015
+ props: {
1016
+ isRoot: true,
1017
+ },
1018
+ });
1019
+ }
945
1020
 
946
- let resolvedAlertState: AlertState = new AlertState();
947
- resolvedAlertState.name = "Resolved";
948
- resolvedAlertState.description =
949
- "When an incident is resolved, it belongs to this state.";
950
- resolvedAlertState.color = Green;
951
- resolvedAlertState.isResolvedState = true;
952
- resolvedAlertState.projectId = createdItem.id!;
953
- resolvedAlertState.order = 3;
954
-
955
- resolvedAlertState = await AlertStateService.create({
956
- data: resolvedAlertState,
957
- props: {
958
- isRoot: true,
959
- },
960
- });
1021
+ if (!existingNames.has("Resolved")) {
1022
+ const resolvedAlertState: AlertState = new AlertState();
1023
+ resolvedAlertState.name = "Resolved";
1024
+ resolvedAlertState.description =
1025
+ "When an incident is resolved, it belongs to this state.";
1026
+ resolvedAlertState.color = Green;
1027
+ resolvedAlertState.isResolvedState = true;
1028
+ resolvedAlertState.projectId = projectId;
1029
+ resolvedAlertState.order = 3;
1030
+
1031
+ await AlertStateService.create({
1032
+ data: resolvedAlertState,
1033
+ props: {
1034
+ isRoot: true,
1035
+ },
1036
+ });
1037
+ }
961
1038
 
962
1039
  return createdItem;
963
1040
  }
964
1041
 
965
1042
  @CaptureSpan()
966
1043
  public async addDefaultAlertSeverity(createdItem: Model): Promise<Model> {
967
- let highSeverity: AlertSeverity = new AlertSeverity();
968
- highSeverity.name = "High";
969
- highSeverity.description =
970
- "Issues causing very high impact to customers. Immediate attention is required.";
971
- highSeverity.color = Moroon500;
972
- highSeverity.projectId = createdItem.id!;
973
- highSeverity.order = 1;
974
-
975
- highSeverity = await AlertSeverityService.create({
976
- data: highSeverity,
977
- props: {
978
- isRoot: true,
979
- },
980
- });
1044
+ const projectId: ObjectID = createdItem.id!;
1045
+
1046
+ // Idempotent — see getExistingProjectScopedNames.
1047
+ const existingNames: Set<string | undefined> =
1048
+ await this.getExistingProjectScopedNames(AlertSeverityService, projectId);
1049
+
1050
+ if (!existingNames.has("High")) {
1051
+ const highSeverity: AlertSeverity = new AlertSeverity();
1052
+ highSeverity.name = "High";
1053
+ highSeverity.description =
1054
+ "Issues causing very high impact to customers. Immediate attention is required.";
1055
+ highSeverity.color = Moroon500;
1056
+ highSeverity.projectId = projectId;
1057
+ highSeverity.order = 1;
1058
+
1059
+ await AlertSeverityService.create({
1060
+ data: highSeverity,
1061
+ props: {
1062
+ isRoot: true,
1063
+ },
1064
+ });
1065
+ }
981
1066
 
982
- let lowSeverity: AlertSeverity = new AlertSeverity();
983
- lowSeverity.name = "Low";
984
- lowSeverity.description = "Issues causing low impact to customers.";
985
- lowSeverity.color = Yellow500;
986
- lowSeverity.projectId = createdItem.id!;
987
- lowSeverity.order = 2;
1067
+ if (!existingNames.has("Low")) {
1068
+ const lowSeverity: AlertSeverity = new AlertSeverity();
1069
+ lowSeverity.name = "Low";
1070
+ lowSeverity.description = "Issues causing low impact to customers.";
1071
+ lowSeverity.color = Yellow500;
1072
+ lowSeverity.projectId = projectId;
1073
+ lowSeverity.order = 2;
988
1074
 
989
- lowSeverity = await AlertSeverityService.create({
990
- data: lowSeverity,
991
- props: {
992
- isRoot: true,
993
- },
994
- });
1075
+ await AlertSeverityService.create({
1076
+ data: lowSeverity,
1077
+ props: {
1078
+ isRoot: true,
1079
+ },
1080
+ });
1081
+ }
995
1082
 
996
1083
  return createdItem;
997
1084
  }
998
1085
 
999
1086
  private async addDefaultIncidentSeverity(createdItem: Model): Promise<Model> {
1000
- let criticalIncident: IncidentSeverity = new IncidentSeverity();
1001
- criticalIncident.name = "Critical Incident";
1002
- criticalIncident.description =
1003
- "Issues causing very high impact to customers. Immediate response is required. Examples include a full outage, or a data breach.";
1004
- criticalIncident.color = Moroon500;
1005
- criticalIncident.projectId = createdItem.id!;
1006
- criticalIncident.order = 1;
1007
-
1008
- criticalIncident = await IncidentSeverityService.create({
1009
- data: criticalIncident,
1010
- props: {
1011
- isRoot: true,
1012
- },
1013
- });
1087
+ const projectId: ObjectID = createdItem.id!;
1014
1088
 
1015
- let majorIncident: IncidentSeverity = new IncidentSeverity();
1016
- majorIncident.name = "Major Incident";
1017
- majorIncident.description =
1018
- "Issues causing significant impact. Immediate response is usually required. We might have some workarounds that mitigate the impact on customers. Examples include an important sub-system failing.";
1019
- majorIncident.color = Red;
1020
- majorIncident.projectId = createdItem.id!;
1021
- majorIncident.order = 2;
1089
+ // Idempotent see getExistingProjectScopedNames.
1090
+ const existingNames: Set<string | undefined> =
1091
+ await this.getExistingProjectScopedNames(
1092
+ IncidentSeverityService,
1093
+ projectId,
1094
+ );
1022
1095
 
1023
- majorIncident = await IncidentSeverityService.create({
1024
- data: majorIncident,
1025
- props: {
1026
- isRoot: true,
1027
- },
1028
- });
1096
+ if (!existingNames.has("Critical Incident")) {
1097
+ const criticalIncident: IncidentSeverity = new IncidentSeverity();
1098
+ criticalIncident.name = "Critical Incident";
1099
+ criticalIncident.description =
1100
+ "Issues causing very high impact to customers. Immediate response is required. Examples include a full outage, or a data breach.";
1101
+ criticalIncident.color = Moroon500;
1102
+ criticalIncident.projectId = projectId;
1103
+ criticalIncident.order = 1;
1104
+
1105
+ await IncidentSeverityService.create({
1106
+ data: criticalIncident,
1107
+ props: {
1108
+ isRoot: true,
1109
+ },
1110
+ });
1111
+ }
1029
1112
 
1030
- let minorIncident: IncidentSeverity = new IncidentSeverity();
1031
- minorIncident.name = "Minor Incident";
1032
- minorIncident.description =
1033
- "Issues with low impact, which can usually be handled within working hours. Most customers are unlikely to notice any problems. Examples include a slight drop in application performance.";
1034
- minorIncident.color = Yellow;
1035
- minorIncident.projectId = createdItem.id!;
1036
- minorIncident.order = 3;
1113
+ if (!existingNames.has("Major Incident")) {
1114
+ const majorIncident: IncidentSeverity = new IncidentSeverity();
1115
+ majorIncident.name = "Major Incident";
1116
+ majorIncident.description =
1117
+ "Issues causing significant impact. Immediate response is usually required. We might have some workarounds that mitigate the impact on customers. Examples include an important sub-system failing.";
1118
+ majorIncident.color = Red;
1119
+ majorIncident.projectId = projectId;
1120
+ majorIncident.order = 2;
1121
+
1122
+ await IncidentSeverityService.create({
1123
+ data: majorIncident,
1124
+ props: {
1125
+ isRoot: true,
1126
+ },
1127
+ });
1128
+ }
1037
1129
 
1038
- minorIncident = await IncidentSeverityService.create({
1039
- data: minorIncident,
1040
- props: {
1041
- isRoot: true,
1042
- },
1043
- });
1130
+ if (!existingNames.has("Minor Incident")) {
1131
+ const minorIncident: IncidentSeverity = new IncidentSeverity();
1132
+ minorIncident.name = "Minor Incident";
1133
+ minorIncident.description =
1134
+ "Issues with low impact, which can usually be handled within working hours. Most customers are unlikely to notice any problems. Examples include a slight drop in application performance.";
1135
+ minorIncident.color = Yellow;
1136
+ minorIncident.projectId = projectId;
1137
+ minorIncident.order = 3;
1138
+
1139
+ await IncidentSeverityService.create({
1140
+ data: minorIncident,
1141
+ props: {
1142
+ isRoot: true,
1143
+ },
1144
+ });
1145
+ }
1044
1146
 
1045
1147
  return createdItem;
1046
1148
  }
1047
1149
 
1048
1150
  public async addDefaultIncidentRoles(createdItem: Model): Promise<Model> {
1049
- let incidentCommander: IncidentRole = new IncidentRole();
1050
- incidentCommander.name = "Incident Commander";
1051
- incidentCommander.description =
1052
- "Primary decision maker during an incident. Responsible for coordinating the response and making final decisions.";
1053
- incidentCommander.color = Purple500;
1054
- incidentCommander.roleIcon = IconProp.ShieldCheck;
1055
- incidentCommander.projectId = createdItem.id!;
1056
- incidentCommander.isPrimaryRole = true;
1057
- incidentCommander.isDeleteable = false;
1058
-
1059
- incidentCommander = await IncidentRoleService.create({
1060
- data: incidentCommander,
1061
- props: {
1062
- isRoot: true,
1063
- },
1064
- });
1065
-
1066
- let responder: IncidentRole = new IncidentRole();
1067
- responder.name = "Responder";
1068
- responder.description =
1069
- "Active participant in incident resolution. Performs hands-on work to resolve the incident.";
1070
- responder.color = Blue500;
1071
- responder.roleIcon = IconProp.Wrench;
1072
- responder.projectId = createdItem.id!;
1073
-
1074
- responder = await IncidentRoleService.create({
1075
- data: responder,
1076
- props: {
1077
- isRoot: true,
1078
- },
1079
- });
1151
+ const projectId: ObjectID = createdItem.id!;
1152
+
1153
+ // Idempotent — see getExistingProjectScopedNames.
1154
+ const existingNames: Set<string | undefined> =
1155
+ await this.getExistingProjectScopedNames(IncidentRoleService, projectId);
1156
+
1157
+ if (!existingNames.has("Incident Commander")) {
1158
+ const incidentCommander: IncidentRole = new IncidentRole();
1159
+ incidentCommander.name = "Incident Commander";
1160
+ incidentCommander.description =
1161
+ "Primary decision maker during an incident. Responsible for coordinating the response and making final decisions.";
1162
+ incidentCommander.color = Purple500;
1163
+ incidentCommander.roleIcon = IconProp.ShieldCheck;
1164
+ incidentCommander.projectId = projectId;
1165
+ incidentCommander.isPrimaryRole = true;
1166
+ incidentCommander.isDeleteable = false;
1167
+
1168
+ await IncidentRoleService.create({
1169
+ data: incidentCommander,
1170
+ props: {
1171
+ isRoot: true,
1172
+ },
1173
+ });
1174
+ }
1080
1175
 
1081
- let communicationsLead: IncidentRole = new IncidentRole();
1082
- communicationsLead.name = "Communications Lead";
1083
- communicationsLead.description =
1084
- "Handles stakeholder communication and status updates during an incident.";
1085
- communicationsLead.color = Teal500;
1086
- communicationsLead.roleIcon = IconProp.Announcement;
1087
- communicationsLead.projectId = createdItem.id!;
1176
+ if (!existingNames.has("Responder")) {
1177
+ const responder: IncidentRole = new IncidentRole();
1178
+ responder.name = "Responder";
1179
+ responder.description =
1180
+ "Active participant in incident resolution. Performs hands-on work to resolve the incident.";
1181
+ responder.color = Blue500;
1182
+ responder.roleIcon = IconProp.Wrench;
1183
+ responder.projectId = projectId;
1184
+
1185
+ await IncidentRoleService.create({
1186
+ data: responder,
1187
+ props: {
1188
+ isRoot: true,
1189
+ },
1190
+ });
1191
+ }
1088
1192
 
1089
- communicationsLead = await IncidentRoleService.create({
1090
- data: communicationsLead,
1091
- props: {
1092
- isRoot: true,
1093
- },
1094
- });
1193
+ if (!existingNames.has("Communications Lead")) {
1194
+ const communicationsLead: IncidentRole = new IncidentRole();
1195
+ communicationsLead.name = "Communications Lead";
1196
+ communicationsLead.description =
1197
+ "Handles stakeholder communication and status updates during an incident.";
1198
+ communicationsLead.color = Teal500;
1199
+ communicationsLead.roleIcon = IconProp.Announcement;
1200
+ communicationsLead.projectId = projectId;
1201
+
1202
+ await IncidentRoleService.create({
1203
+ data: communicationsLead,
1204
+ props: {
1205
+ isRoot: true,
1206
+ },
1207
+ });
1208
+ }
1095
1209
 
1096
- let observer: IncidentRole = new IncidentRole();
1097
- observer.name = "Observer";
1098
- observer.description =
1099
- "Read-only participant who monitors the incident without active involvement.";
1100
- observer.color = Gray500;
1101
- observer.roleIcon = IconProp.Activity;
1102
- observer.projectId = createdItem.id!;
1103
- observer.canAssignMultipleUsers = true;
1104
-
1105
- observer = await IncidentRoleService.create({
1106
- data: observer,
1107
- props: {
1108
- isRoot: true,
1109
- },
1110
- });
1210
+ if (!existingNames.has("Observer")) {
1211
+ const observer: IncidentRole = new IncidentRole();
1212
+ observer.name = "Observer";
1213
+ observer.description =
1214
+ "Read-only participant who monitors the incident without active involvement.";
1215
+ observer.color = Gray500;
1216
+ observer.roleIcon = IconProp.Activity;
1217
+ observer.projectId = projectId;
1218
+ observer.canAssignMultipleUsers = true;
1219
+
1220
+ await IncidentRoleService.create({
1221
+ data: observer,
1222
+ props: {
1223
+ isRoot: true,
1224
+ },
1225
+ });
1226
+ }
1111
1227
 
1112
1228
  return createdItem;
1113
1229
  }
1114
1230
 
1115
1231
  private async addDefaultMonitorStatus(createdItem: Model): Promise<Model> {
1116
- let operationalStatus: MonitorStatus = new MonitorStatus();
1117
- operationalStatus.name = "Operational";
1118
- operationalStatus.description = "Monitor operating normally";
1119
- operationalStatus.projectId = createdItem.id!;
1120
- operationalStatus.priority = 1;
1121
- operationalStatus.isOperationalState = true;
1122
- operationalStatus.color = Green;
1123
-
1124
- operationalStatus = await MonitorStatusService.create({
1125
- data: operationalStatus,
1126
- props: {
1127
- isRoot: true,
1128
- },
1129
- });
1130
-
1131
- let degradedStatus: MonitorStatus = new MonitorStatus();
1132
- degradedStatus.name = "Degraded";
1133
- degradedStatus.description = "Monitor is operating at reduced performance.";
1134
- degradedStatus.priority = 2;
1135
- degradedStatus.projectId = createdItem.id!;
1136
- degradedStatus.color = Yellow;
1137
-
1138
- degradedStatus = await MonitorStatusService.create({
1139
- data: degradedStatus,
1140
- props: {
1141
- isRoot: true,
1142
- },
1143
- });
1232
+ const projectId: ObjectID = createdItem.id!;
1233
+
1234
+ // Idempotent see getExistingProjectScopedNames.
1235
+ const existingNames: Set<string | undefined> =
1236
+ await this.getExistingProjectScopedNames(MonitorStatusService, projectId);
1237
+
1238
+ if (!existingNames.has("Operational")) {
1239
+ const operationalStatus: MonitorStatus = new MonitorStatus();
1240
+ operationalStatus.name = "Operational";
1241
+ operationalStatus.description = "Monitor operating normally";
1242
+ operationalStatus.projectId = projectId;
1243
+ operationalStatus.priority = 1;
1244
+ operationalStatus.isOperationalState = true;
1245
+ operationalStatus.color = Green;
1246
+
1247
+ await MonitorStatusService.create({
1248
+ data: operationalStatus,
1249
+ props: {
1250
+ isRoot: true,
1251
+ },
1252
+ });
1253
+ }
1144
1254
 
1145
- let downStatus: MonitorStatus = new MonitorStatus();
1146
- downStatus.name = "Offline";
1147
- downStatus.description = "Monitor is offline.";
1148
- downStatus.isOfflineState = true;
1149
- downStatus.projectId = createdItem.id!;
1150
- downStatus.priority = 3;
1151
- downStatus.color = Red;
1255
+ if (!existingNames.has("Degraded")) {
1256
+ const degradedStatus: MonitorStatus = new MonitorStatus();
1257
+ degradedStatus.name = "Degraded";
1258
+ degradedStatus.description =
1259
+ "Monitor is operating at reduced performance.";
1260
+ degradedStatus.priority = 2;
1261
+ degradedStatus.projectId = projectId;
1262
+ degradedStatus.color = Yellow;
1263
+
1264
+ await MonitorStatusService.create({
1265
+ data: degradedStatus,
1266
+ props: {
1267
+ isRoot: true,
1268
+ },
1269
+ });
1270
+ }
1152
1271
 
1153
- downStatus = await MonitorStatusService.create({
1154
- data: downStatus,
1155
- props: {
1156
- isRoot: true,
1157
- },
1158
- });
1272
+ if (!existingNames.has("Offline")) {
1273
+ const downStatus: MonitorStatus = new MonitorStatus();
1274
+ downStatus.name = "Offline";
1275
+ downStatus.description = "Monitor is offline.";
1276
+ downStatus.isOfflineState = true;
1277
+ downStatus.projectId = projectId;
1278
+ downStatus.priority = 3;
1279
+ downStatus.color = Red;
1280
+
1281
+ await MonitorStatusService.create({
1282
+ data: downStatus,
1283
+ props: {
1284
+ isRoot: true,
1285
+ },
1286
+ });
1287
+ }
1159
1288
 
1160
1289
  return createdItem;
1161
1290
  }