@oneuptime/common 10.0.70 → 10.0.71
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/KubernetesCluster.ts +6 -4
- package/Models/DatabaseModels/Project.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +6 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
- package/Server/Services/AIBillingService.ts +2 -2
- package/Server/Services/BillingService.ts +116 -48
- package/Server/Services/NotificationService.ts +2 -2
- package/Server/Types/Database/QueryUtil.ts +13 -7
- package/Server/Utils/Monitor/MonitorAlert.ts +79 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +79 -0
- package/Types/BaseDatabase/IncludesNone.ts +1 -4
- package/Types/Email.ts +50 -0
- package/UI/Components/Filters/DateFilter.tsx +16 -8
- package/UI/Components/Filters/EntityFilter.tsx +33 -18
- package/UI/Components/Filters/FilterViewer.tsx +7 -5
- package/UI/Components/Filters/FiltersForm.tsx +1 -3
- package/UI/Components/Filters/NumberFilter.tsx +3 -2
- package/UI/Components/Filters/TextFilter.tsx +5 -4
- package/UI/Components/ModelTable/BaseModelTable.tsx +5 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +6 -4
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +5 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +4 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +1 -0
- package/build/dist/Server/Services/AIBillingService.js +2 -2
- package/build/dist/Server/Services/AIBillingService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +99 -39
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/NotificationService.js +2 -2
- package/build/dist/Server/Services/NotificationService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +13 -7
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +68 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -1
- package/build/dist/Types/Email.js +42 -0
- package/build/dist/Types/Email.js.map +1 -1
- package/build/dist/UI/Components/Filters/DateFilter.js +1 -4
- package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +21 -14
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +1 -2
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js +0 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/TextFilter.js +5 -4
- package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +5 -3
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/package.json +1 -1
|
@@ -73,10 +73,12 @@ import {
|
|
|
73
73
|
})
|
|
74
74
|
@CrudApiEndpoint(new Route("/kubernetes-cluster"))
|
|
75
75
|
@SlugifyColumn("name", "slug")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
/*
|
|
77
|
+
* Enforce one cluster row per (projectId, clusterIdentifier) at the DB level.
|
|
78
|
+
* Without this, two pods emitting OTel telemetry for a new cluster at the
|
|
79
|
+
* same time (e.g. when the agent is first installed or during a rolling
|
|
80
|
+
* update) race in findOrCreateByClusterIdentifier and create duplicate rows.
|
|
81
|
+
*/
|
|
80
82
|
@Index(["projectId", "clusterIdentifier"], { unique: true })
|
|
81
83
|
@TableMetadata({
|
|
82
84
|
tableName: "KubernetesCluster",
|
|
@@ -342,15 +342,15 @@ export default class Project extends TenantModel {
|
|
|
342
342
|
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
|
343
343
|
})
|
|
344
344
|
@TableColumn({
|
|
345
|
-
type: TableColumnType.
|
|
345
|
+
type: TableColumnType.LongText,
|
|
346
346
|
title: "Finance / Accounting Email",
|
|
347
347
|
description:
|
|
348
|
-
"Invoices, receipts and billing related notifications will be sent to
|
|
349
|
-
example: "accounting@example.com",
|
|
348
|
+
"Invoices, receipts and billing related notifications will be sent to these emails in addition to project owner. Separate multiple emails with a comma.",
|
|
349
|
+
example: "accounting@example.com, finance@example.com",
|
|
350
350
|
})
|
|
351
351
|
@Column({
|
|
352
|
-
type: ColumnType.
|
|
353
|
-
length: ColumnLength.
|
|
352
|
+
type: ColumnType.LongText,
|
|
353
|
+
length: ColumnLength.LongText,
|
|
354
354
|
nullable: true,
|
|
355
355
|
unique: false,
|
|
356
356
|
})
|
|
@@ -26,7 +26,8 @@ import { MigrationInterface, QueryRunner } from "typeorm";
|
|
|
26
26
|
export class DedupeKubernetesClustersAndAddUniqueIndex1776881254913
|
|
27
27
|
implements MigrationInterface
|
|
28
28
|
{
|
|
29
|
-
public name: string =
|
|
29
|
+
public name: string =
|
|
30
|
+
"DedupeKubernetesClustersAndAddUniqueIndex1776881254913";
|
|
30
31
|
|
|
31
32
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
32
33
|
// 1: reparent KubernetesResource FKs from duplicates -> survivor.
|
|
@@ -128,7 +129,9 @@ export class DedupeKubernetesClustersAndAddUniqueIndex1776881254913
|
|
|
128
129
|
await queryRunner.query(
|
|
129
130
|
`DROP INDEX "public"."IDX_9756988b48848f4f7532a2af0d"`,
|
|
130
131
|
);
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
/*
|
|
133
|
+
* Duplicate rows dropped in up() are lost — a down-migration cannot
|
|
134
|
+
* resurrect them (and reinstating duplicates is not desirable anyway).
|
|
135
|
+
*/
|
|
133
136
|
}
|
|
134
137
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class MigrationName1776886248361 implements MigrationInterface {
|
|
4
|
+
public name = "MigrationName1776886248361";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(
|
|
8
|
+
`ALTER TABLE "Project" ALTER COLUMN "financeAccountingEmail" TYPE character varying(500)`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
await queryRunner.query(
|
|
14
|
+
`ALTER TABLE "Project" ALTER COLUMN "financeAccountingEmail" TYPE character varying(100)`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -94,8 +94,8 @@ export class AIBillingService extends BaseService {
|
|
|
94
94
|
amountInUSD,
|
|
95
95
|
{
|
|
96
96
|
sendInvoiceByEmail: project.sendInvoicesByEmail || false,
|
|
97
|
-
|
|
98
|
-
?
|
|
97
|
+
recipientEmails: project.financeAccountingEmail
|
|
98
|
+
? Email.parseList(project.financeAccountingEmail)
|
|
99
99
|
: undefined,
|
|
100
100
|
projectId: project.id || undefined,
|
|
101
101
|
},
|
|
@@ -138,13 +138,37 @@ export class BillingService extends BaseService {
|
|
|
138
138
|
line2 = rest.substring(0, 200);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/*
|
|
142
|
+
* financeAccountingEmail may be a comma/semicolon-separated list. Parse the first
|
|
143
|
+
* valid address for Stripe (which stores a single email), and keep the full list
|
|
144
|
+
* in metadata so OneUptime can fan out invoice emails to every recipient.
|
|
145
|
+
*/
|
|
146
|
+
const parsedEmails: Array<Email> = financeAccountingEmail
|
|
147
|
+
? (() => {
|
|
148
|
+
try {
|
|
149
|
+
return Email.parseList(financeAccountingEmail);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error(
|
|
152
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for customer ${id}: ${err}`,
|
|
153
|
+
);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const normalizedEmailList: string = parsedEmails
|
|
160
|
+
.map((e: Email): string => {
|
|
161
|
+
return e.toString();
|
|
162
|
+
})
|
|
163
|
+
.join(", ");
|
|
164
|
+
|
|
141
165
|
const metadata: Record<string, string> = {
|
|
142
166
|
business_details_full: businessDetails.substring(0, 5000),
|
|
143
167
|
};
|
|
144
|
-
if (
|
|
145
|
-
metadata["finance_accounting_email"] =
|
|
168
|
+
if (normalizedEmailList) {
|
|
169
|
+
metadata["finance_accounting_email"] = normalizedEmailList.substring(
|
|
146
170
|
0,
|
|
147
|
-
|
|
171
|
+
500,
|
|
148
172
|
);
|
|
149
173
|
} else {
|
|
150
174
|
// Remove if cleared
|
|
@@ -165,11 +189,11 @@ export class BillingService extends BaseService {
|
|
|
165
189
|
};
|
|
166
190
|
|
|
167
191
|
/*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
192
|
+
* Stripe's customer.email only accepts one address. Use the first parsed email;
|
|
193
|
+
* the rest are delivered by OneUptime's own webhook-driven invoice email path.
|
|
170
194
|
*/
|
|
171
|
-
if (
|
|
172
|
-
updateParams.email =
|
|
195
|
+
if (parsedEmails.length > 0) {
|
|
196
|
+
updateParams.email = parsedEmails[0]!.toString();
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
if (line1) {
|
|
@@ -956,11 +980,16 @@ export class BillingService extends BaseService {
|
|
|
956
980
|
@CaptureSpan()
|
|
957
981
|
public async sendInvoiceByEmail(
|
|
958
982
|
invoiceId: string,
|
|
959
|
-
|
|
983
|
+
recipientEmails?: Array<Email>,
|
|
960
984
|
projectId?: ObjectID,
|
|
961
985
|
): Promise<void> {
|
|
986
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
987
|
+
.map((e: Email): string => {
|
|
988
|
+
return e.toString();
|
|
989
|
+
})
|
|
990
|
+
.join(", ");
|
|
962
991
|
logger.debug(
|
|
963
|
-
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId},
|
|
992
|
+
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId}, recipients: ${recipientsForLog}`,
|
|
964
993
|
);
|
|
965
994
|
|
|
966
995
|
if (!this.isBillingEnabled()) {
|
|
@@ -986,13 +1015,22 @@ export class BillingService extends BaseService {
|
|
|
986
1015
|
return;
|
|
987
1016
|
}
|
|
988
1017
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1018
|
+
/*
|
|
1019
|
+
* Determine recipient list. Fall back to the invoice's customer email when no
|
|
1020
|
+
* explicit recipients were supplied by the caller.
|
|
1021
|
+
*/
|
|
1022
|
+
let toEmails: Array<Email> = recipientEmails || [];
|
|
1023
|
+
if (toEmails.length === 0 && stripeInvoice.customer_email) {
|
|
1024
|
+
try {
|
|
1025
|
+
toEmails = Email.parseList(stripeInvoice.customer_email);
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
logger.error(
|
|
1028
|
+
`[Invoice Email] Failed to parse Stripe customer_email "${stripeInvoice.customer_email}" for invoice ${invoiceId}: ${err}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
993
1031
|
}
|
|
994
1032
|
|
|
995
|
-
if (
|
|
1033
|
+
if (toEmails.length === 0) {
|
|
996
1034
|
logger.error(
|
|
997
1035
|
`[Invoice Email] No recipient email found for invoice ${invoiceId}`,
|
|
998
1036
|
{ projectId: projectId?.toString() } as LogAttributes,
|
|
@@ -1021,33 +1059,41 @@ export class BillingService extends BaseService {
|
|
|
1021
1059
|
dashboardLink = `${DashboardClientUrl.toString()}/dashboard/${projectId.toString()}/settings/billing`;
|
|
1022
1060
|
}
|
|
1023
1061
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1062
|
+
for (const toEmail of toEmails) {
|
|
1063
|
+
logger.debug(
|
|
1064
|
+
`[Invoice Email] Sending invoice email to ${toEmail.toString()} - Invoice #${invoiceNumber}, Amount: ${amount}`,
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
await MailService.sendMail(
|
|
1068
|
+
{
|
|
1069
|
+
toEmail: toEmail,
|
|
1070
|
+
templateType: EmailTemplateType.Invoice,
|
|
1071
|
+
vars: {
|
|
1072
|
+
invoiceNumber: invoiceNumber,
|
|
1073
|
+
invoiceDate: invoiceDate,
|
|
1074
|
+
amount: amount,
|
|
1075
|
+
description: description || "",
|
|
1076
|
+
invoicePdfUrl: invoicePdfUrl || "",
|
|
1077
|
+
dashboardLink: dashboardLink || "",
|
|
1078
|
+
},
|
|
1079
|
+
subject: `Invoice #${invoiceNumber} from OneUptime`,
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
projectId: projectId,
|
|
1083
|
+
},
|
|
1084
|
+
);
|
|
1047
1085
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1086
|
+
logger.debug(
|
|
1087
|
+
`[Invoice Email] Successfully sent invoice ${invoiceId} email to ${toEmail.toString()}`,
|
|
1088
|
+
);
|
|
1089
|
+
} catch (perRecipientErr) {
|
|
1090
|
+
// Keep going for the remaining recipients if one fails.
|
|
1091
|
+
logger.error(
|
|
1092
|
+
`[Invoice Email] Failed to send invoice ${invoiceId} to ${toEmail.toString()}: ${perRecipientErr}`,
|
|
1093
|
+
{ projectId: projectId?.toString() } as LogAttributes,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1051
1097
|
} catch (err) {
|
|
1052
1098
|
logger.error(
|
|
1053
1099
|
`[Invoice Email] Failed to send invoice ${invoiceId} by email: ${err}`,
|
|
@@ -1108,16 +1154,21 @@ export class BillingService extends BaseService {
|
|
|
1108
1154
|
amountInUsd: number,
|
|
1109
1155
|
options?: {
|
|
1110
1156
|
sendInvoiceByEmail?: boolean | undefined;
|
|
1111
|
-
|
|
1157
|
+
recipientEmails?: Array<Email> | undefined;
|
|
1112
1158
|
projectId?: ObjectID | undefined;
|
|
1113
1159
|
},
|
|
1114
1160
|
): Promise<void> {
|
|
1115
1161
|
const sendInvoiceByEmail: boolean = options?.sendInvoiceByEmail || false;
|
|
1116
|
-
const
|
|
1162
|
+
const recipientEmails: Array<Email> | undefined = options?.recipientEmails;
|
|
1117
1163
|
const projectId: ObjectID | undefined = options?.projectId;
|
|
1118
1164
|
|
|
1165
|
+
const recipientsForLog: string = (recipientEmails || [])
|
|
1166
|
+
.map((e: Email): string => {
|
|
1167
|
+
return e.toString();
|
|
1168
|
+
})
|
|
1169
|
+
.join(", ");
|
|
1119
1170
|
logger.debug(
|
|
1120
|
-
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail},
|
|
1171
|
+
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail}, recipients: ${recipientsForLog}, projectId: ${projectId?.toString()}`,
|
|
1121
1172
|
);
|
|
1122
1173
|
|
|
1123
1174
|
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
|
|
@@ -1161,7 +1212,7 @@ export class BillingService extends BaseService {
|
|
|
1161
1212
|
logger.debug(
|
|
1162
1213
|
`[Invoice Email] sendInvoiceByEmail is true, sending invoice ${invoice.id} by email`,
|
|
1163
1214
|
);
|
|
1164
|
-
await this.sendInvoiceByEmail(invoice.id!,
|
|
1215
|
+
await this.sendInvoiceByEmail(invoice.id!, recipientEmails, projectId);
|
|
1165
1216
|
} else {
|
|
1166
1217
|
logger.debug(
|
|
1167
1218
|
`[Invoice Email] sendInvoiceByEmail is false, skipping email for invoice ${invoice.id}`,
|
|
@@ -1356,16 +1407,33 @@ export class BillingService extends BaseService {
|
|
|
1356
1407
|
},
|
|
1357
1408
|
});
|
|
1358
1409
|
|
|
1359
|
-
let
|
|
1410
|
+
let recipientEmails: Array<Email> | undefined = undefined;
|
|
1360
1411
|
let projectId: ObjectID | undefined = undefined;
|
|
1361
1412
|
|
|
1362
1413
|
if (project) {
|
|
1363
1414
|
projectId = project.id || undefined;
|
|
1364
1415
|
if (project.financeAccountingEmail) {
|
|
1365
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
recipientEmails = Email.parseList(
|
|
1418
|
+
project.financeAccountingEmail,
|
|
1419
|
+
);
|
|
1420
|
+
} catch (parseErr) {
|
|
1421
|
+
/*
|
|
1422
|
+
* Log and fall back to Stripe customer email rather than dropping
|
|
1423
|
+
* the invoice delivery entirely due to a malformed address.
|
|
1424
|
+
*/
|
|
1425
|
+
logger.error(
|
|
1426
|
+
`[Invoice Email] Failed to parse financeAccountingEmail for project ${projectId?.toString()}: ${parseErr}`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1366
1429
|
}
|
|
1430
|
+
const parsedForLog: string = (recipientEmails || [])
|
|
1431
|
+
.map((e: Email): string => {
|
|
1432
|
+
return e.toString();
|
|
1433
|
+
})
|
|
1434
|
+
.join(", ");
|
|
1367
1435
|
logger.debug(
|
|
1368
|
-
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail: ${
|
|
1436
|
+
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail recipients: ${parsedForLog}`,
|
|
1369
1437
|
);
|
|
1370
1438
|
} else {
|
|
1371
1439
|
logger.debug(
|
|
@@ -1376,7 +1444,7 @@ export class BillingService extends BaseService {
|
|
|
1376
1444
|
logger.debug(
|
|
1377
1445
|
`[Invoice Email] Sending invoice ${invoice.id} by email`,
|
|
1378
1446
|
);
|
|
1379
|
-
await this.sendInvoiceByEmail(invoice.id,
|
|
1447
|
+
await this.sendInvoiceByEmail(invoice.id, recipientEmails, projectId);
|
|
1380
1448
|
logger.debug(
|
|
1381
1449
|
`[Invoice Email] Successfully processed invoice.finalized - sent invoice ${invoice.id} by email`,
|
|
1382
1450
|
);
|
|
@@ -90,8 +90,8 @@ export class NotificationService extends BaseService {
|
|
|
90
90
|
amountInUSD,
|
|
91
91
|
{
|
|
92
92
|
sendInvoiceByEmail: project.sendInvoicesByEmail || false,
|
|
93
|
-
|
|
94
|
-
?
|
|
93
|
+
recipientEmails: project.financeAccountingEmail
|
|
94
|
+
? Email.parseList(project.financeAccountingEmail)
|
|
95
95
|
: undefined,
|
|
96
96
|
projectId: project.id || undefined,
|
|
97
97
|
},
|
|
@@ -212,8 +212,10 @@ export default class QueryUtil {
|
|
|
212
212
|
relationColumnName: manyToManyMeta.relationColumnName,
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
/*
|
|
216
|
+
* Remove the relation-based filter so TypeORM does not create a
|
|
217
|
+
* JOIN that would yield OR semantics.
|
|
218
|
+
*/
|
|
217
219
|
delete query[key];
|
|
218
220
|
|
|
219
221
|
const existingIdFilter: any = (query as any)._id;
|
|
@@ -445,11 +447,13 @@ export default class QueryUtil {
|
|
|
445
447
|
return null;
|
|
446
448
|
}
|
|
447
449
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
/*
|
|
451
|
+
* Only the owning side of a many-to-many has join/inverse columns. Follow
|
|
452
|
+
* the inverse relation when needed.
|
|
453
|
+
*/
|
|
450
454
|
const owningRelation: RelationMetadata = relation.isOwning
|
|
451
455
|
? relation
|
|
452
|
-
:
|
|
456
|
+
: relation.inverseRelation ?? relation;
|
|
453
457
|
|
|
454
458
|
const joinTableName: string | undefined =
|
|
455
459
|
owningRelation.junctionEntityMetadata?.tableName;
|
|
@@ -458,8 +462,10 @@ export default class QueryUtil {
|
|
|
458
462
|
return null;
|
|
459
463
|
}
|
|
460
464
|
|
|
461
|
-
|
|
462
|
-
|
|
465
|
+
/*
|
|
466
|
+
* When `modelType` is the owning side, its id lives on joinColumns. When
|
|
467
|
+
* it is the inverse side, its id lives on inverseJoinColumns.
|
|
468
|
+
*/
|
|
463
469
|
const ownerColumns: Array<any> = relation.isOwning
|
|
464
470
|
? owningRelation.joinColumns
|
|
465
471
|
: owningRelation.inverseJoinColumns;
|
|
@@ -16,7 +16,9 @@ import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
|
|
|
16
16
|
import { DisableAutomaticAlertCreation } from "../../EnvironmentConfig";
|
|
17
17
|
import AlertService from "../../Services/AlertService";
|
|
18
18
|
import AlertSeverityService from "../../Services/AlertSeverityService";
|
|
19
|
+
import AlertStateService from "../../Services/AlertStateService";
|
|
19
20
|
import AlertStateTimelineService from "../../Services/AlertStateTimelineService";
|
|
21
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
20
22
|
import logger, { LogAttributes } from "../Logger";
|
|
21
23
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
22
24
|
import DataToProcess from "./DataToProcess";
|
|
@@ -51,6 +53,7 @@ export default class MonitorAlert {
|
|
|
51
53
|
projectId: true,
|
|
52
54
|
alertNumber: true,
|
|
53
55
|
alertNumberWithPrefix: true,
|
|
56
|
+
currentAlertStateId: true,
|
|
54
57
|
},
|
|
55
58
|
props: {
|
|
56
59
|
isRoot: true,
|
|
@@ -328,6 +331,82 @@ export default class MonitorAlert {
|
|
|
328
331
|
input.openAlert.projectId!,
|
|
329
332
|
);
|
|
330
333
|
|
|
334
|
+
/*
|
|
335
|
+
* Skip the Resolved insert if the alert's timeline is already at or past
|
|
336
|
+
* the Resolved state in the project's workflow order. Two cases:
|
|
337
|
+
* 1. Latest timeline state is Resolved but Alert.currentAlertStateId is
|
|
338
|
+
* stuck on an earlier state (partial-failure from a prior resolve).
|
|
339
|
+
* Re-inserting Resolved would throw "Alert state cannot be same as
|
|
340
|
+
* previous state" from AlertStateTimelineService.onBeforeCreate.
|
|
341
|
+
* 2. The project defines a custom state after Resolved (e.g. Closed) and
|
|
342
|
+
* the alert has moved into it. Inserting Resolved would throw
|
|
343
|
+
* "cannot transition to Resolved from Closed because Resolved is
|
|
344
|
+
* before Closed in the order of alert states."
|
|
345
|
+
* Either failure bubbles up through ingest workers and causes monitors to
|
|
346
|
+
* flap. Reconcile Alert.currentAlertStateId if out of sync with the
|
|
347
|
+
* timeline, then return.
|
|
348
|
+
*/
|
|
349
|
+
const [resolvedState, latestTimeline]: [
|
|
350
|
+
AlertState | null,
|
|
351
|
+
AlertStateTimeline | null,
|
|
352
|
+
] = await Promise.all([
|
|
353
|
+
AlertStateService.findOneBy({
|
|
354
|
+
query: {
|
|
355
|
+
_id: resolvedStateId.toString(),
|
|
356
|
+
},
|
|
357
|
+
select: {
|
|
358
|
+
order: true,
|
|
359
|
+
},
|
|
360
|
+
props: {
|
|
361
|
+
isRoot: true,
|
|
362
|
+
},
|
|
363
|
+
}),
|
|
364
|
+
AlertStateTimelineService.findOneBy({
|
|
365
|
+
query: {
|
|
366
|
+
alertId: input.openAlert.id!,
|
|
367
|
+
},
|
|
368
|
+
sort: {
|
|
369
|
+
startsAt: SortOrder.Descending,
|
|
370
|
+
},
|
|
371
|
+
select: {
|
|
372
|
+
alertStateId: true,
|
|
373
|
+
alertState: {
|
|
374
|
+
order: true,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
props: {
|
|
378
|
+
isRoot: true,
|
|
379
|
+
},
|
|
380
|
+
}),
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const latestOrder: number | undefined | null =
|
|
384
|
+
latestTimeline?.alertState?.order;
|
|
385
|
+
const resolvedOrder: number | undefined | null = resolvedState?.order;
|
|
386
|
+
|
|
387
|
+
if (
|
|
388
|
+
latestTimeline?.alertStateId &&
|
|
389
|
+
typeof latestOrder === "number" &&
|
|
390
|
+
typeof resolvedOrder === "number" &&
|
|
391
|
+
latestOrder >= resolvedOrder
|
|
392
|
+
) {
|
|
393
|
+
if (
|
|
394
|
+
input.openAlert.currentAlertStateId?.toString() !==
|
|
395
|
+
latestTimeline.alertStateId.toString()
|
|
396
|
+
) {
|
|
397
|
+
await AlertService.updateOneById({
|
|
398
|
+
id: input.openAlert.id!,
|
|
399
|
+
data: {
|
|
400
|
+
currentAlertStateId: latestTimeline.alertStateId,
|
|
401
|
+
},
|
|
402
|
+
props: {
|
|
403
|
+
isRoot: true,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
331
410
|
const alertStateTimeline: AlertStateTimeline = new AlertStateTimeline();
|
|
332
411
|
alertStateTimeline.alertId = input.openAlert.id!;
|
|
333
412
|
alertStateTimeline.alertStateId = resolvedStateId;
|
|
@@ -17,8 +17,10 @@ import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
|
|
|
17
17
|
import { DisableAutomaticIncidentCreation } from "../../EnvironmentConfig";
|
|
18
18
|
import IncidentService from "../../Services/IncidentService";
|
|
19
19
|
import IncidentSeverityService from "../../Services/IncidentSeverityService";
|
|
20
|
+
import IncidentStateService from "../../Services/IncidentStateService";
|
|
20
21
|
import IncidentStateTimelineService from "../../Services/IncidentStateTimelineService";
|
|
21
22
|
import IncidentMemberService from "../../Services/IncidentMemberService";
|
|
23
|
+
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
|
22
24
|
import logger, { LogAttributes } from "../Logger";
|
|
23
25
|
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
24
26
|
import DataToProcess from "./DataToProcess";
|
|
@@ -57,6 +59,7 @@ export default class MonitorIncident {
|
|
|
57
59
|
projectId: true,
|
|
58
60
|
incidentNumber: true,
|
|
59
61
|
incidentNumberWithPrefix: true,
|
|
62
|
+
currentIncidentStateId: true,
|
|
60
63
|
},
|
|
61
64
|
props: {
|
|
62
65
|
isRoot: true,
|
|
@@ -393,6 +396,82 @@ export default class MonitorIncident {
|
|
|
393
396
|
input.openIncident.projectId!,
|
|
394
397
|
);
|
|
395
398
|
|
|
399
|
+
/*
|
|
400
|
+
* Skip the Resolved insert if the incident's timeline is already at or
|
|
401
|
+
* past the Resolved state in the project's workflow order. Two cases:
|
|
402
|
+
* 1. Latest timeline state is Resolved but Incident.currentIncidentStateId
|
|
403
|
+
* is stuck on an earlier state (partial-failure from a prior resolve).
|
|
404
|
+
* Re-inserting Resolved would throw "state cannot be same as previous"
|
|
405
|
+
* from IncidentStateTimelineService.onBeforeCreate.
|
|
406
|
+
* 2. The project defines a custom state after Resolved (e.g. Closed) and
|
|
407
|
+
* the incident has moved into it. Inserting Resolved would throw
|
|
408
|
+
* "cannot transition to Resolved from Closed because Resolved is
|
|
409
|
+
* before Closed in the order of incident states."
|
|
410
|
+
* Either failure bubbles up through ingest workers and causes monitors to
|
|
411
|
+
* flap. Reconcile Incident.currentIncidentStateId if out of sync with the
|
|
412
|
+
* timeline, then return.
|
|
413
|
+
*/
|
|
414
|
+
const [resolvedState, latestTimeline]: [
|
|
415
|
+
IncidentState | null,
|
|
416
|
+
IncidentStateTimeline | null,
|
|
417
|
+
] = await Promise.all([
|
|
418
|
+
IncidentStateService.findOneBy({
|
|
419
|
+
query: {
|
|
420
|
+
_id: resolvedStateId.toString(),
|
|
421
|
+
},
|
|
422
|
+
select: {
|
|
423
|
+
order: true,
|
|
424
|
+
},
|
|
425
|
+
props: {
|
|
426
|
+
isRoot: true,
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
IncidentStateTimelineService.findOneBy({
|
|
430
|
+
query: {
|
|
431
|
+
incidentId: input.openIncident.id!,
|
|
432
|
+
},
|
|
433
|
+
sort: {
|
|
434
|
+
startsAt: SortOrder.Descending,
|
|
435
|
+
},
|
|
436
|
+
select: {
|
|
437
|
+
incidentStateId: true,
|
|
438
|
+
incidentState: {
|
|
439
|
+
order: true,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
props: {
|
|
443
|
+
isRoot: true,
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
const latestOrder: number | undefined | null =
|
|
449
|
+
latestTimeline?.incidentState?.order;
|
|
450
|
+
const resolvedOrder: number | undefined | null = resolvedState?.order;
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
latestTimeline?.incidentStateId &&
|
|
454
|
+
typeof latestOrder === "number" &&
|
|
455
|
+
typeof resolvedOrder === "number" &&
|
|
456
|
+
latestOrder >= resolvedOrder
|
|
457
|
+
) {
|
|
458
|
+
if (
|
|
459
|
+
input.openIncident.currentIncidentStateId?.toString() !==
|
|
460
|
+
latestTimeline.incidentStateId.toString()
|
|
461
|
+
) {
|
|
462
|
+
await IncidentService.updateOneById({
|
|
463
|
+
id: input.openIncident.id!,
|
|
464
|
+
data: {
|
|
465
|
+
currentIncidentStateId: latestTimeline.incidentStateId,
|
|
466
|
+
},
|
|
467
|
+
props: {
|
|
468
|
+
isRoot: true,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
396
475
|
const incidentStateTimeline: IncidentStateTimeline =
|
|
397
476
|
new IncidentStateTimeline();
|
|
398
477
|
incidentStateTimeline.incidentId = input.openIncident.id!;
|
|
@@ -4,10 +4,7 @@ import JSONFunctions from "../JSONFunctions";
|
|
|
4
4
|
import ObjectID from "../ObjectID";
|
|
5
5
|
import QueryOperator from "./QueryOperator";
|
|
6
6
|
|
|
7
|
-
export type IncludesNoneType =
|
|
8
|
-
| Array<string>
|
|
9
|
-
| Array<ObjectID>
|
|
10
|
-
| Array<number>;
|
|
7
|
+
export type IncludesNoneType = Array<string> | Array<ObjectID> | Array<number>;
|
|
11
8
|
|
|
12
9
|
export default class IncludesNone extends QueryOperator<IncludesNoneType> {
|
|
13
10
|
private _values: IncludesNoneType = [];
|
package/Types/Email.ts
CHANGED
|
@@ -75,6 +75,56 @@ export default class Email extends DatabaseProperty {
|
|
|
75
75
|
return new Email(value);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
public static parseList(value: string | null | undefined): Array<Email> {
|
|
79
|
+
if (!value) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const seen: Set<string> = new Set<string>();
|
|
84
|
+
const emails: Array<Email> = [];
|
|
85
|
+
|
|
86
|
+
for (const part of value.split(/[,;\s]+/)) {
|
|
87
|
+
const trimmed: string = part.trim();
|
|
88
|
+
if (!trimmed) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const email: Email = new Email(trimmed);
|
|
92
|
+
const key: string = email.toString();
|
|
93
|
+
if (!seen.has(key)) {
|
|
94
|
+
seen.add(key);
|
|
95
|
+
emails.push(email);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return emails;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public static isValidList(value: string | null | undefined): boolean {
|
|
103
|
+
if (!value) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parts: Array<string> = value
|
|
108
|
+
.split(/[,;\s]+/)
|
|
109
|
+
.map((p: string): string => {
|
|
110
|
+
return p.trim();
|
|
111
|
+
})
|
|
112
|
+
.filter((p: string): boolean => {
|
|
113
|
+
return p.length > 0;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (parts.length === 0) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
if (!Email.isValid(part)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
78
128
|
public static override fromJSON(json: JSONObject): Email {
|
|
79
129
|
if (json["_type"] === ObjectType.Email) {
|
|
80
130
|
return new Email((json["value"] as string) || "");
|