@oneuptime/common 8.0.5159 → 8.0.5163

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.
@@ -0,0 +1,479 @@
1
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
2
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
3
+ import RestrictionTimes, {
4
+ RestrictionType,
5
+ WeeklyResctriction,
6
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
7
+ import Recurring from "../../../Types/Events/Recurring";
8
+ import OneUptimeDate from "../../../Types/Date";
9
+ import User from "../../../Models/DatabaseModels/User";
10
+ import EventInterval from "../../../Types/Events/EventInterval";
11
+ import DayOfWeek, { DayOfWeekUtil } from "../../../Types/Day/DayOfWeek";
12
+
13
+ // Helper to create a user model with id only.
14
+ function user(id: string): User {
15
+ return {
16
+ id: {
17
+ toString: () => {
18
+ return id;
19
+ },
20
+ } as any,
21
+ } as User;
22
+ }
23
+
24
+ function buildLayerProps(data: {
25
+ users: string[];
26
+ start: Date;
27
+ handoff: Date;
28
+ restriction?: { type: RestrictionType; start?: string; end?: string };
29
+ weeklyRestrictions?: Array<{
30
+ startDay: DayOfWeek;
31
+ endDay: DayOfWeek;
32
+ start: string; // HH:mm
33
+ end: string; // HH:mm
34
+ }>;
35
+ rotation?: { intervalType: EventInterval; intervalCount: number };
36
+ }): LayerProps {
37
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
38
+
39
+ if (data.restriction) {
40
+ restrictionTimes.restictionType = data.restriction.type;
41
+ if (
42
+ data.restriction.type === RestrictionType.Daily &&
43
+ data.restriction.start &&
44
+ data.restriction.end
45
+ ) {
46
+ restrictionTimes.dayRestrictionTimes = {
47
+ startTime: OneUptimeDate.getDateWithCustomTime({
48
+ hours: parseInt(data.restriction.start.split(":")[0] || "0"),
49
+ minutes: parseInt(data.restriction.start.split(":")[1] || "0"),
50
+ seconds: 0,
51
+ }),
52
+ endTime: OneUptimeDate.getDateWithCustomTime({
53
+ hours: parseInt(data.restriction.end.split(":")[0] || "0"),
54
+ minutes: parseInt(data.restriction.end.split(":")[1] || "0"),
55
+ seconds: 0,
56
+ }),
57
+ };
58
+ }
59
+ } else if (data.weeklyRestrictions && data.weeklyRestrictions.length > 0) {
60
+ restrictionTimes.restictionType = RestrictionType.Weekly;
61
+ const weekly: Array<WeeklyResctriction> = [];
62
+ // Base week anchor (start of week for provided start date)
63
+ const baseWeekStart: Date = OneUptimeDate.getStartOfTheWeek(data.start);
64
+ const baseWeekDay: DayOfWeek = OneUptimeDate.getDayOfWeek(baseWeekStart);
65
+ const baseWeekDayNumber: number =
66
+ DayOfWeekUtil.getNumberOfDayOfWeek(baseWeekDay);
67
+
68
+ for (const r of data.weeklyRestrictions) {
69
+ const desiredStartDayNum: number = DayOfWeekUtil.getNumberOfDayOfWeek(
70
+ r.startDay,
71
+ );
72
+ const desiredEndDayNum: number = DayOfWeekUtil.getNumberOfDayOfWeek(
73
+ r.endDay,
74
+ );
75
+
76
+ const startOffsetDays: number = desiredStartDayNum - baseWeekDayNumber;
77
+ const endOffsetDays: number = desiredEndDayNum - baseWeekDayNumber;
78
+
79
+ const startDate: Date = OneUptimeDate.addRemoveDays(
80
+ baseWeekStart,
81
+ startOffsetDays,
82
+ );
83
+ const endDate: Date = OneUptimeDate.addRemoveDays(
84
+ baseWeekStart,
85
+ endOffsetDays,
86
+ );
87
+
88
+ const startTime: Date = OneUptimeDate.keepTimeButMoveDay(
89
+ OneUptimeDate.getDateWithCustomTime({
90
+ hours: parseInt(r.start.split(":")[0] || "0"),
91
+ minutes: parseInt(r.start.split(":")[1] || "0"),
92
+ seconds: 0,
93
+ }),
94
+ startDate,
95
+ );
96
+
97
+ const endTime: Date = OneUptimeDate.keepTimeButMoveDay(
98
+ OneUptimeDate.getDateWithCustomTime({
99
+ hours: parseInt(r.end.split(":")[0] || "0"),
100
+ minutes: parseInt(r.end.split(":")[1] || "0"),
101
+ seconds: 0,
102
+ }),
103
+ endDate,
104
+ );
105
+
106
+ weekly.push({
107
+ startDay: r.startDay,
108
+ endDay: r.endDay,
109
+ startTime,
110
+ endTime,
111
+ });
112
+ }
113
+
114
+ restrictionTimes.weeklyRestrictionTimes = weekly;
115
+ } else {
116
+ restrictionTimes.restictionType = RestrictionType.None;
117
+ restrictionTimes.dayRestrictionTimes = null;
118
+ }
119
+
120
+ const rotation: Recurring = data.rotation
121
+ ? Recurring.fromJSON({
122
+ _type: "Recurring",
123
+ value: {
124
+ intervalType: data.rotation.intervalType,
125
+ intervalCount: {
126
+ _type: "PositiveNumber",
127
+ value: data.rotation.intervalCount,
128
+ },
129
+ },
130
+ } as any)
131
+ : Recurring.fromJSON({
132
+ _type: "Recurring",
133
+ value: {
134
+ intervalType: EventInterval.Day,
135
+ intervalCount: { _type: "PositiveNumber", value: 1 },
136
+ },
137
+ } as any);
138
+
139
+ return {
140
+ users: data.users.map(user),
141
+ startDateTimeOfLayer: data.start,
142
+ handOffTime: data.handoff,
143
+ restrictionTimes,
144
+ rotation,
145
+ };
146
+ }
147
+
148
+ describe("LayerUtil getEvents - Daily Restrictions", () => {
149
+ test("Should return full-day events when no restriction", () => {
150
+ const util: LayerUtil = new LayerUtil();
151
+ const start: Date = OneUptimeDate.getStartOfDay(new Date());
152
+ const end: Date = OneUptimeDate.addRemoveDays(start, 1); // one day calendar
153
+
154
+ const layer: LayerProps = buildLayerProps({
155
+ users: ["u1"],
156
+ start: start,
157
+ handoff: OneUptimeDate.addRemoveDays(start, 10),
158
+ });
159
+
160
+ const events: Array<CalendarEvent> = util.getEvents({
161
+ ...layer,
162
+ calendarStartDate: start,
163
+ calendarEndDate: end,
164
+ });
165
+
166
+ expect(events.length).toBe(1);
167
+ const only: CalendarEvent = events[0]!;
168
+ expect(only.start.getTime()).toBe(start.getTime());
169
+ expect(only.end.getTime()).toBe(end.getTime());
170
+ });
171
+
172
+ test("Should trim to same-day restriction window (11:00-23:00)", () => {
173
+ const util: LayerUtil = new LayerUtil();
174
+ const start: Date = OneUptimeDate.getStartOfDay(new Date());
175
+ const calendarEnd: Date = OneUptimeDate.addRemoveDays(start, 1);
176
+
177
+ const layer: LayerProps = buildLayerProps({
178
+ users: ["u1"],
179
+ start: start,
180
+ handoff: OneUptimeDate.addRemoveDays(start, 2),
181
+ restriction: {
182
+ type: RestrictionType.Daily,
183
+ start: "11:00",
184
+ end: "23:00",
185
+ },
186
+ });
187
+
188
+ const events: Array<CalendarEvent> = util.getEvents({
189
+ ...layer,
190
+ calendarStartDate: start,
191
+ calendarEndDate: calendarEnd,
192
+ });
193
+
194
+ expect(events.length).toBe(1);
195
+ const ev: CalendarEvent = events[0]!;
196
+ expect(OneUptimeDate.getLocalHourAndMinuteFromDate(ev.start)).toBe("11:00");
197
+ expect(OneUptimeDate.getLocalHourAndMinuteFromDate(ev.end)).toBe("23:00");
198
+ });
199
+
200
+ test("Should produce two segments for overnight window (23:00-11:00 next day)", () => {
201
+ const util: LayerUtil = new LayerUtil();
202
+ const todayStart: Date = OneUptimeDate.getStartOfDay(new Date());
203
+ // Extend calendar to cover next day morning (till at least 12:00) so both segments can appear.
204
+ const calendarEnd: Date = OneUptimeDate.addRemoveHours(todayStart, 36); // 24h + 12h
205
+
206
+ const layer: LayerProps = buildLayerProps({
207
+ users: ["u1"],
208
+ start: todayStart,
209
+ handoff: OneUptimeDate.addRemoveDays(todayStart, 2),
210
+ restriction: {
211
+ type: RestrictionType.Daily,
212
+ start: "23:00",
213
+ end: "11:00",
214
+ },
215
+ });
216
+
217
+ const events: Array<CalendarEvent> = util.getEvents({
218
+ ...layer,
219
+ calendarStartDate: todayStart,
220
+ calendarEndDate: calendarEnd,
221
+ });
222
+
223
+ // Expect two events: 23:00 -> 23:59:59 (approx) and 00:00 -> 11:00 next day (depending on trimming logic)
224
+ // We simplify by checking presence of one starting at 23:00 and one ending at 11:00.
225
+ expect(events.length).toBeGreaterThanOrEqual(2); // Expect at least two distinct segments across midnight.
226
+ const has23Window: boolean = events.some((e: CalendarEvent) => {
227
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "23:00";
228
+ });
229
+ // End might be 10:59 or 11:00 depending on second trimming; allow both 10 or 11 hour boundary.
230
+ const hasMorningCoverage: boolean = events.some((e: CalendarEvent) => {
231
+ const startHM: string = OneUptimeDate.getLocalHourAndMinuteFromDate(
232
+ e.start,
233
+ );
234
+ const endHM: string = OneUptimeDate.getLocalHourAndMinuteFromDate(e.end);
235
+ // Morning segment should end at or near 11:00 and start at or near 00:00
236
+ return (
237
+ (startHM === "00:00" || startHM === "00:01" || startHM === "23:59") &&
238
+ (endHM === "11:00" || endHM === "10:59" || endHM === "10:58")
239
+ );
240
+ });
241
+
242
+ expect(has23Window).toBeTruthy();
243
+ expect(hasMorningCoverage).toBeTruthy();
244
+ });
245
+ });
246
+
247
+ describe("LayerUtil getEvents - Multi-day Daily Windows", () => {
248
+ test("Daily restriction (09:00-17:00) over 3 day calendar produces one window per day", () => {
249
+ const util: LayerUtil = new LayerUtil();
250
+ const day1: Date = OneUptimeDate.getStartOfDay(new Date());
251
+ const calendarEnd: Date = OneUptimeDate.addRemoveDays(day1, 3); // 3 days window
252
+
253
+ const layer: LayerProps = buildLayerProps({
254
+ users: ["u1"],
255
+ start: day1,
256
+ handoff: OneUptimeDate.addRemoveDays(day1, 1), // initial handoff end of day1
257
+ restriction: {
258
+ type: RestrictionType.Daily,
259
+ start: "09:00",
260
+ end: "17:00",
261
+ },
262
+ rotation: { intervalType: EventInterval.Day, intervalCount: 1 },
263
+ });
264
+
265
+ const events: Array<CalendarEvent> = util.getEvents({
266
+ ...layer,
267
+ calendarStartDate: day1,
268
+ calendarEndDate: calendarEnd,
269
+ });
270
+
271
+ const windowsStartingAtNine: Array<CalendarEvent> = events.filter(
272
+ (e: CalendarEvent) => {
273
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "09:00";
274
+ },
275
+ );
276
+ expect(windowsStartingAtNine.length).toBeGreaterThanOrEqual(2);
277
+ });
278
+ });
279
+
280
+ describe("LayerUtil getEvents - Weekly Restrictions", () => {
281
+ test("Simple weekly window Monday 09:00 to Wednesday 17:00 yields trimmed events", () => {
282
+ const util: LayerUtil = new LayerUtil();
283
+ const monday: Date = OneUptimeDate.getStartOfTheWeek(new Date());
284
+ const calendarEnd: Date = OneUptimeDate.addRemoveDays(monday, 7);
285
+
286
+ const layer: LayerProps = buildLayerProps({
287
+ users: ["u1"],
288
+ start: monday,
289
+ handoff: OneUptimeDate.addRemoveWeeks(monday, 1),
290
+ weeklyRestrictions: [
291
+ {
292
+ startDay: DayOfWeek.Monday,
293
+ endDay: DayOfWeek.Wednesday,
294
+ start: "09:00",
295
+ end: "17:00",
296
+ },
297
+ ],
298
+ rotation: { intervalType: EventInterval.Week, intervalCount: 1 },
299
+ });
300
+
301
+ const events: Array<CalendarEvent> = util.getEvents({
302
+ ...layer,
303
+ calendarStartDate: monday,
304
+ calendarEndDate: calendarEnd,
305
+ });
306
+
307
+ const hasStartNine: boolean = events.some((e: CalendarEvent) => {
308
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "09:00";
309
+ });
310
+ const hasEndSeventeen: boolean = events.some((e: CalendarEvent) => {
311
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.end) === "17:00";
312
+ });
313
+ expect(hasStartNine).toBeTruthy();
314
+ expect(hasEndSeventeen).toBeTruthy();
315
+ });
316
+
317
+ test("Weekly wrap-around Friday 22:00 to Monday 06:00 produces appropriate segments", () => {
318
+ const util: LayerUtil = new LayerUtil();
319
+ const monday: Date = OneUptimeDate.getStartOfTheWeek(new Date());
320
+ const calendarEnd: Date = OneUptimeDate.addRemoveDays(monday, 7);
321
+
322
+ const layer: LayerProps = buildLayerProps({
323
+ users: ["u1"],
324
+ start: monday,
325
+ handoff: OneUptimeDate.addRemoveWeeks(monday, 1),
326
+ weeklyRestrictions: [
327
+ {
328
+ startDay: DayOfWeek.Friday,
329
+ endDay: DayOfWeek.Monday,
330
+ start: "22:00",
331
+ end: "06:00",
332
+ },
333
+ ],
334
+ rotation: { intervalType: EventInterval.Week, intervalCount: 1 },
335
+ });
336
+
337
+ const events: Array<CalendarEvent> = util.getEvents({
338
+ ...layer,
339
+ calendarStartDate: monday,
340
+ calendarEndDate: calendarEnd,
341
+ });
342
+
343
+ const has22: boolean = events.some((e: CalendarEvent) => {
344
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.start) === "22:00";
345
+ });
346
+ const has06: boolean = events.some((e: CalendarEvent) => {
347
+ return OneUptimeDate.getLocalHourAndMinuteFromDate(e.end) === "06:00";
348
+ });
349
+ expect(has22).toBeTruthy();
350
+ expect(has06).toBeTruthy();
351
+ });
352
+ });
353
+
354
+ describe("LayerUtil getEvents - Daily Rotation Across Users", () => {
355
+ test("Daily rotation cycles users", () => {
356
+ const util: LayerUtil = new LayerUtil();
357
+ const day1: Date = OneUptimeDate.getStartOfDay(new Date());
358
+ const calendarEnd: Date = OneUptimeDate.addRemoveDays(day1, 3); // 3 days
359
+
360
+ const layer: LayerProps = buildLayerProps({
361
+ users: ["a", "b"],
362
+ start: day1,
363
+ handoff: OneUptimeDate.addRemoveDays(day1, 1),
364
+ rotation: { intervalType: EventInterval.Day, intervalCount: 1 },
365
+ });
366
+
367
+ const events: Array<CalendarEvent> = util.getEvents({
368
+ ...layer,
369
+ calendarStartDate: day1,
370
+ calendarEndDate: calendarEnd,
371
+ });
372
+
373
+ if (events.length >= 2) {
374
+ expect(events[0]!.title).not.toBe(events[1]!.title);
375
+ }
376
+ });
377
+ });
378
+
379
+ describe("LayerUtil getMultiLayerEvents - Partial Overlap Trimming", () => {
380
+ test("Primary layer inside backup trims backup", () => {
381
+ const util: LayerUtil = new LayerUtil();
382
+ const start: Date = OneUptimeDate.getStartOfDay(new Date());
383
+ const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 6);
384
+
385
+ const primary: LayerProps = buildLayerProps({
386
+ users: ["primary"],
387
+ start: OneUptimeDate.addRemoveHours(start, 2),
388
+ handoff: OneUptimeDate.addRemoveHours(start, 4),
389
+ });
390
+
391
+ const backup: LayerProps = buildLayerProps({
392
+ users: ["backup"],
393
+ start: start,
394
+ handoff: OneUptimeDate.addRemoveHours(start, 6),
395
+ });
396
+
397
+ const events: Array<CalendarEvent> = util.getMultiLayerEvents({
398
+ layers: [primary, backup],
399
+ calendarStartDate: start,
400
+ calendarEndDate: calendarEnd,
401
+ });
402
+
403
+ const containsPrimary: boolean = events.some((e: CalendarEvent) => {
404
+ return e.title === "primary";
405
+ });
406
+ const containsBackup: boolean = events.some((e: CalendarEvent) => {
407
+ return e.title === "backup";
408
+ });
409
+ expect(containsPrimary).toBeTruthy();
410
+ expect(containsBackup).toBeTruthy();
411
+ });
412
+ });
413
+
414
+ describe("LayerUtil getEvents - Rotation Handoff", () => {
415
+ test("Hourly rotation changes user after each hour", () => {
416
+ const util: LayerUtil = new LayerUtil();
417
+ const start: Date = OneUptimeDate.getStartOfDay(new Date());
418
+ const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 5); // 5 hours window
419
+
420
+ const layer: LayerProps = buildLayerProps({
421
+ users: ["u1", "u2", "u3"],
422
+ start: start,
423
+ handoff: OneUptimeDate.addRemoveHours(start, 1), // first handoff at +1h
424
+ rotation: { intervalType: EventInterval.Hour, intervalCount: 1 },
425
+ });
426
+
427
+ const events: Array<CalendarEvent> = util.getEvents({
428
+ ...layer,
429
+ calendarStartDate: start,
430
+ calendarEndDate: calendarEnd,
431
+ });
432
+
433
+ // Expect roughly 5 events (one per hour) and user IDs rotate in sequence.
434
+ expect(events.length).toBeGreaterThanOrEqual(4);
435
+ const userSequence: Array<string> = events.map((e: CalendarEvent) => {
436
+ return e.title;
437
+ });
438
+
439
+ // Titles are user ids (strings we passed) according to implementation.
440
+ // Check that at least first three rotate u1 -> u2 -> u3
441
+ expect(userSequence.slice(0, 3)).toEqual(["u1", "u2", "u3"]);
442
+ });
443
+ });
444
+
445
+ describe("LayerUtil getMultiLayerEvents - Overlap Priority", () => {
446
+ test("Higher priority (lower index) layer should trim overlapping lower priority events", () => {
447
+ const util: LayerUtil = new LayerUtil();
448
+ const start: Date = OneUptimeDate.getStartOfDay(new Date());
449
+ const calendarEnd: Date = OneUptimeDate.addRemoveHours(start, 6);
450
+
451
+ const layer1: LayerProps = buildLayerProps({
452
+ users: ["primary"],
453
+ start: start,
454
+ handoff: OneUptimeDate.addRemoveHours(start, 6),
455
+ });
456
+
457
+ const layer2: LayerProps = buildLayerProps({
458
+ users: ["backup"],
459
+ start: start,
460
+ handoff: OneUptimeDate.addRemoveHours(start, 6),
461
+ });
462
+
463
+ const events: Array<CalendarEvent> = util.getMultiLayerEvents({
464
+ layers: [layer1, layer2],
465
+ calendarStartDate: start,
466
+ calendarEndDate: calendarEnd,
467
+ });
468
+
469
+ // All events should belong to primary (priority 1) with no backup overlapping intervals left.
470
+ const titles: Array<string> = events.map((e: CalendarEvent) => {
471
+ return e.title;
472
+ });
473
+ expect(
474
+ titles.every((t: string) => {
475
+ return t === "primary";
476
+ }),
477
+ ).toBeTruthy();
478
+ });
479
+ });
@@ -718,6 +718,79 @@ export default class LayerUtil {
718
718
  let restrictionStartTime: Date = dayRestrictionTimes.startTime;
719
719
  let restrictionEndTime: Date = dayRestrictionTimes.endTime;
720
720
 
721
+ // Special Case: Overnight (wrap-around) window where end time is logically on the next day.
722
+ // Example: 23:00 -> 11:00 (next day). Existing algorithm assumed end >= start within same day
723
+ // and returned no events. We explicitly expand such windows into two segments per day:
724
+ // 1) start -> endOfDay(start)
725
+ // 2) startOfNextDay -> end (moved to next day)
726
+ if (OneUptimeDate.isBefore(restrictionEndTime, restrictionStartTime)) {
727
+ const results: Array<StartAndEndTime> = [];
728
+
729
+ // We'll iterate day-by-day within the event range (max 31 iterations safeguard)
730
+ let currentDayStart: Date = OneUptimeDate.getStartOfDay(
731
+ data.eventStartTime,
732
+ );
733
+ const absoluteEventEnd: Date = data.eventEndTime;
734
+ let safetyCounter: number = 0;
735
+ const maxDays: number = 62; // generous safeguard
736
+
737
+ while (
738
+ OneUptimeDate.isOnOrBefore(currentDayStart, absoluteEventEnd) &&
739
+ safetyCounter < maxDays
740
+ ) {
741
+ safetyCounter++;
742
+
743
+ const segmentNightStart: Date = OneUptimeDate.keepTimeButMoveDay(
744
+ restrictionStartTime,
745
+ currentDayStart,
746
+ );
747
+ const segmentNightEnd: Date =
748
+ OneUptimeDate.getEndOfDay(segmentNightStart);
749
+
750
+ const nextDayStart: Date = OneUptimeDate.addRemoveDays(
751
+ currentDayStart,
752
+ 1,
753
+ );
754
+ const segmentMorningStart: Date =
755
+ OneUptimeDate.getStartOfDay(nextDayStart);
756
+ const segmentMorningEnd: Date = OneUptimeDate.keepTimeButMoveDay(
757
+ restrictionEndTime,
758
+ nextDayStart,
759
+ );
760
+
761
+ // helper to add intersection if it overlaps the event window
762
+ const addIntersection: (segStart: Date, segEnd: Date) => void = (
763
+ segStart: Date,
764
+ segEnd: Date,
765
+ ): void => {
766
+ // normalize zero / invalid lengths
767
+ if (OneUptimeDate.isOnOrBefore(segEnd, segStart)) {
768
+ return; // no length
769
+ }
770
+ // intersect with [eventStart, eventEnd]
771
+ const start: Date = OneUptimeDate.getGreaterDate(
772
+ segStart,
773
+ data.eventStartTime,
774
+ );
775
+ const end: Date = OneUptimeDate.getLesserDate(
776
+ segEnd,
777
+ data.eventEndTime,
778
+ );
779
+ if (OneUptimeDate.isAfter(end, start)) {
780
+ results.push({ startTime: start, endTime: end });
781
+ }
782
+ };
783
+
784
+ addIntersection(segmentNightStart, segmentNightEnd);
785
+ addIntersection(segmentMorningStart, segmentMorningEnd);
786
+
787
+ // advance a day
788
+ currentDayStart = nextDayStart;
789
+ }
790
+
791
+ return results;
792
+ }
793
+
721
794
  let currentStartTime: Date = data.eventStartTime;
722
795
  const currentEndTime: Date = data.eventEndTime;
723
796
 
@@ -83,7 +83,9 @@ const JSONTable: FunctionComponent<JSONTableProps> = (
83
83
  if (!groupMap[prefix]) {
84
84
  groupMap[prefix] = [];
85
85
  }
86
- groupMap[prefix].push({ index, value: working[key] });
86
+ // At this point groupMap[prefix] is initialized. Use a local ref so TS can narrow.
87
+ const entries: Array<GroupEntry> = groupMap[prefix]!;
88
+ entries.push({ index, value: working[key] });
87
89
  keysToRemove.add(key);
88
90
  }
89
91
 
package/Utils/API.ts CHANGED
@@ -504,55 +504,58 @@ export default class API {
504
504
  }
505
505
  }
