@oneuptime/common 10.0.69 → 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 +7 -0
- package/Models/DatabaseModels/Project.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +11 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AIBillingService.ts +2 -2
- package/Server/Services/BillingService.ts +116 -48
- package/Server/Services/DatabaseService.ts +10 -27
- package/Server/Services/KubernetesResourceService.ts +33 -10
- package/Server/Services/NotificationService.ts +2 -2
- package/Server/Types/Database/QueryHelper.ts +127 -0
- package/Server/Types/Database/QueryUtil.ts +250 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +79 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +79 -0
- package/Types/BaseDatabase/EndsWith.ts +41 -0
- package/Types/BaseDatabase/IncludesAll.ts +45 -0
- package/Types/BaseDatabase/IncludesNone.ts +45 -0
- package/Types/BaseDatabase/NotContains.ts +41 -0
- package/Types/BaseDatabase/StartsWith.ts +41 -0
- package/Types/Email.ts +50 -0
- package/Types/JSON.ts +20 -0
- package/Types/SerializableObjectDictionary.ts +10 -0
- package/UI/Components/Filters/BooleanFilter.tsx +1 -0
- package/UI/Components/Filters/DateFilter.tsx +220 -25
- package/UI/Components/Filters/DropdownFilter.tsx +1 -0
- package/UI/Components/Filters/EntityFilter.tsx +229 -41
- package/UI/Components/Filters/FilterViewer.tsx +231 -147
- package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
- package/UI/Components/Filters/FiltersForm.tsx +146 -97
- package/UI/Components/Filters/NumberFilter.tsx +220 -34
- package/UI/Components/Filters/OperatorSelector.tsx +91 -0
- package/UI/Components/Filters/TextFilter.tsx +183 -71
- package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +10 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +9 -1
- 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/1776865086264-MigrationName.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +125 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
- 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/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- 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/DatabaseService.js +9 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +4 -2
- package/build/dist/Server/Services/KubernetesResourceService.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/QueryHelper.js +110 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +186 -0
- 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/EndsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
- package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
- package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
- package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
- package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
- package/build/dist/Types/Email.js +42 -0
- package/build/dist/Types/Email.js.map +1 -1
- package/build/dist/Types/JSON.js +5 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +10 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
- package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DateFilter.js +155 -14
- package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
- package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +181 -30
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +188 -98
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
- package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/NumberFilter.js +164 -23
- package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
- package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
- package/build/dist/UI/Components/Filters/TextFilter.js +131 -53
- package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
- package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +9 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/package.json +1 -1
|
@@ -138,6 +138,48 @@ export default class QueryHelper {
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
@CaptureSpan()
|
|
142
|
+
public static notContains(name: string): FindWhereProperty<any> {
|
|
143
|
+
name = name.toLowerCase().trim();
|
|
144
|
+
const rid: string = Text.generateRandomText(10);
|
|
145
|
+
return Raw(
|
|
146
|
+
(alias: string) => {
|
|
147
|
+
return `(CAST(${alias} AS TEXT) NOT ILIKE :${rid} OR ${alias} IS NULL)`;
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
[rid]: `%${name}%`,
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@CaptureSpan()
|
|
156
|
+
public static startsWith(name: string): FindWhereProperty<any> {
|
|
157
|
+
name = name.toLowerCase().trim();
|
|
158
|
+
const rid: string = Text.generateRandomText(10);
|
|
159
|
+
return Raw(
|
|
160
|
+
(alias: string) => {
|
|
161
|
+
return `(CAST(${alias} AS TEXT) ILIKE :${rid})`;
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
[rid]: `${name}%`,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@CaptureSpan()
|
|
170
|
+
public static endsWith(name: string): FindWhereProperty<any> {
|
|
171
|
+
name = name.toLowerCase().trim();
|
|
172
|
+
const rid: string = Text.generateRandomText(10);
|
|
173
|
+
return Raw(
|
|
174
|
+
(alias: string) => {
|
|
175
|
+
return `(CAST(${alias} AS TEXT) ILIKE :${rid})`;
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
[rid]: `%${name}`,
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
141
183
|
@CaptureSpan()
|
|
142
184
|
public static all(values: Array<string | ObjectID>): FindWhereProperty<any> {
|
|
143
185
|
values = values.map((value: string | ObjectID) => {
|
|
@@ -168,6 +210,91 @@ export default class QueryHelper {
|
|
|
168
210
|
return this.in(values); // any and in are the same
|
|
169
211
|
}
|
|
170
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Returns a filter that matches owner rows that are linked to *none* of the
|
|
215
|
+
* provided related entity ids through a many-to-many join table. The
|
|
216
|
+
* returned FindOperator is intended to be applied to the primary id column
|
|
217
|
+
* of the owner entity.
|
|
218
|
+
*/
|
|
219
|
+
@CaptureSpan()
|
|
220
|
+
public static noneEntitiesInManyToMany(data: {
|
|
221
|
+
values: Array<string | ObjectID>;
|
|
222
|
+
joinTableName: string;
|
|
223
|
+
ownerColumnName: string;
|
|
224
|
+
relationColumnName: string;
|
|
225
|
+
}): FindWhereProperty<any> {
|
|
226
|
+
const values: Array<string> = data.values.map(
|
|
227
|
+
(value: string | ObjectID) => {
|
|
228
|
+
return value.toString();
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!values || values.length === 0) {
|
|
233
|
+
return Raw(() => {
|
|
234
|
+
return `TRUE = TRUE`;
|
|
235
|
+
}, {});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const valuesRid: string = Text.generateRandomText(10);
|
|
239
|
+
|
|
240
|
+
const joinTable: string = data.joinTableName.replace(/"/g, '""');
|
|
241
|
+
const ownerCol: string = data.ownerColumnName.replace(/"/g, '""');
|
|
242
|
+
const relationCol: string = data.relationColumnName.replace(/"/g, '""');
|
|
243
|
+
|
|
244
|
+
return Raw(
|
|
245
|
+
(alias: string) => {
|
|
246
|
+
return `(${alias} NOT IN (SELECT "${joinTable}"."${ownerCol}" FROM "${joinTable}" WHERE "${joinTable}"."${relationCol}" IN (:...${valuesRid})))`;
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
[valuesRid]: values,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Returns a filter that matches owner rows that are linked to *all* of the
|
|
256
|
+
* provided related entity ids through a many-to-many join table. The
|
|
257
|
+
* returned FindOperator is intended to be applied to the primary id column
|
|
258
|
+
* of the owner entity.
|
|
259
|
+
*/
|
|
260
|
+
@CaptureSpan()
|
|
261
|
+
public static allEntitiesInManyToMany(data: {
|
|
262
|
+
values: Array<string | ObjectID>;
|
|
263
|
+
joinTableName: string;
|
|
264
|
+
ownerColumnName: string;
|
|
265
|
+
relationColumnName: string;
|
|
266
|
+
}): FindWhereProperty<any> {
|
|
267
|
+
const values: Array<string> = data.values.map(
|
|
268
|
+
(value: string | ObjectID) => {
|
|
269
|
+
return value.toString();
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (!values || values.length === 0) {
|
|
274
|
+
return Raw(() => {
|
|
275
|
+
return `TRUE = FALSE`;
|
|
276
|
+
}, {});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const valuesRid: string = Text.generateRandomText(10);
|
|
280
|
+
const countRid: string = Text.generateRandomText(10);
|
|
281
|
+
|
|
282
|
+
// Escape identifiers so they can safely be embedded in the SQL string.
|
|
283
|
+
const joinTable: string = data.joinTableName.replace(/"/g, '""');
|
|
284
|
+
const ownerCol: string = data.ownerColumnName.replace(/"/g, '""');
|
|
285
|
+
const relationCol: string = data.relationColumnName.replace(/"/g, '""');
|
|
286
|
+
|
|
287
|
+
return Raw(
|
|
288
|
+
(alias: string) => {
|
|
289
|
+
return `(${alias} IN (SELECT "${joinTable}"."${ownerCol}" FROM "${joinTable}" WHERE "${joinTable}"."${relationCol}" IN (:...${valuesRid}) GROUP BY "${joinTable}"."${ownerCol}" HAVING COUNT(DISTINCT "${joinTable}"."${relationCol}") >= :${countRid}))`;
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
[valuesRid]: values,
|
|
293
|
+
[countRid]: values.length,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
171
298
|
private static in(
|
|
172
299
|
values: Array<string | ObjectID | number>,
|
|
173
300
|
): FindWhereProperty<any> {
|
|
@@ -6,6 +6,11 @@ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
|
6
6
|
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
7
7
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
8
8
|
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
9
|
+
import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
|
|
10
|
+
import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
|
|
11
|
+
import StartsWith from "../../../Types/BaseDatabase/StartsWith";
|
|
12
|
+
import EndsWith from "../../../Types/BaseDatabase/EndsWith";
|
|
13
|
+
import NotContains from "../../../Types/BaseDatabase/NotContains";
|
|
9
14
|
import IsNull from "../../../Types/BaseDatabase/IsNull";
|
|
10
15
|
import LessThan from "../../../Types/BaseDatabase/LessThan";
|
|
11
16
|
import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
|
|
@@ -17,12 +22,16 @@ import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
|
17
22
|
import { JSONObject } from "../../../Types/JSON";
|
|
18
23
|
import ObjectID from "../../../Types/ObjectID";
|
|
19
24
|
import Typeof from "../../../Types/Typeof";
|
|
25
|
+
import { And, DataSource } from "typeorm";
|
|
20
26
|
import { FindOperator } from "typeorm/find-options/FindOperator";
|
|
21
27
|
import { CompareType } from "../../../Types/Database/CompareBase";
|
|
22
28
|
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
|
|
23
29
|
import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
|
|
24
30
|
import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
|
|
25
31
|
import EqualTo from "../../../Types/BaseDatabase/EqualTo";
|
|
32
|
+
import PostgresAppInstance from "../../Infrastructure/PostgresDatabase";
|
|
33
|
+
import { RelationMetadata } from "typeorm/metadata/RelationMetadata";
|
|
34
|
+
import { EntityMetadata } from "typeorm/metadata/EntityMetadata";
|
|
26
35
|
|
|
27
36
|
export default class QueryUtil {
|
|
28
37
|
@CaptureSpan()
|
|
@@ -111,6 +120,30 @@ export default class QueryUtil {
|
|
|
111
120
|
query[key] = QueryHelper.search(
|
|
112
121
|
(query[key] as Search<string>).toString() as any,
|
|
113
122
|
) as any;
|
|
123
|
+
} else if (
|
|
124
|
+
query[key] &&
|
|
125
|
+
query[key] instanceof NotContains &&
|
|
126
|
+
tableColumnMetadata
|
|
127
|
+
) {
|
|
128
|
+
query[key] = QueryHelper.notContains(
|
|
129
|
+
(query[key] as NotContains<string>).toString() as any,
|
|
130
|
+
) as any;
|
|
131
|
+
} else if (
|
|
132
|
+
query[key] &&
|
|
133
|
+
query[key] instanceof StartsWith &&
|
|
134
|
+
tableColumnMetadata
|
|
135
|
+
) {
|
|
136
|
+
query[key] = QueryHelper.startsWith(
|
|
137
|
+
(query[key] as StartsWith<string>).toString() as any,
|
|
138
|
+
) as any;
|
|
139
|
+
} else if (
|
|
140
|
+
query[key] &&
|
|
141
|
+
query[key] instanceof EndsWith &&
|
|
142
|
+
tableColumnMetadata
|
|
143
|
+
) {
|
|
144
|
+
query[key] = QueryHelper.endsWith(
|
|
145
|
+
(query[key] as EndsWith<string>).toString() as any,
|
|
146
|
+
) as any;
|
|
114
147
|
} else if (
|
|
115
148
|
query[key] &&
|
|
116
149
|
query[key] instanceof LessThan &&
|
|
@@ -142,6 +175,140 @@ export default class QueryUtil {
|
|
|
142
175
|
query[key] = QueryHelper.greaterThan(
|
|
143
176
|
(query[key] as GreaterThan<CompareType>).toString() as any,
|
|
144
177
|
) as any;
|
|
178
|
+
} else if (
|
|
179
|
+
query[key] &&
|
|
180
|
+
query[key] instanceof IncludesAll &&
|
|
181
|
+
tableColumnMetadata
|
|
182
|
+
) {
|
|
183
|
+
if (tableColumnMetadata.type === TableColumnType.EntityArray) {
|
|
184
|
+
const includesAll: IncludesAll = query[key] as IncludesAll;
|
|
185
|
+
const values: Array<string | ObjectID> = (
|
|
186
|
+
includesAll.values as Array<string | ObjectID | number>
|
|
187
|
+
).map((item: string | ObjectID | number) => {
|
|
188
|
+
if (
|
|
189
|
+
item !== null &&
|
|
190
|
+
typeof item === Typeof.Object &&
|
|
191
|
+
!(item instanceof ObjectID)
|
|
192
|
+
) {
|
|
193
|
+
const itemRecord: JSONObject = item as unknown as JSONObject;
|
|
194
|
+
if (itemRecord["_id"]) {
|
|
195
|
+
return itemRecord["_id"] as string;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return item.toString();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const manyToManyMeta: {
|
|
202
|
+
joinTableName: string;
|
|
203
|
+
ownerColumnName: string;
|
|
204
|
+
relationColumnName: string;
|
|
205
|
+
} | null = QueryUtil.getManyToManyRelationMetadata(modelType, key);
|
|
206
|
+
|
|
207
|
+
if (manyToManyMeta && values.length > 0) {
|
|
208
|
+
const subqueryFilter: any = QueryHelper.allEntitiesInManyToMany({
|
|
209
|
+
values,
|
|
210
|
+
joinTableName: manyToManyMeta.joinTableName,
|
|
211
|
+
ownerColumnName: manyToManyMeta.ownerColumnName,
|
|
212
|
+
relationColumnName: manyToManyMeta.relationColumnName,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* Remove the relation-based filter so TypeORM does not create a
|
|
217
|
+
* JOIN that would yield OR semantics.
|
|
218
|
+
*/
|
|
219
|
+
delete query[key];
|
|
220
|
+
|
|
221
|
+
const existingIdFilter: any = (query as any)._id;
|
|
222
|
+
if (existingIdFilter instanceof FindOperator) {
|
|
223
|
+
(query as any)._id = And(existingIdFilter, subqueryFilter);
|
|
224
|
+
} else if (
|
|
225
|
+
existingIdFilter &&
|
|
226
|
+
typeof existingIdFilter === Typeof.String
|
|
227
|
+
) {
|
|
228
|
+
(query as any)._id = And(
|
|
229
|
+
QueryHelper.equalTo(existingIdFilter as string),
|
|
230
|
+
subqueryFilter,
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
(query as any)._id = subqueryFilter;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// Fall back to OR behavior when metadata cannot be resolved.
|
|
237
|
+
query[key] = values as any;
|
|
238
|
+
}
|
|
239
|
+
} else if (tableColumnMetadata.type === TableColumnType.Entity) {
|
|
240
|
+
// Entity (single) columns treat AND as a single match — same as OR.
|
|
241
|
+
query[key] = (query[key] as IncludesAll).values as any;
|
|
242
|
+
} else {
|
|
243
|
+
query[key] = QueryHelper.any(
|
|
244
|
+
(query[key] as IncludesAll).values,
|
|
245
|
+
) as any;
|
|
246
|
+
}
|
|
247
|
+
} else if (
|
|
248
|
+
query[key] &&
|
|
249
|
+
query[key] instanceof IncludesNone &&
|
|
250
|
+
tableColumnMetadata
|
|
251
|
+
) {
|
|
252
|
+
if (tableColumnMetadata.type === TableColumnType.EntityArray) {
|
|
253
|
+
const includesNone: IncludesNone = query[key] as IncludesNone;
|
|
254
|
+
const values: Array<string | ObjectID> = (
|
|
255
|
+
includesNone.values as Array<string | ObjectID | number>
|
|
256
|
+
).map((item: string | ObjectID | number) => {
|
|
257
|
+
if (
|
|
258
|
+
item !== null &&
|
|
259
|
+
typeof item === Typeof.Object &&
|
|
260
|
+
!(item instanceof ObjectID)
|
|
261
|
+
) {
|
|
262
|
+
const itemRecord: JSONObject = item as unknown as JSONObject;
|
|
263
|
+
if (itemRecord["_id"]) {
|
|
264
|
+
return itemRecord["_id"] as string;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return item.toString();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const manyToManyMeta: {
|
|
271
|
+
joinTableName: string;
|
|
272
|
+
ownerColumnName: string;
|
|
273
|
+
relationColumnName: string;
|
|
274
|
+
} | null = QueryUtil.getManyToManyRelationMetadata(modelType, key);
|
|
275
|
+
|
|
276
|
+
if (manyToManyMeta && values.length > 0) {
|
|
277
|
+
const subqueryFilter: any = QueryHelper.noneEntitiesInManyToMany({
|
|
278
|
+
values,
|
|
279
|
+
joinTableName: manyToManyMeta.joinTableName,
|
|
280
|
+
ownerColumnName: manyToManyMeta.ownerColumnName,
|
|
281
|
+
relationColumnName: manyToManyMeta.relationColumnName,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
delete query[key];
|
|
285
|
+
|
|
286
|
+
const existingIdFilter: any = (query as any)._id;
|
|
287
|
+
if (existingIdFilter instanceof FindOperator) {
|
|
288
|
+
(query as any)._id = And(existingIdFilter, subqueryFilter);
|
|
289
|
+
} else if (
|
|
290
|
+
existingIdFilter &&
|
|
291
|
+
typeof existingIdFilter === Typeof.String
|
|
292
|
+
) {
|
|
293
|
+
(query as any)._id = And(
|
|
294
|
+
QueryHelper.equalTo(existingIdFilter as string),
|
|
295
|
+
subqueryFilter,
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
(query as any)._id = subqueryFilter;
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
delete query[key];
|
|
302
|
+
}
|
|
303
|
+
} else if (tableColumnMetadata.type === TableColumnType.Entity) {
|
|
304
|
+
query[key] = QueryHelper.notIn(
|
|
305
|
+
(query[key] as IncludesNone).values as Array<string | ObjectID>,
|
|
306
|
+
) as any;
|
|
307
|
+
} else {
|
|
308
|
+
query[key] = QueryHelper.notIn(
|
|
309
|
+
(query[key] as IncludesNone).values as Array<string | ObjectID>,
|
|
310
|
+
) as any;
|
|
311
|
+
}
|
|
145
312
|
} else if (
|
|
146
313
|
query[key] &&
|
|
147
314
|
query[key] instanceof Includes &&
|
|
@@ -237,4 +404,87 @@ export default class QueryUtil {
|
|
|
237
404
|
|
|
238
405
|
return query;
|
|
239
406
|
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Resolves the join-table metadata for a many-to-many relation declared on
|
|
410
|
+
* the provided model. Returns null when the column is not a many-to-many
|
|
411
|
+
* relation, the database connection is not yet ready, or required metadata
|
|
412
|
+
* is missing.
|
|
413
|
+
*/
|
|
414
|
+
public static getManyToManyRelationMetadata<TBaseModel extends BaseModel>(
|
|
415
|
+
modelType: { new (): TBaseModel },
|
|
416
|
+
propertyPath: string,
|
|
417
|
+
): {
|
|
418
|
+
joinTableName: string;
|
|
419
|
+
ownerColumnName: string;
|
|
420
|
+
relationColumnName: string;
|
|
421
|
+
} | null {
|
|
422
|
+
if (!PostgresAppInstance.isConnected()) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const dataSource: DataSource | null = PostgresAppInstance.getDataSource();
|
|
427
|
+
|
|
428
|
+
if (!dataSource) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let entityMetadata: EntityMetadata | undefined;
|
|
433
|
+
try {
|
|
434
|
+
entityMetadata = dataSource.getMetadata(modelType);
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!entityMetadata) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const relation: RelationMetadata | undefined =
|
|
444
|
+
entityMetadata.findRelationWithPropertyPath(propertyPath);
|
|
445
|
+
|
|
446
|
+
if (!relation || !relation.isManyToMany) {
|
|
447
|
+
return null;
|
|
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
|
+
*/
|
|
454
|
+
const owningRelation: RelationMetadata = relation.isOwning
|
|
455
|
+
? relation
|
|
456
|
+
: relation.inverseRelation ?? relation;
|
|
457
|
+
|
|
458
|
+
const joinTableName: string | undefined =
|
|
459
|
+
owningRelation.junctionEntityMetadata?.tableName;
|
|
460
|
+
|
|
461
|
+
if (!joinTableName) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
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
|
+
*/
|
|
469
|
+
const ownerColumns: Array<any> = relation.isOwning
|
|
470
|
+
? owningRelation.joinColumns
|
|
471
|
+
: owningRelation.inverseJoinColumns;
|
|
472
|
+
const relationColumns: Array<any> = relation.isOwning
|
|
473
|
+
? owningRelation.inverseJoinColumns
|
|
474
|
+
: owningRelation.joinColumns;
|
|
475
|
+
|
|
476
|
+
const ownerColumnName: string | undefined = ownerColumns[0]?.databaseName;
|
|
477
|
+
const relationColumnName: string | undefined =
|
|
478
|
+
relationColumns[0]?.databaseName;
|
|
479
|
+
|
|
480
|
+
if (!ownerColumnName || !relationColumnName) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
joinTableName,
|
|
486
|
+
ownerColumnName,
|
|
487
|
+
relationColumnName,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
240
490
|
}
|
|
@@ -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!;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import BadDataException from "../Exception/BadDataException";
|
|
2
|
+
import { JSONObject, ObjectType } from "../JSON";
|
|
3
|
+
import QueryOperator from "./QueryOperator";
|
|
4
|
+
|
|
5
|
+
export default class EndsWith<T extends string> extends QueryOperator<T> {
|
|
6
|
+
private _value!: T;
|
|
7
|
+
|
|
8
|
+
public get value(): T {
|
|
9
|
+
return this._value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public set value(v: T) {
|
|
13
|
+
this._value = v;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public constructor(value: T) {
|
|
17
|
+
super();
|
|
18
|
+
this.value = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public override toString(): T {
|
|
22
|
+
return this.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public override toJSON(): JSONObject {
|
|
26
|
+
return {
|
|
27
|
+
_type: ObjectType.EndsWith,
|
|
28
|
+
value: (this as EndsWith<T>).toString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static override fromJSON<T extends string>(
|
|
33
|
+
json: JSONObject,
|
|
34
|
+
): EndsWith<T> {
|
|
35
|
+
if (json["_type"] === ObjectType.EndsWith) {
|
|
36
|
+
return new EndsWith<T>((json["value"] as T) || ("" as T));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
|
|
40
|
+
}
|
|
41
|
+
}
|