@oneuptime/common 10.2.18 → 10.2.20
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/Index.ts +4 -0
- package/Models/DatabaseModels/ScheduledMaintenanceFeed.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenanceLabelRule.ts +742 -0
- package/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.ts +828 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1778703414082-AddScheduledMaintenanceRules.ts +375 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/ScheduledMaintenanceLabelRuleEngineService.ts +463 -0
- package/Server/Services/ScheduledMaintenanceLabelRuleService.ts +14 -0
- package/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.ts +545 -0
- package/Server/Services/ScheduledMaintenanceOwnerRuleService.ts +14 -0
- package/Server/Services/ScheduledMaintenanceService.ts +34 -0
- package/Types/Call/CallRequest.ts +29 -5
- package/Types/Docs/DocsLanguage.ts +36 -0
- package/Types/Permission.ts +96 -0
- package/UI/Components/AlertBanner/AlertBanner.tsx +4 -1
- package/UI/Components/Alerts/Alert.tsx +15 -4
- package/UI/Components/Button/DropdownButton.tsx +4 -2
- package/UI/Components/Detail/Detail.tsx +5 -1
- package/UI/Components/Detail/FieldLabel.tsx +14 -6
- package/UI/Components/Detail/PlaceholderText.tsx +4 -1
- package/UI/Components/Dropdown/Dropdown.tsx +13 -4
- package/UI/Components/ErrorMessage/ErrorMessage.tsx +9 -2
- package/UI/Components/Filters/FilterViewer.tsx +42 -31
- package/UI/Components/Filters/FiltersForm.tsx +13 -6
- package/UI/Components/Forms/BasicForm.tsx +23 -6
- package/UI/Components/Forms/Fields/FieldLabel.tsx +18 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +16 -13
- package/UI/Components/MoreMenu/MoreMenuSection.tsx +4 -1
- package/UI/Components/Navbar/NavBarItem.tsx +4 -1
- package/UI/Components/Navbar/NavBarMenuItem.tsx +7 -2
- package/UI/Components/Navbar/NavBarMenuSubItem.tsx +4 -1
- package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +4 -1
- package/UI/Components/Table/Table.tsx +13 -7
- package/UI/Components/Table/TableHeader.tsx +3 -1
- package/UI/Components/Tabs/Tab.tsx +5 -1
- package/UI/Components/TopAlert/TopAlert.tsx +8 -2
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceLabelRule.js +749 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.js +834 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceOwnerRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778703414082-AddScheduledMaintenanceRules.js +132 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1778703414082-AddScheduledMaintenanceRules.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/ScheduledMaintenanceLabelRuleEngineService.js +375 -0
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleService.js +13 -0
- package/build/dist/Server/Services/ScheduledMaintenanceLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js +431 -0
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/ScheduledMaintenanceOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +28 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Types/Call/CallRequest.js +28 -5
- package/build/dist/Types/Call/CallRequest.js.map +1 -1
- package/build/dist/Types/Docs/DocsLanguage.js +25 -0
- package/build/dist/Types/Docs/DocsLanguage.js.map +1 -0
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/AlertBanner/AlertBanner.js +5 -1
- package/build/dist/UI/Components/AlertBanner/AlertBanner.js.map +1 -1
- package/build/dist/UI/Components/Alerts/Alert.js +9 -4
- package/build/dist/UI/Components/Alerts/Alert.js.map +1 -1
- package/build/dist/UI/Components/Button/DropdownButton.js +6 -2
- package/build/dist/UI/Components/Button/DropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +4 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +11 -6
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/Detail/PlaceholderText.js +5 -1
- package/build/dist/UI/Components/Detail/PlaceholderText.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +11 -4
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +8 -2
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +49 -32
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +9 -3
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +16 -6
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FieldLabel.js +13 -6
- package/build/dist/UI/Components/Forms/Fields/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +15 -10
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuSection.js +5 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuSection.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarItem.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBarItem.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuItem.js +7 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuItem.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuSubItem.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuSubItem.js.map +1 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +5 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +12 -7
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +4 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js +5 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/TopAlert/TopAlert.js +7 -2
- package/build/dist/UI/Components/TopAlert/TopAlert.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import Label from "../../Models/DatabaseModels/Label";
|
|
2
|
+
import Monitor from "../../Models/DatabaseModels/Monitor";
|
|
3
|
+
import MonitorOwnerTeam from "../../Models/DatabaseModels/MonitorOwnerTeam";
|
|
4
|
+
import MonitorOwnerUser from "../../Models/DatabaseModels/MonitorOwnerUser";
|
|
5
|
+
import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
|
|
6
|
+
import ScheduledMaintenanceOwnerRule from "../../Models/DatabaseModels/ScheduledMaintenanceOwnerRule";
|
|
7
|
+
import Team from "../../Models/DatabaseModels/Team";
|
|
8
|
+
import User from "../../Models/DatabaseModels/User";
|
|
9
|
+
import MonitorOwnerTeamService from "./MonitorOwnerTeamService";
|
|
10
|
+
import MonitorOwnerUserService from "./MonitorOwnerUserService";
|
|
11
|
+
import MonitorService from "./MonitorService";
|
|
12
|
+
import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
|
|
13
|
+
import ScheduledMaintenanceOwnerRuleService from "./ScheduledMaintenanceOwnerRuleService";
|
|
14
|
+
import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
|
|
15
|
+
import TeamService from "./TeamService";
|
|
16
|
+
import UserService from "./UserService";
|
|
17
|
+
import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
|
|
18
|
+
import { Indigo500 } from "../../Types/BrandColors";
|
|
19
|
+
import ObjectID from "../../Types/ObjectID";
|
|
20
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
21
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
22
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
23
|
+
import logger, { LogAttributes } from "../Utils/Logger";
|
|
24
|
+
|
|
25
|
+
class ScheduledMaintenanceOwnerRuleEngineServiceClass {
|
|
26
|
+
/**
|
|
27
|
+
* Evaluates ScheduledMaintenanceOwnerRule rows for the given event and adds
|
|
28
|
+
* matched owner users / teams via ScheduledMaintenanceService.addOwners.
|
|
29
|
+
* Rules with notifyOwners set notify the added owners; rules with
|
|
30
|
+
* notifyOwners off add silently.
|
|
31
|
+
*/
|
|
32
|
+
@CaptureSpan()
|
|
33
|
+
public async applyRulesToScheduledMaintenance(
|
|
34
|
+
scheduledMaintenance: ScheduledMaintenance,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
if (!scheduledMaintenance.id || !scheduledMaintenance.projectId) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const rules: Array<ScheduledMaintenanceOwnerRule> =
|
|
42
|
+
await ScheduledMaintenanceOwnerRuleService.findBy({
|
|
43
|
+
query: {
|
|
44
|
+
projectId: scheduledMaintenance.projectId,
|
|
45
|
+
isEnabled: true,
|
|
46
|
+
},
|
|
47
|
+
props: { isRoot: true },
|
|
48
|
+
select: {
|
|
49
|
+
_id: true,
|
|
50
|
+
name: true,
|
|
51
|
+
notifyOwners: true,
|
|
52
|
+
monitors: { _id: true },
|
|
53
|
+
scheduledMaintenanceLabels: { _id: true },
|
|
54
|
+
monitorLabels: { _id: true },
|
|
55
|
+
titlePattern: true,
|
|
56
|
+
descriptionPattern: true,
|
|
57
|
+
monitorNamePattern: true,
|
|
58
|
+
monitorDescriptionPattern: true,
|
|
59
|
+
ownerUsers: { _id: true },
|
|
60
|
+
ownerTeams: { _id: true },
|
|
61
|
+
inheritOwnersFromMonitors: true,
|
|
62
|
+
},
|
|
63
|
+
limit: 100,
|
|
64
|
+
skip: 0,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (rules.length === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const usersByNotify: Map<boolean, Set<string>> = new Map([
|
|
72
|
+
[true, new Set()],
|
|
73
|
+
[false, new Set()],
|
|
74
|
+
]);
|
|
75
|
+
const teamsByNotify: Map<boolean, Set<string>> = new Map([
|
|
76
|
+
[true, new Set()],
|
|
77
|
+
[false, new Set()],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const matchedRules: Array<ScheduledMaintenanceOwnerRule> = [];
|
|
81
|
+
const allUserIds: Set<string> = new Set();
|
|
82
|
+
const allTeamIds: Set<string> = new Set();
|
|
83
|
+
let inheritFromMonitors: boolean = false;
|
|
84
|
+
const inheritNotifyMode: { value: boolean | null } = { value: null };
|
|
85
|
+
|
|
86
|
+
for (const rule of rules) {
|
|
87
|
+
const matches: boolean = await this.doesScheduledMaintenanceMatchRule(
|
|
88
|
+
scheduledMaintenance,
|
|
89
|
+
rule,
|
|
90
|
+
);
|
|
91
|
+
if (!matches) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let ruleAddedAny: boolean = false;
|
|
95
|
+
const notify: boolean = rule.notifyOwners !== false;
|
|
96
|
+
for (const user of rule.ownerUsers || []) {
|
|
97
|
+
if (user.id) {
|
|
98
|
+
usersByNotify.get(notify)!.add(user.id.toString());
|
|
99
|
+
allUserIds.add(user.id.toString());
|
|
100
|
+
ruleAddedAny = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const team of rule.ownerTeams || []) {
|
|
104
|
+
if (team.id) {
|
|
105
|
+
teamsByNotify.get(notify)!.add(team.id.toString());
|
|
106
|
+
allTeamIds.add(team.id.toString());
|
|
107
|
+
ruleAddedAny = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (rule.inheritOwnersFromMonitors) {
|
|
111
|
+
inheritFromMonitors = true;
|
|
112
|
+
ruleAddedAny = true;
|
|
113
|
+
inheritNotifyMode.value =
|
|
114
|
+
inheritNotifyMode.value === true ? true : notify;
|
|
115
|
+
}
|
|
116
|
+
if (ruleAddedAny) {
|
|
117
|
+
matchedRules.push(rule);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const inheritedFromMonitorUserIds: Set<string> = new Set();
|
|
122
|
+
const inheritedFromMonitorTeamIds: Set<string> = new Set();
|
|
123
|
+
|
|
124
|
+
if (inheritFromMonitors && scheduledMaintenance.monitors?.length) {
|
|
125
|
+
const monitorIds: Array<ObjectID> = scheduledMaintenance.monitors
|
|
126
|
+
.map((m: Monitor) => {
|
|
127
|
+
return m.id;
|
|
128
|
+
})
|
|
129
|
+
.filter((id: ObjectID | null | undefined): id is ObjectID => {
|
|
130
|
+
return Boolean(id);
|
|
131
|
+
});
|
|
132
|
+
if (monitorIds.length > 0) {
|
|
133
|
+
const [monitorOwnerUsers, monitorOwnerTeams]: [
|
|
134
|
+
Array<MonitorOwnerUser>,
|
|
135
|
+
Array<MonitorOwnerTeam>,
|
|
136
|
+
] = await Promise.all([
|
|
137
|
+
MonitorOwnerUserService.findBy({
|
|
138
|
+
query: { monitorId: QueryHelper.any(monitorIds) },
|
|
139
|
+
select: { userId: true },
|
|
140
|
+
props: { isRoot: true },
|
|
141
|
+
limit: LIMIT_MAX,
|
|
142
|
+
skip: 0,
|
|
143
|
+
}),
|
|
144
|
+
MonitorOwnerTeamService.findBy({
|
|
145
|
+
query: { monitorId: QueryHelper.any(monitorIds) },
|
|
146
|
+
select: { teamId: true },
|
|
147
|
+
props: { isRoot: true },
|
|
148
|
+
limit: LIMIT_MAX,
|
|
149
|
+
skip: 0,
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
152
|
+
for (const ownerUser of monitorOwnerUsers) {
|
|
153
|
+
if (ownerUser.userId) {
|
|
154
|
+
inheritedFromMonitorUserIds.add(ownerUser.userId.toString());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const ownerTeam of monitorOwnerTeams) {
|
|
158
|
+
if (ownerTeam.teamId) {
|
|
159
|
+
inheritedFromMonitorTeamIds.add(ownerTeam.teamId.toString());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
inheritedFromMonitorUserIds.size > 0 ||
|
|
167
|
+
inheritedFromMonitorTeamIds.size > 0
|
|
168
|
+
) {
|
|
169
|
+
const inheritNotify: boolean = inheritNotifyMode.value === true;
|
|
170
|
+
for (const id of inheritedFromMonitorUserIds) {
|
|
171
|
+
usersByNotify.get(inheritNotify)!.add(id);
|
|
172
|
+
allUserIds.add(id);
|
|
173
|
+
}
|
|
174
|
+
for (const id of inheritedFromMonitorTeamIds) {
|
|
175
|
+
teamsByNotify.get(inheritNotify)!.add(id);
|
|
176
|
+
allTeamIds.add(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (matchedRules.length === 0) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (allUserIds.size === 0 && allTeamIds.size === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const notify of [true, false]) {
|
|
189
|
+
const userIds: Array<ObjectID> = Array.from(
|
|
190
|
+
usersByNotify.get(notify)!,
|
|
191
|
+
).map((id: string) => {
|
|
192
|
+
return new ObjectID(id);
|
|
193
|
+
});
|
|
194
|
+
const teamIds: Array<ObjectID> = Array.from(
|
|
195
|
+
teamsByNotify.get(notify)!,
|
|
196
|
+
).map((id: string) => {
|
|
197
|
+
return new ObjectID(id);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (userIds.length === 0 && teamIds.length === 0) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await ScheduledMaintenanceService.addOwners(
|
|
205
|
+
scheduledMaintenance.projectId,
|
|
206
|
+
scheduledMaintenance.id,
|
|
207
|
+
userIds,
|
|
208
|
+
teamIds,
|
|
209
|
+
notify,
|
|
210
|
+
{ isRoot: true },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
logger.debug(
|
|
215
|
+
`ScheduledMaintenanceOwnerRuleEngine added owners to event ${scheduledMaintenance.id}`,
|
|
216
|
+
{
|
|
217
|
+
projectId: scheduledMaintenance.projectId.toString(),
|
|
218
|
+
} as LogAttributes,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await this.createRuleExecutedFeedItem({
|
|
222
|
+
scheduledMaintenance,
|
|
223
|
+
matchedRules,
|
|
224
|
+
userIds: Array.from(allUserIds),
|
|
225
|
+
teamIds: Array.from(allTeamIds),
|
|
226
|
+
inheritedFromMonitors:
|
|
227
|
+
inheritedFromMonitorUserIds.size + inheritedFromMonitorTeamIds.size >
|
|
228
|
+
0,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.error(
|
|
232
|
+
`Error applying scheduled maintenance owner rules: ${error}`,
|
|
233
|
+
{
|
|
234
|
+
projectId: scheduledMaintenance.projectId?.toString(),
|
|
235
|
+
scheduledMaintenanceId: scheduledMaintenance.id?.toString(),
|
|
236
|
+
} as LogAttributes,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@CaptureSpan()
|
|
242
|
+
private async createRuleExecutedFeedItem(data: {
|
|
243
|
+
scheduledMaintenance: ScheduledMaintenance;
|
|
244
|
+
matchedRules: Array<ScheduledMaintenanceOwnerRule>;
|
|
245
|
+
userIds: Array<string>;
|
|
246
|
+
teamIds: Array<string>;
|
|
247
|
+
inheritedFromMonitors: boolean;
|
|
248
|
+
}): Promise<void> {
|
|
249
|
+
const {
|
|
250
|
+
scheduledMaintenance,
|
|
251
|
+
matchedRules,
|
|
252
|
+
userIds,
|
|
253
|
+
teamIds,
|
|
254
|
+
inheritedFromMonitors,
|
|
255
|
+
} = data;
|
|
256
|
+
if (
|
|
257
|
+
!scheduledMaintenance.id ||
|
|
258
|
+
!scheduledMaintenance.projectId ||
|
|
259
|
+
matchedRules.length === 0 ||
|
|
260
|
+
(userIds.length === 0 && teamIds.length === 0)
|
|
261
|
+
) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const userObjectIds: Array<ObjectID> = userIds.map((id: string) => {
|
|
267
|
+
return new ObjectID(id);
|
|
268
|
+
});
|
|
269
|
+
const teamObjectIds: Array<ObjectID> = teamIds.map((id: string) => {
|
|
270
|
+
return new ObjectID(id);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const [users, teams]: [Array<User>, Array<Team>] = await Promise.all([
|
|
274
|
+
userObjectIds.length > 0
|
|
275
|
+
? UserService.findBy({
|
|
276
|
+
query: { _id: QueryHelper.any(userObjectIds) },
|
|
277
|
+
select: { name: true, email: true },
|
|
278
|
+
props: { isRoot: true },
|
|
279
|
+
limit: LIMIT_MAX,
|
|
280
|
+
skip: 0,
|
|
281
|
+
})
|
|
282
|
+
: Promise.resolve([] as Array<User>),
|
|
283
|
+
teamObjectIds.length > 0
|
|
284
|
+
? TeamService.findBy({
|
|
285
|
+
query: { _id: QueryHelper.any(teamObjectIds) },
|
|
286
|
+
select: { name: true },
|
|
287
|
+
props: { isRoot: true },
|
|
288
|
+
limit: LIMIT_MAX,
|
|
289
|
+
skip: 0,
|
|
290
|
+
})
|
|
291
|
+
: Promise.resolve([] as Array<Team>),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const userLines: Array<string> = users.map((u: User) => {
|
|
295
|
+
const display: string =
|
|
296
|
+
u.name?.toString() || u.email?.toString() || "Unknown User";
|
|
297
|
+
return `\n- 👤 ${display}`;
|
|
298
|
+
});
|
|
299
|
+
const teamLines: Array<string> = teams.map((t: Team) => {
|
|
300
|
+
return `\n- 👥 ${t.name?.toString() || "Unnamed Team"}`;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const ruleNames: Array<string> = matchedRules
|
|
304
|
+
.map((r: ScheduledMaintenanceOwnerRule) => {
|
|
305
|
+
return r.name?.toString() || "Unnamed Rule";
|
|
306
|
+
})
|
|
307
|
+
.filter((n: string) => {
|
|
308
|
+
return n !== "";
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const rulesPart: string =
|
|
312
|
+
ruleNames.length === 1
|
|
313
|
+
? `**${ruleNames[0]}**`
|
|
314
|
+
: ruleNames
|
|
315
|
+
.map((n: string) => {
|
|
316
|
+
return `**${n}**`;
|
|
317
|
+
})
|
|
318
|
+
.join(", ");
|
|
319
|
+
|
|
320
|
+
const ownersPart: string =
|
|
321
|
+
userLines.length + teamLines.length > 0
|
|
322
|
+
? userLines.concat(teamLines).join("")
|
|
323
|
+
: "\n- (no named owners)";
|
|
324
|
+
|
|
325
|
+
const inheritedNote: string = inheritedFromMonitors
|
|
326
|
+
? `\n\n_Some owners were inherited from the event's monitors._`
|
|
327
|
+
: "";
|
|
328
|
+
|
|
329
|
+
const feedInfoInMarkdown: string = `🛡️ **Scheduled Maintenance Owner Rule${
|
|
330
|
+
matchedRules.length > 1 ? "s" : ""
|
|
331
|
+
} executed:** ${rulesPart}\n\nAssigned the following owner${
|
|
332
|
+
userLines.length + teamLines.length === 1 ? "" : "s"
|
|
333
|
+
} to the event:${ownersPart}${inheritedNote}`;
|
|
334
|
+
|
|
335
|
+
await ScheduledMaintenanceFeedService.createScheduledMaintenanceFeedItem({
|
|
336
|
+
scheduledMaintenanceId: scheduledMaintenance.id,
|
|
337
|
+
projectId: scheduledMaintenance.projectId,
|
|
338
|
+
scheduledMaintenanceFeedEventType:
|
|
339
|
+
ScheduledMaintenanceFeedEventType.OwnerRuleExecuted,
|
|
340
|
+
displayColor: Indigo500,
|
|
341
|
+
feedInfoInMarkdown,
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.error(
|
|
345
|
+
`ScheduledMaintenanceOwnerRuleEngine: failed to create rule-executed feed item: ${
|
|
346
|
+
error instanceof Error ? error.message : String(error)
|
|
347
|
+
}`,
|
|
348
|
+
{
|
|
349
|
+
projectId: scheduledMaintenance.projectId?.toString(),
|
|
350
|
+
scheduledMaintenanceId: scheduledMaintenance.id?.toString(),
|
|
351
|
+
} as LogAttributes,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@CaptureSpan()
|
|
357
|
+
private async doesScheduledMaintenanceMatchRule(
|
|
358
|
+
scheduledMaintenance: ScheduledMaintenance,
|
|
359
|
+
rule: ScheduledMaintenanceOwnerRule,
|
|
360
|
+
): Promise<boolean> {
|
|
361
|
+
if (rule.monitors && rule.monitors.length > 0) {
|
|
362
|
+
if (
|
|
363
|
+
!scheduledMaintenance.monitors ||
|
|
364
|
+
scheduledMaintenance.monitors.length === 0
|
|
365
|
+
) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const ruleMonitorIds: Array<string> = rule.monitors.map((m: Monitor) => {
|
|
369
|
+
return m.id?.toString() || "";
|
|
370
|
+
});
|
|
371
|
+
const eventMonitorIds: Array<string> = scheduledMaintenance.monitors.map(
|
|
372
|
+
(m: Monitor) => {
|
|
373
|
+
return m.id?.toString() || "";
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
if (
|
|
377
|
+
!ruleMonitorIds.some((id: string) => {
|
|
378
|
+
return eventMonitorIds.includes(id);
|
|
379
|
+
})
|
|
380
|
+
) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (
|
|
386
|
+
rule.scheduledMaintenanceLabels &&
|
|
387
|
+
rule.scheduledMaintenanceLabels.length > 0
|
|
388
|
+
) {
|
|
389
|
+
if (
|
|
390
|
+
!scheduledMaintenance.labels ||
|
|
391
|
+
scheduledMaintenance.labels.length === 0
|
|
392
|
+
) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const ruleLabelIds: Array<string> = rule.scheduledMaintenanceLabels.map(
|
|
396
|
+
(l: Label) => {
|
|
397
|
+
return l.id?.toString() || "";
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
const eventLabelIds: Array<string> = scheduledMaintenance.labels.map(
|
|
401
|
+
(l: Label) => {
|
|
402
|
+
return l.id?.toString() || "";
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
if (
|
|
406
|
+
!ruleLabelIds.some((id: string) => {
|
|
407
|
+
return eventLabelIds.includes(id);
|
|
408
|
+
})
|
|
409
|
+
) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const hasMonitorCriteria: boolean = Boolean(
|
|
415
|
+
(rule.monitorLabels && rule.monitorLabels.length > 0) ||
|
|
416
|
+
rule.monitorNamePattern ||
|
|
417
|
+
rule.monitorDescriptionPattern,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (hasMonitorCriteria) {
|
|
421
|
+
if (
|
|
422
|
+
!scheduledMaintenance.monitors ||
|
|
423
|
+
scheduledMaintenance.monitors.length === 0
|
|
424
|
+
) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let anyMonitorMatches: boolean = false;
|
|
429
|
+
for (const eventMonitor of scheduledMaintenance.monitors) {
|
|
430
|
+
if (!eventMonitor.id) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const monitor: Monitor | null = await MonitorService.findOneById({
|
|
434
|
+
id: eventMonitor.id,
|
|
435
|
+
select: {
|
|
436
|
+
name: true,
|
|
437
|
+
description: true,
|
|
438
|
+
labels: { _id: true },
|
|
439
|
+
},
|
|
440
|
+
props: { isRoot: true },
|
|
441
|
+
});
|
|
442
|
+
if (!monitor) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let monitorMatches: boolean = true;
|
|
447
|
+
|
|
448
|
+
if (rule.monitorLabels && rule.monitorLabels.length > 0) {
|
|
449
|
+
if (!monitor.labels || monitor.labels.length === 0) {
|
|
450
|
+
monitorMatches = false;
|
|
451
|
+
} else {
|
|
452
|
+
const ruleMonitorLabelIds: Array<string> = rule.monitorLabels.map(
|
|
453
|
+
(l: Label) => {
|
|
454
|
+
return l.id?.toString() || "";
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
const monitorLabelIds: Array<string> = monitor.labels.map(
|
|
458
|
+
(l: Label) => {
|
|
459
|
+
return l.id?.toString() || "";
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
if (
|
|
463
|
+
!ruleMonitorLabelIds.some((id: string) => {
|
|
464
|
+
return monitorLabelIds.includes(id);
|
|
465
|
+
})
|
|
466
|
+
) {
|
|
467
|
+
monitorMatches = false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
monitorMatches &&
|
|
474
|
+
rule.monitorNamePattern &&
|
|
475
|
+
(!monitor.name ||
|
|
476
|
+
!this.testRegex(rule.monitorNamePattern, monitor.name, rule))
|
|
477
|
+
) {
|
|
478
|
+
monitorMatches = false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (
|
|
482
|
+
monitorMatches &&
|
|
483
|
+
rule.monitorDescriptionPattern &&
|
|
484
|
+
(!monitor.description ||
|
|
485
|
+
!this.testRegex(
|
|
486
|
+
rule.monitorDescriptionPattern,
|
|
487
|
+
monitor.description,
|
|
488
|
+
rule,
|
|
489
|
+
))
|
|
490
|
+
) {
|
|
491
|
+
monitorMatches = false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (monitorMatches) {
|
|
495
|
+
anyMonitorMatches = true;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!anyMonitorMatches) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (
|
|
506
|
+
rule.titlePattern &&
|
|
507
|
+
(!scheduledMaintenance.title ||
|
|
508
|
+
!this.testRegex(rule.titlePattern, scheduledMaintenance.title, rule))
|
|
509
|
+
) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (
|
|
514
|
+
rule.descriptionPattern &&
|
|
515
|
+
(!scheduledMaintenance.description ||
|
|
516
|
+
!this.testRegex(
|
|
517
|
+
rule.descriptionPattern,
|
|
518
|
+
scheduledMaintenance.description,
|
|
519
|
+
rule,
|
|
520
|
+
))
|
|
521
|
+
) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private testRegex(
|
|
529
|
+
pattern: string,
|
|
530
|
+
value: string,
|
|
531
|
+
rule: ScheduledMaintenanceOwnerRule,
|
|
532
|
+
): boolean {
|
|
533
|
+
try {
|
|
534
|
+
const regex: RegExp = new RegExp(pattern, "i");
|
|
535
|
+
return regex.test(value);
|
|
536
|
+
} catch {
|
|
537
|
+
logger.warn(
|
|
538
|
+
`Invalid regex in scheduled maintenance owner rule ${rule.id}: ${pattern}`,
|
|
539
|
+
);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export default new ScheduledMaintenanceOwnerRuleEngineServiceClass();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/ScheduledMaintenanceOwnerRule";
|
|
3
|
+
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
4
|
+
|
|
5
|
+
export class Service extends DatabaseService<Model> {
|
|
6
|
+
public constructor() {
|
|
7
|
+
super(Model);
|
|
8
|
+
if (IsBillingEnabled) {
|
|
9
|
+
this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default new Service();
|
|
@@ -49,6 +49,8 @@ import Protocol from "../../Types/API/Protocol";
|
|
|
49
49
|
import { IsBillingEnabled } from "../EnvironmentConfig";
|
|
50
50
|
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
|
51
51
|
import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
|
|
52
|
+
import ScheduledMaintenanceLabelRuleEngineService from "./ScheduledMaintenanceLabelRuleEngineService";
|
|
53
|
+
import ScheduledMaintenanceOwnerRuleEngineService from "./ScheduledMaintenanceOwnerRuleEngineService";
|
|
52
54
|
import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
|
|
53
55
|
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
|
54
56
|
import StatusPageSubscriberWebhookUtil from "../Utils/StatusPageSubscriberWebhook";
|
|
@@ -935,6 +937,38 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
|
|
|
935
937
|
return Promise.resolve();
|
|
936
938
|
}
|
|
937
939
|
})
|
|
940
|
+
.then(async () => {
|
|
941
|
+
// Apply owner rules: add matched owner users/teams to the event.
|
|
942
|
+
try {
|
|
943
|
+
await ScheduledMaintenanceOwnerRuleEngineService.applyRulesToScheduledMaintenance(
|
|
944
|
+
createdItem,
|
|
945
|
+
);
|
|
946
|
+
} catch (error) {
|
|
947
|
+
logger.error(
|
|
948
|
+
`Apply scheduled maintenance owner rules failed in ScheduledMaintenanceService.onCreateSuccess: ${error}`,
|
|
949
|
+
{
|
|
950
|
+
projectId: createdItem.projectId?.toString(),
|
|
951
|
+
scheduledMaintenanceId: createdItem.id?.toString(),
|
|
952
|
+
} as LogAttributes,
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
})
|
|
956
|
+
.then(async () => {
|
|
957
|
+
// Apply label rules: attach matched (and optionally inherited monitor) labels to the event.
|
|
958
|
+
try {
|
|
959
|
+
await ScheduledMaintenanceLabelRuleEngineService.applyRulesToScheduledMaintenance(
|
|
960
|
+
createdItem,
|
|
961
|
+
);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
logger.error(
|
|
964
|
+
`Apply scheduled maintenance label rules failed in ScheduledMaintenanceService.onCreateSuccess: ${error}`,
|
|
965
|
+
{
|
|
966
|
+
projectId: createdItem.projectId?.toString(),
|
|
967
|
+
scheduledMaintenanceId: createdItem.id?.toString(),
|
|
968
|
+
} as LogAttributes,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
})
|
|
938
972
|
.catch((error: Error) => {
|
|
939
973
|
logger.error(
|
|
940
974
|
`Critical error in ScheduledMaintenanceService sequential operations: ${error}`,
|
|
@@ -31,13 +31,37 @@ export default interface CallRequest extends CallRequestMessage {
|
|
|
31
31
|
|
|
32
32
|
type IsHighRiskPhoneNumberFunction = (phoneNumber: Phone) => boolean;
|
|
33
33
|
|
|
34
|
+
/*
|
|
35
|
+
* Country dialing codes flagged as high-risk for IRSF / SMS pumping fraud.
|
|
36
|
+
* Twilio surfaces its current list dynamically in the Console; this set is
|
|
37
|
+
* drawn from publicly reported fraud patterns and should be reviewed
|
|
38
|
+
* periodically against Twilio Geo Permissions and Fraud Guard.
|
|
39
|
+
*/
|
|
40
|
+
const HIGH_RISK_COUNTRY_DIALING_CODES: Array<string> = [
|
|
41
|
+
"+92", // Pakistan
|
|
42
|
+
"+27", // South Africa
|
|
43
|
+
"+213", // Algeria
|
|
44
|
+
"+371", // Latvia
|
|
45
|
+
"+370", // Lithuania
|
|
46
|
+
"+372", // Estonia
|
|
47
|
+
"+252", // Somalia
|
|
48
|
+
"+232", // Sierra Leone
|
|
49
|
+
"+231", // Liberia
|
|
50
|
+
"+53", // Cuba
|
|
51
|
+
"+960", // Maldives
|
|
52
|
+
"+992", // Tajikistan
|
|
53
|
+
"+880", // Bangladesh
|
|
54
|
+
"+62", // Indonesia
|
|
55
|
+
"+84", // Vietnam
|
|
56
|
+
"+95", // Myanmar
|
|
57
|
+
];
|
|
58
|
+
|
|
34
59
|
export const isHighRiskPhoneNumber: IsHighRiskPhoneNumberFunction = (
|
|
35
60
|
phoneNumber: Phone,
|
|
36
61
|
): boolean => {
|
|
37
|
-
|
|
38
|
-
if (phoneNumber.toString().startsWith("+92")) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
62
|
+
const phoneNumberString: string = phoneNumber.toString();
|
|
41
63
|
|
|
42
|
-
return
|
|
64
|
+
return HIGH_RISK_COUNTRY_DIALING_CODES.some((code: string) => {
|
|
65
|
+
return phoneNumberString.startsWith(code);
|
|
66
|
+
});
|
|
43
67
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface DocsLanguage {
|
|
2
|
+
code: string;
|
|
3
|
+
nativeName: string;
|
|
4
|
+
englishName: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_DOCS_LANGUAGE: string = "en";
|
|
8
|
+
|
|
9
|
+
export const SUPPORTED_DOCS_LANGUAGES: Array<DocsLanguage> = [
|
|
10
|
+
{ code: "en", nativeName: "English", englishName: "English" },
|
|
11
|
+
{ code: "de", nativeName: "Deutsch", englishName: "German" },
|
|
12
|
+
{ code: "fr", nativeName: "Français", englishName: "French" },
|
|
13
|
+
{ code: "es", nativeName: "Español", englishName: "Spanish" },
|
|
14
|
+
{ code: "it", nativeName: "Italiano", englishName: "Italian" },
|
|
15
|
+
{ code: "pt", nativeName: "Português", englishName: "Portuguese" },
|
|
16
|
+
{ code: "nl", nativeName: "Nederlands", englishName: "Dutch" },
|
|
17
|
+
{ code: "da", nativeName: "Dansk", englishName: "Danish" },
|
|
18
|
+
{ code: "no", nativeName: "Norsk", englishName: "Norwegian" },
|
|
19
|
+
{ code: "sv", nativeName: "Svenska", englishName: "Swedish" },
|
|
20
|
+
{ code: "ru", nativeName: "Русский", englishName: "Russian" },
|
|
21
|
+
{ code: "ja", nativeName: "日本語", englishName: "Japanese" },
|
|
22
|
+
{ code: "ko", nativeName: "한국어", englishName: "Korean" },
|
|
23
|
+
{ code: "zh", nativeName: "中文", englishName: "Chinese" },
|
|
24
|
+
{ code: "hi", nativeName: "हिन्दी", englishName: "Hindi" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const SUPPORTED_DOCS_LANGUAGE_CODES: Array<string> =
|
|
28
|
+
SUPPORTED_DOCS_LANGUAGES.map((language: DocsLanguage) => {
|
|
29
|
+
return language.code;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const isSupportedDocsLanguage: (code: string) => boolean = (
|
|
33
|
+
code: string,
|
|
34
|
+
): boolean => {
|
|
35
|
+
return SUPPORTED_DOCS_LANGUAGE_CODES.includes(code);
|
|
36
|
+
};
|