506
506
 
507
- if (errorString.toLocaleLowerCase().includes("network error")) {
507
+ const status: number | undefined = (error as AxiosError)?.response?.status;
508
+ const lowerErr: string = errorString.toLocaleLowerCase();
509
+
510
+ if (status !== 400 && lowerErr.includes("network error")) {
508
511
  return "Network Error.";
509
512
  }
510
513
 
511
- if (errorString.toLocaleLowerCase().includes("timeout")) {
514
+ if (status !== 400 && lowerErr.includes("timeout")) {
512
515
  return "Timeout Error.";
513
516
  }
514
517
 
515
- if (errorString.toLocaleLowerCase().includes("request aborted")) {
518
+ if (status !== 400 && lowerErr.includes("request aborted")) {
516
519
  return "Request Aborted.";
517
520
  }
518
521
 
519
- if (errorString.toLocaleLowerCase().includes("canceled")) {
522
+ if (status !== 400 && lowerErr.includes("canceled")) {
520
523
  return "Request Canceled.";
521
524
  }
522
525
 
523
- if (errorString.toLocaleLowerCase().includes("connection refused")) {
526
+ if (status !== 400 && lowerErr.includes("connection refused")) {
524
527
  return "Connection Refused.";
525
528
  }
526
529
 
527
- if (errorString.toLocaleLowerCase().includes("connection reset")) {
530
+ if (status !== 400 && lowerErr.includes("connection reset")) {
528
531
  return "Connection Reset.";
529
532
  }
530
533
 
531
- if (errorString.toLocaleLowerCase().includes("connection closed")) {
534
+ if (status !== 400 && lowerErr.includes("connection closed")) {
532
535
  return "Connection Closed.";
533
536
  }
534
537
 
535
- if (errorString.toLocaleLowerCase().includes("connection failed")) {
538
+ if (status !== 400 && lowerErr.includes("connection failed")) {
536
539
  return "Connection Failed.";
537
540
  }
538
541
 
539
- if (errorString.toLocaleLowerCase().includes("enotfound")) {
542
+ if (status !== 400 && lowerErr.includes("enotfound")) {
540
543
  return "Cannot Find Host.";
541
544
  }
542
545
 
543
- if (errorString.toLocaleLowerCase().includes("econnreset")) {
546
+ if (status !== 400 && lowerErr.includes("econnreset")) {
544
547
  return "Connection Reset.";
545
548
  }
546
549
 
547
- if (errorString.toLocaleLowerCase().includes("econnrefused")) {
550
+ if (status !== 400 && lowerErr.includes("econnrefused")) {
548
551
  return "Connection Refused.";
549
552
  }
550
553
 
551
- if (errorString.toLocaleLowerCase().includes("econnaborted")) {
554
+ if (status !== 400 && lowerErr.includes("econnaborted")) {
552
555
  return "Connection Aborted.";
553
556
  }
554
557
 
555
- if (errorString.toLocaleLowerCase().includes("certificate has expired")) {
558
+ if (status !== 400 && lowerErr.includes("certificate has expired")) {
556
559
  return "SSL Certificate Expired.";
557
560
  }
558
561