@oneuptime/common 11.3.20 → 11.3.22
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/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
- package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
- package/Models/AnalyticsModels/Log.ts +1 -0
- package/Models/AnalyticsModels/Metric.ts +1 -0
- package/Models/AnalyticsModels/MonitorLog.ts +1 -0
- package/Models/AnalyticsModels/Span.ts +1 -0
- package/Models/DatabaseModels/Alert.ts +100 -0
- package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
- package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
- package/Models/DatabaseModels/AlertSeverity.ts +2 -0
- package/Models/DatabaseModels/AlertState.ts +2 -0
- package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
- package/Models/DatabaseModels/GlobalConfig.ts +19 -0
- package/Models/DatabaseModels/Incident.ts +100 -0
- package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
- package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
- package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/Label.ts +2 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
- package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
- package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
- package/Server/API/DashboardAPI.ts +221 -0
- package/Server/API/EnterpriseLicenseAPI.ts +279 -12
- package/Server/API/GlobalConfigAPI.ts +81 -16
- package/Server/API/StatusPageAPI.ts +894 -791
- package/Server/EnvironmentConfig.ts +9 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Services/AlertReminderRuleService.ts +253 -0
- package/Server/Services/AlertService.ts +106 -0
- package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
- package/Server/Services/IncidentReminderRuleService.ts +256 -0
- package/Server/Services/IncidentService.ts +143 -0
- package/Server/Services/Index.ts +8 -0
- package/Server/Services/IoTDeviceService.ts +99 -4
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
- package/Server/Services/ScheduledMaintenanceService.ts +127 -0
- package/Server/Services/StatusPageService.ts +104 -45
- package/Server/Services/TelemetryExceptionService.ts +123 -0
- package/Server/Services/UserNotificationSettingService.ts +24 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
- package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
- package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
- package/ServiceRoute.ts +4 -0
- package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
- package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
- package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
- package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
- package/Types/Email/EmailTemplateType.ts +3 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
- package/Types/Monitor/MonitorStep.ts +32 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
- package/Types/Permission.ts +144 -0
- package/Types/Reminder/ReminderStopState.ts +6 -0
- package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
- package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Alert.js +103 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +18 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +103 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
- package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Label.js +2 -0
- package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +166 -16
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +74 -31
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/AlertService.js +87 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
- package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
- package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +116 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +8 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +66 -4
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +91 -43
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
- package/build/dist/ServiceRoute.js +1 -0
- package/build/dist/ServiceRoute.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +3 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +25 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/Permission.js +126 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
- package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
- package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ import MonitorCriteriaExpectationBuilder from "./MonitorCriteriaExpectationBuild
|
|
|
29
29
|
import MetricMonitorResponse from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
|
30
30
|
import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
|
|
31
31
|
import MetricFormulaConfigData from "../../../Types/Metrics/MetricFormulaConfigData";
|
|
32
|
+
import MetricsViewConfig from "../../../Types/Metrics/MetricsViewConfig";
|
|
32
33
|
import MetricUnitUtil from "../../../Utils/MetricUnitUtil";
|
|
33
34
|
|
|
34
35
|
export default class MonitorCriteriaObservationBuilder {
|
|
@@ -1208,12 +1209,13 @@ export default class MonitorCriteriaObservationBuilder {
|
|
|
1208
1209
|
return input.values;
|
|
1209
1210
|
}
|
|
1210
1211
|
|
|
1212
|
+
const metricViewConfig: MetricsViewConfig | undefined =
|
|
1213
|
+
MonitorStep.getMetricsViewConfig(input.monitorStep);
|
|
1214
|
+
|
|
1211
1215
|
const queryConfigs: Array<MetricQueryConfigData> =
|
|
1212
|
-
|
|
1213
|
-
[];
|
|
1216
|
+
metricViewConfig?.queryConfigs || [];
|
|
1214
1217
|
const formulaConfigs: Array<MetricFormulaConfigData> =
|
|
1215
|
-
|
|
1216
|
-
[];
|
|
1218
|
+
metricViewConfig?.formulaConfigs || [];
|
|
1217
1219
|
|
|
1218
1220
|
const alias: string | null = input.alias;
|
|
1219
1221
|
const matchedQuery: MetricQueryConfigData | undefined = alias
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
1
2
|
import OneUptimeDate from "../../../Types/Date";
|
|
2
3
|
import { JSONArray, JSONObject, JSONValue } from "../../../Types/JSON";
|
|
3
4
|
import logger from "../Logger";
|
|
@@ -109,6 +110,29 @@ function toNumberOrNull(value: unknown): number | null {
|
|
|
109
110
|
return null;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/*
|
|
114
|
+
* A device with a future-skewed clock must not be allowed to stamp a
|
|
115
|
+
* future observedAt: it becomes lastSeenAt / metricsUpdatedAt, and the
|
|
116
|
+
* `>=` dominance guards in IoTDeviceService would then reject every
|
|
117
|
+
* legitimate (present-time) update until the wall clock catches up to
|
|
118
|
+
* the skew. Anything more than this far ahead of the ingest clock is
|
|
119
|
+
* folded back to "now" at ingest time.
|
|
120
|
+
*/
|
|
121
|
+
export const IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES: number = 5;
|
|
122
|
+
|
|
123
|
+
export function clampIoTTimestamp(date: Date, nowOverride?: Date): Date {
|
|
124
|
+
const now: Date = nowOverride || OneUptimeDate.getCurrentDate();
|
|
125
|
+
const maxAllowedMs: number =
|
|
126
|
+
now.getTime() + IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES * 60 * 1000;
|
|
127
|
+
if (date.getTime() > maxAllowedMs) {
|
|
128
|
+
logger.debug(
|
|
129
|
+
`IoT timestamp ${date.toISOString()} is beyond the ${IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES}m future-skew tolerance; clamping to ingest time.`,
|
|
130
|
+
);
|
|
131
|
+
return now;
|
|
132
|
+
}
|
|
133
|
+
return date;
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
/*
|
|
113
137
|
* Same fall-back-to-now parse contract as the ingest service's
|
|
114
138
|
* safeParseUnixNano — only the Date is needed on the snapshot path.
|
|
@@ -200,9 +224,11 @@ export function bufferIoTSnapshotMetric(data: {
|
|
|
200
224
|
return;
|
|
201
225
|
}
|
|
202
226
|
|
|
203
|
-
const observedAt: Date =
|
|
204
|
-
|
|
205
|
-
|
|
227
|
+
const observedAt: Date = clampIoTTimestamp(
|
|
228
|
+
parseUnixNanoToDate(
|
|
229
|
+
data.datapoint["timeUnixNano"] as string | number | undefined,
|
|
230
|
+
"iot snapshot timeUnixNano",
|
|
231
|
+
),
|
|
206
232
|
);
|
|
207
233
|
|
|
208
234
|
const dpAttributes: JSONArray =
|
|
@@ -214,14 +240,26 @@ export function bufferIoTSnapshotMetric(data: {
|
|
|
214
240
|
);
|
|
215
241
|
|
|
216
242
|
// Identity lives in the `device.id` datapoint label.
|
|
217
|
-
|
|
218
|
-
dpAttributes,
|
|
219
|
-
"device.id",
|
|
220
|
-
);
|
|
243
|
+
let externalId: string | null = getStringAttribute(dpAttributes, "device.id");
|
|
221
244
|
if (!externalId) {
|
|
222
245
|
return;
|
|
223
246
|
}
|
|
224
247
|
|
|
248
|
+
/*
|
|
249
|
+
* The inventory identity columns are ShortText (100 chars). Truncate
|
|
250
|
+
* over-long ids at ingest so the fold key, the inventory upsert and
|
|
251
|
+
* the latest-metric mirror all agree on the same (truncated)
|
|
252
|
+
* identity — IoTDeviceService also sanitizes before building its
|
|
253
|
+
* INSERT chunks, so one malformed device can never fail the chunk
|
|
254
|
+
* and drop the other rows with it.
|
|
255
|
+
*/
|
|
256
|
+
if (externalId.length > ColumnLength.ShortText) {
|
|
257
|
+
logger.warn(
|
|
258
|
+
`IoT device.id exceeds ${ColumnLength.ShortText} chars; truncating to "${externalId.substring(0, ColumnLength.ShortText)}".`,
|
|
259
|
+
);
|
|
260
|
+
externalId = externalId.substring(0, ColumnLength.ShortText);
|
|
261
|
+
}
|
|
262
|
+
|
|
225
263
|
/*
|
|
226
264
|
* Kind/type/firmware: read from the iot.device.* attributes the
|
|
227
265
|
* device SDK stamps; kind falls back to "Device" so inventory still
|
|
@@ -35,6 +35,8 @@ const templateDashboardLinkVariableMap: Partial<
|
|
|
35
35
|
[WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "incident_link",
|
|
36
36
|
[WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "incident_link",
|
|
37
37
|
[WhatsAppTemplateIds.IncidentMemberAddedNotification]: "incident_link",
|
|
38
|
+
[WhatsAppTemplateIds.IncidentReminderOwnerNotification]: "incident_link",
|
|
39
|
+
[WhatsAppTemplateIds.AlertReminderOwnerNotification]: "alert_link",
|
|
38
40
|
[WhatsAppTemplateIds.MonitorOwnerAddedNotification]: "monitor_link",
|
|
39
41
|
[WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: "monitor_link",
|
|
40
42
|
[WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: "monitor_link",
|
|
@@ -48,6 +50,8 @@ const templateDashboardLinkVariableMap: Partial<
|
|
|
48
50
|
"maintenance_link",
|
|
49
51
|
[WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification]:
|
|
50
52
|
"maintenance_link",
|
|
53
|
+
[WhatsAppTemplateIds.ScheduledMaintenanceReminderOwnerNotification]:
|
|
54
|
+
"event_link",
|
|
51
55
|
[WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification]:
|
|
52
56
|
"status_page_link",
|
|
53
57
|
[WhatsAppTemplateIds.StatusPageCreatedOwnerNotification]: "status_page_link",
|
|
@@ -78,6 +82,10 @@ const templateIdByEventType: Record<
|
|
|
78
82
|
WhatsAppTemplateIds.IncidentOwnerAddedNotification,
|
|
79
83
|
[NotificationSettingEventType.SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION]:
|
|
80
84
|
WhatsAppTemplateIds.IncidentMemberAddedNotification,
|
|
85
|
+
[NotificationSettingEventType.SEND_INCIDENT_REMINDER_OWNER_NOTIFICATION]:
|
|
86
|
+
WhatsAppTemplateIds.IncidentReminderOwnerNotification,
|
|
87
|
+
[NotificationSettingEventType.SEND_ALERT_REMINDER_OWNER_NOTIFICATION]:
|
|
88
|
+
WhatsAppTemplateIds.AlertReminderOwnerNotification,
|
|
81
89
|
[NotificationSettingEventType.SEND_ALERT_CREATED_OWNER_NOTIFICATION]:
|
|
82
90
|
WhatsAppTemplateIds.AlertCreatedOwnerNotification,
|
|
83
91
|
[NotificationSettingEventType.SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION]:
|
|
@@ -124,6 +132,8 @@ const templateIdByEventType: Record<
|
|
|
124
132
|
WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification,
|
|
125
133
|
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION]:
|
|
126
134
|
WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification,
|
|
135
|
+
[NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_REMINDER_OWNER_NOTIFICATION]:
|
|
136
|
+
WhatsAppTemplateIds.ScheduledMaintenanceReminderOwnerNotification,
|
|
127
137
|
[NotificationSettingEventType.SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION]:
|
|
128
138
|
WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification,
|
|
129
139
|
[NotificationSettingEventType.SEND_STATUS_PAGE_CREATED_OWNER_NOTIFICATION]:
|
package/ServiceRoute.ts
CHANGED
|
@@ -39,3 +39,7 @@ export const DocsRoute: Route = new Route("/docs");
|
|
|
39
39
|
export const StatusPageApiRoute: Route = new Route("/status-page-api");
|
|
40
40
|
|
|
41
41
|
export const PublicDashboardRoute: Route = new Route("/public-dashboard");
|
|
42
|
+
|
|
43
|
+
export const PublicDashboardApiRoute: Route = new Route(
|
|
44
|
+
"/public-dashboard-api",
|
|
45
|
+
);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import IoTDeviceService, {
|
|
2
|
+
IoTDeviceLatestMetric,
|
|
3
|
+
ParsedIoTDevice,
|
|
4
|
+
} from "../../../Server/Services/IoTDeviceService";
|
|
5
|
+
import { IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES } from "../../../Server/Utils/Telemetry/IoTSnapshotScan";
|
|
6
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Malformed-device hardening for the IoTDevice inventory write path.
|
|
10
|
+
* The service builds raw parameterized SQL against the TypeORM manager
|
|
11
|
+
* — these tests mock the query runner (no Postgres) and lock in:
|
|
12
|
+
*
|
|
13
|
+
* - ShortText values (externalId and friends) truncate to the
|
|
14
|
+
* 100-char column limit BEFORE the chunk is built, so one bad
|
|
15
|
+
* device can never fail the whole 500-row INSERT and drop the
|
|
16
|
+
* other 499 with it,
|
|
17
|
+
* - identities that collide after truncation dedupe to the newest
|
|
18
|
+
* row (two VALUES rows on one conflict target abort the statement),
|
|
19
|
+
* - a future-skewed lastSeenAt / observedAt clamps to ingest time so
|
|
20
|
+
* the `>=` dominance guards can't get wedged by one bogus device
|
|
21
|
+
* clock,
|
|
22
|
+
* - the latest-metric mirror truncates identity the SAME way the
|
|
23
|
+
* upsert does, so the UPDATE still finds the row the upsert wrote.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const PROJECT_ID: ObjectID = ObjectID.generate();
|
|
27
|
+
const FLEET_ID: ObjectID = ObjectID.generate();
|
|
28
|
+
const SHORT_TEXT_LIMIT: number = 100;
|
|
29
|
+
const UPSERT_PARAMS_PER_ROW: number = 11;
|
|
30
|
+
|
|
31
|
+
type QueryCall = [string, Array<unknown>];
|
|
32
|
+
|
|
33
|
+
function mockQueryRunner(result: unknown = []): jest.Mock {
|
|
34
|
+
const query: jest.Mock = jest.fn().mockResolvedValue(result);
|
|
35
|
+
jest
|
|
36
|
+
.spyOn(IoTDeviceService, "getRepository")
|
|
37
|
+
.mockReturnValue({ manager: { query } } as any);
|
|
38
|
+
return query;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parsedDevice(
|
|
42
|
+
overrides: Partial<ParsedIoTDevice> = {},
|
|
43
|
+
): ParsedIoTDevice {
|
|
44
|
+
return {
|
|
45
|
+
kind: "Sensor",
|
|
46
|
+
externalId: "sensor-1",
|
|
47
|
+
name: "greenhouse-sensor",
|
|
48
|
+
deviceType: "temperature",
|
|
49
|
+
firmwareVersion: "1.2.3",
|
|
50
|
+
isUp: true,
|
|
51
|
+
uptimeSeconds: 3600,
|
|
52
|
+
lastSeenAt: new Date("2026-06-13T00:00:00.000Z"),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function latestMetric(
|
|
58
|
+
overrides: Partial<IoTDeviceLatestMetric> = {},
|
|
59
|
+
): IoTDeviceLatestMetric {
|
|
60
|
+
return {
|
|
61
|
+
kind: "Sensor",
|
|
62
|
+
externalId: "sensor-1",
|
|
63
|
+
cpuPercent: 12.5,
|
|
64
|
+
memoryBytes: 1024,
|
|
65
|
+
maxMemoryBytes: 4096,
|
|
66
|
+
memoryPercent: 25,
|
|
67
|
+
batteryPercent: 88,
|
|
68
|
+
signalStrengthDbm: -61,
|
|
69
|
+
temperatureCelsius: 21.4,
|
|
70
|
+
observedAt: new Date("2026-06-13T00:00:00.000Z"),
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
jest.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("IoTDeviceService.bulkUpsert — ShortText sanitation", () => {
|
|
80
|
+
test("truncates an over-long externalId instead of failing the chunk", async () => {
|
|
81
|
+
const query: jest.Mock = mockQueryRunner();
|
|
82
|
+
const longId: string = "x".repeat(150);
|
|
83
|
+
|
|
84
|
+
await IoTDeviceService.bulkUpsert({
|
|
85
|
+
projectId: PROJECT_ID,
|
|
86
|
+
iotFleetId: FLEET_ID,
|
|
87
|
+
devices: [
|
|
88
|
+
parsedDevice({ externalId: longId }),
|
|
89
|
+
parsedDevice({ externalId: "sensor-good" }),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
94
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
95
|
+
|
|
96
|
+
// Both rows made it into the one chunk — the bad id dropped nobody.
|
|
97
|
+
expect(params).toHaveLength(2 * UPSERT_PARAMS_PER_ROW);
|
|
98
|
+
expect(params).toContain("x".repeat(SHORT_TEXT_LIMIT));
|
|
99
|
+
expect(params).toContain("sensor-good");
|
|
100
|
+
expect(params).not.toContain(longId);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("truncates over-long kind/name/deviceType/firmwareVersion values", async () => {
|
|
104
|
+
const query: jest.Mock = mockQueryRunner();
|
|
105
|
+
|
|
106
|
+
await IoTDeviceService.bulkUpsert({
|
|
107
|
+
projectId: PROJECT_ID,
|
|
108
|
+
iotFleetId: FLEET_ID,
|
|
109
|
+
devices: [
|
|
110
|
+
parsedDevice({
|
|
111
|
+
kind: "k".repeat(140),
|
|
112
|
+
name: "n".repeat(140),
|
|
113
|
+
deviceType: "t".repeat(140),
|
|
114
|
+
firmwareVersion: "f".repeat(140),
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
120
|
+
expect(params).toContain("k".repeat(SHORT_TEXT_LIMIT));
|
|
121
|
+
expect(params).toContain("n".repeat(SHORT_TEXT_LIMIT));
|
|
122
|
+
expect(params).toContain("t".repeat(SHORT_TEXT_LIMIT));
|
|
123
|
+
expect(params).toContain("f".repeat(SHORT_TEXT_LIMIT));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("identities colliding after truncation dedupe to the newest row", async () => {
|
|
127
|
+
const query: jest.Mock = mockQueryRunner();
|
|
128
|
+
const shared: string = "z".repeat(SHORT_TEXT_LIMIT);
|
|
129
|
+
const older: Date = new Date("2026-06-13T00:00:00.000Z");
|
|
130
|
+
const newer: Date = new Date("2026-06-13T00:05:00.000Z");
|
|
131
|
+
|
|
132
|
+
await IoTDeviceService.bulkUpsert({
|
|
133
|
+
projectId: PROJECT_ID,
|
|
134
|
+
iotFleetId: FLEET_ID,
|
|
135
|
+
devices: [
|
|
136
|
+
parsedDevice({
|
|
137
|
+
externalId: `${shared}-alpha`,
|
|
138
|
+
name: "older-device",
|
|
139
|
+
lastSeenAt: older,
|
|
140
|
+
}),
|
|
141
|
+
parsedDevice({
|
|
142
|
+
externalId: `${shared}-bravo`,
|
|
143
|
+
name: "newer-device",
|
|
144
|
+
lastSeenAt: newer,
|
|
145
|
+
}),
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
150
|
+
|
|
151
|
+
// One conflict target → exactly one VALUES row, the newest one.
|
|
152
|
+
expect(params).toHaveLength(UPSERT_PARAMS_PER_ROW);
|
|
153
|
+
expect(params).toContain(shared);
|
|
154
|
+
expect(params).toContain("newer-device");
|
|
155
|
+
expect(params).not.toContain("older-device");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("skips rows whose externalId is empty", async () => {
|
|
159
|
+
const query: jest.Mock = mockQueryRunner();
|
|
160
|
+
|
|
161
|
+
await IoTDeviceService.bulkUpsert({
|
|
162
|
+
projectId: PROJECT_ID,
|
|
163
|
+
iotFleetId: FLEET_ID,
|
|
164
|
+
devices: [parsedDevice({ externalId: "" })],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(query).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("IoTDeviceService.bulkUpsert — future clock-skew clamping", () => {
|
|
172
|
+
test("clamps a lastSeenAt beyond the skew tolerance to ingest time", async () => {
|
|
173
|
+
const query: jest.Mock = mockQueryRunner();
|
|
174
|
+
const before: number = Date.now();
|
|
175
|
+
|
|
176
|
+
await IoTDeviceService.bulkUpsert({
|
|
177
|
+
projectId: PROJECT_ID,
|
|
178
|
+
iotFleetId: FLEET_ID,
|
|
179
|
+
devices: [
|
|
180
|
+
parsedDevice({
|
|
181
|
+
lastSeenAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const after: number = Date.now();
|
|
187
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
188
|
+
const lastSeenAt: Date = params[9] as Date;
|
|
189
|
+
|
|
190
|
+
expect(lastSeenAt.getTime()).toBeGreaterThanOrEqual(before);
|
|
191
|
+
expect(lastSeenAt.getTime()).toBeLessThanOrEqual(after);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("keeps a lastSeenAt within the skew tolerance untouched", async () => {
|
|
195
|
+
const query: jest.Mock = mockQueryRunner();
|
|
196
|
+
const slightlyAhead: Date = new Date(
|
|
197
|
+
Date.now() + (IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES - 1) * 60 * 1000,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
await IoTDeviceService.bulkUpsert({
|
|
201
|
+
projectId: PROJECT_ID,
|
|
202
|
+
iotFleetId: FLEET_ID,
|
|
203
|
+
devices: [parsedDevice({ lastSeenAt: slightlyAhead })],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
207
|
+
expect(params[9]).toEqual(slightlyAhead);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("IoTDeviceService.bulkUpdateLatestMetrics — sanitation matches the upsert", () => {
|
|
212
|
+
test("truncates an over-long externalId to the same identity the upsert wrote", async () => {
|
|
213
|
+
const query: jest.Mock = mockQueryRunner();
|
|
214
|
+
const longId: string = "x".repeat(150);
|
|
215
|
+
|
|
216
|
+
await IoTDeviceService.bulkUpdateLatestMetrics({
|
|
217
|
+
projectId: PROJECT_ID,
|
|
218
|
+
iotFleetId: FLEET_ID,
|
|
219
|
+
metrics: [latestMetric({ externalId: longId })],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
223
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
224
|
+
|
|
225
|
+
// Params: projectId, fleetId, then kind, externalId, ... per row.
|
|
226
|
+
expect(params[3]).toBe("x".repeat(SHORT_TEXT_LIMIT));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("clamps a future observedAt beyond the skew tolerance", async () => {
|
|
230
|
+
const query: jest.Mock = mockQueryRunner();
|
|
231
|
+
const before: number = Date.now();
|
|
232
|
+
|
|
233
|
+
await IoTDeviceService.bulkUpdateLatestMetrics({
|
|
234
|
+
projectId: PROJECT_ID,
|
|
235
|
+
iotFleetId: FLEET_ID,
|
|
236
|
+
metrics: [
|
|
237
|
+
latestMetric({
|
|
238
|
+
observedAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
239
|
+
}),
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const after: number = Date.now();
|
|
244
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
245
|
+
const observedAt: Date = params[11] as Date;
|
|
246
|
+
|
|
247
|
+
expect(observedAt.getTime()).toBeGreaterThanOrEqual(before);
|
|
248
|
+
expect(observedAt.getTime()).toBeLessThanOrEqual(after);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("identities colliding after truncation dedupe to the newest observation", async () => {
|
|
252
|
+
const query: jest.Mock = mockQueryRunner();
|
|
253
|
+
const shared: string = "z".repeat(SHORT_TEXT_LIMIT);
|
|
254
|
+
|
|
255
|
+
await IoTDeviceService.bulkUpdateLatestMetrics({
|
|
256
|
+
projectId: PROJECT_ID,
|
|
257
|
+
iotFleetId: FLEET_ID,
|
|
258
|
+
metrics: [
|
|
259
|
+
latestMetric({
|
|
260
|
+
externalId: `${shared}-alpha`,
|
|
261
|
+
batteryPercent: 10,
|
|
262
|
+
observedAt: new Date("2026-06-13T00:00:00.000Z"),
|
|
263
|
+
}),
|
|
264
|
+
latestMetric({
|
|
265
|
+
externalId: `${shared}-bravo`,
|
|
266
|
+
batteryPercent: 90,
|
|
267
|
+
observedAt: new Date("2026-06-13T00:05:00.000Z"),
|
|
268
|
+
}),
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const [, params] = query.mock.calls[0] as QueryCall;
|
|
273
|
+
|
|
274
|
+
// 2 shared params + one 9-param row (kind..temp) + observedAt.
|
|
275
|
+
expect(params).toHaveLength(2 + 10);
|
|
276
|
+
expect(params[3]).toBe(shared);
|
|
277
|
+
expect(params).toContain(90);
|
|
278
|
+
expect(params).not.toContain(10);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -672,6 +672,55 @@ describe("StatementGenerator", () => {
|
|
|
672
672
|
expect(statement.query).toBe("");
|
|
673
673
|
expect(statement.query_params).toStrictEqual({});
|
|
674
674
|
});
|
|
675
|
+
|
|
676
|
+
test("emits NOT hasAny(...) for IncludesNone on an Array(String) column", () => {
|
|
677
|
+
const statement: Statement = arrayGenerator.toWhereStatement({
|
|
678
|
+
entityKeys: new IncludesNone(["210dac24142f1baa"]),
|
|
679
|
+
} as any);
|
|
680
|
+
expect(statement.query).toBe(
|
|
681
|
+
"AND NOT hasAny({p0:Identifier}, {p1:Array(String)})",
|
|
682
|
+
);
|
|
683
|
+
expect(statement.query_params).toStrictEqual({
|
|
684
|
+
p0: "entityKeys",
|
|
685
|
+
p1: ["210dac24142f1baa"],
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("drops empty IncludesNone instead of NOT hasAny(col, [])", () => {
|
|
690
|
+
const statement: Statement = arrayGenerator.toWhereStatement({
|
|
691
|
+
entityKeys: new IncludesNone([]),
|
|
692
|
+
} as any);
|
|
693
|
+
expect(statement.query).toBe("");
|
|
694
|
+
expect(statement.query_params).toStrictEqual({});
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe("scalar IncludesNone exclusion", () => {
|
|
699
|
+
/*
|
|
700
|
+
* "is none of" on a scalar column compiles to `col NOT IN (...)` —
|
|
701
|
+
* the exception monitor uses this to exclude occurrences of
|
|
702
|
+
* resolved/archived exception groups by fingerprint.
|
|
703
|
+
*/
|
|
704
|
+
test("emits NOT IN (...) for IncludesNone on a Text column", () => {
|
|
705
|
+
const statement: Statement = generator.toWhereStatement({
|
|
706
|
+
column_1: new IncludesNone(["<fingerprint-1>", "<fingerprint-2>"]),
|
|
707
|
+
} as any);
|
|
708
|
+
expect(statement.query).toBe(
|
|
709
|
+
"AND {p0:Identifier} NOT IN {p1:Array(String)}",
|
|
710
|
+
);
|
|
711
|
+
expect(statement.query_params).toStrictEqual({
|
|
712
|
+
p0: "column_1",
|
|
713
|
+
p1: ["<fingerprint-1>", "<fingerprint-2>"],
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("drops empty IncludesNone instead of producing NOT IN ()", () => {
|
|
718
|
+
const statement: Statement = generator.toWhereStatement({
|
|
719
|
+
column_1: new IncludesNone([]),
|
|
720
|
+
} as any);
|
|
721
|
+
expect(statement.query).toBe("");
|
|
722
|
+
expect(statement.query_params).toStrictEqual({});
|
|
723
|
+
});
|
|
675
724
|
});
|
|
676
725
|
|
|
677
726
|
describe("entityScope synthetic key", () => {
|