@oneuptime/common 10.4.14 → 10.4.16

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.
Files changed (152) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +49 -0
  2. package/Models/AnalyticsModels/AuditLog.ts +8 -0
  3. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  4. package/Models/AnalyticsModels/Log.ts +1 -0
  5. package/Models/AnalyticsModels/Metric.ts +10 -0
  6. package/Models/AnalyticsModels/MonitorLog.ts +1 -0
  7. package/Models/AnalyticsModels/Profile.ts +1 -0
  8. package/Models/AnalyticsModels/ProfileSample.ts +1 -0
  9. package/Models/AnalyticsModels/Span.ts +1 -0
  10. package/Models/DatabaseModels/AlertCustomField.ts +37 -0
  11. package/Models/DatabaseModels/IncidentCustomField.ts +37 -0
  12. package/Models/DatabaseModels/IncidentMember.ts +9 -0
  13. package/Models/DatabaseModels/MonitorCustomField.ts +37 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +37 -0
  15. package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +37 -0
  16. package/Models/DatabaseModels/StatusPageCustomField.ts +37 -0
  17. package/Models/DatabaseModels/TableView.ts +40 -0
  18. package/Models/DatabaseModels/TeamMemberCustomField.ts +37 -0
  19. package/Server/API/BaseAnalyticsAPI.ts +128 -20
  20. package/Server/API/MetricAPI.ts +5 -138
  21. package/Server/API/StatusAPI.ts +103 -7
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.ts +13 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.ts +34 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.ts +67 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Services/AccessTokenService.ts +1 -1
  27. package/Server/Services/AnalyticsDatabaseService.ts +24 -4
  28. package/Server/Services/MetricService.ts +113 -0
  29. package/Server/Services/ProjectService.ts +21 -1
  30. package/Server/Utils/Response.ts +4 -1
  31. package/Server/Utils/UserPermission/UserPermission.ts +17 -1
  32. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +2 -2
  33. package/Types/API/HTTPResponse.ts +16 -0
  34. package/Types/BaseDatabase/ListResult.ts +6 -0
  35. package/Types/CustomField/CustomFieldType.ts +2 -0
  36. package/Types/Date.ts +9 -1
  37. package/Types/ListData.ts +14 -0
  38. package/Types/Monitor/DnsMonitor/DnsMonitorResponse.ts +3 -0
  39. package/Types/Monitor/DnssecMonitor/DnssecMonitorResponse.ts +5 -0
  40. package/Types/Monitor/DomainMonitor/DomainMonitorResponse.ts +4 -0
  41. package/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.ts +4 -0
  42. package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +3 -0
  43. package/Types/Probe/ProbeAttempt.ts +9 -0
  44. package/Types/Probe/ProbeMonitorResponse.ts +3 -0
  45. package/UI/Components/BulkUpdate/BulkOwnerActions.tsx +504 -0
  46. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +64 -54
  47. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +38 -0
  48. package/UI/Components/CustomFields/DropdownOptionsInput.tsx +150 -0
  49. package/UI/Components/Detail/Detail.tsx +78 -11
  50. package/UI/Components/List/List.tsx +6 -0
  51. package/UI/Components/ModelTable/BaseModelTable.tsx +74 -2
  52. package/UI/Components/ModelTable/TableView.tsx +74 -30
  53. package/UI/Components/Pagination/Pagination.tsx +75 -33
  54. package/UI/Components/Table/Table.tsx +6 -0
  55. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +1 -0
  56. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +33 -0
  57. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  58. package/build/dist/Models/AnalyticsModels/AuditLog.js +8 -0
  59. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  60. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  61. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  62. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  63. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  64. package/build/dist/Models/AnalyticsModels/Metric.js +10 -0
  65. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  66. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  67. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  68. package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
  69. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  70. package/build/dist/Models/AnalyticsModels/ProfileSample.js +1 -0
  71. package/build/dist/Models/AnalyticsModels/ProfileSample.js.map +1 -1
  72. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  73. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  74. package/build/dist/Models/DatabaseModels/AlertCustomField.js +38 -0
  75. package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +38 -0
  77. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/IncidentMember.js +11 -1
  79. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/MonitorCustomField.js +38 -0
  81. package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +38 -0
  83. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
  84. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +38 -0
  85. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +38 -0
  87. package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  89. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +38 -0
  91. package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
  92. package/build/dist/Server/API/BaseAnalyticsAPI.js +105 -18
  93. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  94. package/build/dist/Server/API/MetricAPI.js +5 -113
  95. package/build/dist/Server/API/MetricAPI.js.map +1 -1
  96. package/build/dist/Server/API/StatusAPI.js +75 -8
  97. package/build/dist/Server/API/StatusAPI.js.map +1 -1
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js +12 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779536271671-AddFacetsToTableView.js.map +1 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js +27 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779540427366-AddIsMemberNotifiedIndex.js.map +1 -0
  102. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js +28 -0
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779619108628-AddDropdownOptionsToCustomFields.js.map +1 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  106. package/build/dist/Server/Services/AccessTokenService.js +1 -1
  107. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  108. package/build/dist/Server/Services/AnalyticsDatabaseService.js +22 -3
  109. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  110. package/build/dist/Server/Services/MetricService.js +89 -0
  111. package/build/dist/Server/Services/MetricService.js.map +1 -1
  112. package/build/dist/Server/Services/ProjectService.js +19 -1
  113. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  114. package/build/dist/Server/Utils/Response.js +6 -5
  115. package/build/dist/Server/Utils/Response.js.map +1 -1
  116. package/build/dist/Server/Utils/UserPermission/UserPermission.js +13 -1
  117. package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
  118. package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js +2 -2
  119. package/build/dist/Tests/Server/Services/AnalyticsDatabaseService.test.js.map +1 -1
  120. package/build/dist/Types/API/HTTPResponse.js +15 -0
  121. package/build/dist/Types/API/HTTPResponse.js.map +1 -1
  122. package/build/dist/Types/CustomField/CustomFieldType.js +2 -0
  123. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  124. package/build/dist/Types/Date.js +10 -1
  125. package/build/dist/Types/Date.js.map +1 -1
  126. package/build/dist/Types/ListData.js +4 -0
  127. package/build/dist/Types/ListData.js.map +1 -1
  128. package/build/dist/Types/Probe/ProbeAttempt.js +2 -0
  129. package/build/dist/Types/Probe/ProbeAttempt.js.map +1 -0
  130. package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js +376 -0
  131. package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js.map +1 -0
  132. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +32 -25
  133. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  134. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +32 -0
  135. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  136. package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js +84 -0
  137. package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js.map +1 -0
  138. package/build/dist/UI/Components/Detail/Detail.js +34 -3
  139. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  140. package/build/dist/UI/Components/List/List.js +1 -1
  141. package/build/dist/UI/Components/List/List.js.map +1 -1
  142. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +45 -5
  143. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  144. package/build/dist/UI/Components/ModelTable/TableView.js +44 -19
  145. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  146. package/build/dist/UI/Components/Pagination/Pagination.js +62 -36
  147. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  148. package/build/dist/UI/Components/Table/Table.js +1 -1
  149. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  150. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +1 -0
  151. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  152. package/package.json +1 -1
@@ -4,12 +4,15 @@ import Metric from "../../Models/AnalyticsModels/Metric";
4
4
  import AggregateBy, {
5
5
  AggregateUtil,
6
6
  } from "../Types/AnalyticsDatabase/AggregateBy";
7
+ import DeleteBy from "../Types/AnalyticsDatabase/DeleteBy";
8
+ import Query from "../Types/AnalyticsDatabase/Query";
7
9
  import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
8
10
  import AggregationType, {
9
11
  getPercentileLevel,
10
12
  isPercentileAggregation,
11
13
  } from "../../Types/BaseDatabase/AggregationType";
12
14
  import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
15
+ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
13
16
  import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
14
17
  import logger, { LogAttributes } from "../Utils/Logger";
15
18
 
@@ -18,6 +21,116 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
18
21
  super({ modelType: Metric, database: clickhouseDatabase });
19
22
  }
20
23
 
24
+ /*
25
+ * Cascade deletes from `MetricItemV2` into the aggregating
26
+ * materialized-view target tables.
27
+ *
28
+ * `MetricItemAggMV1m` and `MetricBaselineHourly` are AggregatingMergeTree
29
+ * tables populated by attached MVs that only fire on inserts —
30
+ * `ALTER ... DELETE` against the source table does not roll back the
31
+ * previously-accumulated `sumState`/`countState` rows already in the MV
32
+ * tables. Without a matching DELETE on each MV, dashboard widgets that
33
+ * read from `MetricItemAggMV1m` keep counting and averaging metrics
34
+ * belonging to entities (incidents, alerts) the user has just deleted.
35
+ * See https://github.com/OneUptime/oneuptime/issues/2419.
36
+ *
37
+ * The cascade only runs when the caller scoped the delete by
38
+ * `serviceId`. Global time-based purges (TTL cleanup) are handled by
39
+ * each MV table's own `retentionDate TTL DELETE`, so cascading those
40
+ * would pointlessly scan the whole MV. The per-host MV
41
+ * (`MetricItemAggMV1mByHost`) is keyed by `hostIdentifier` rather than
42
+ * `serviceId`, so a service-scoped delete has nothing to remove there —
43
+ * skip it.
44
+ */
45
+ public override async deleteBy(deleteBy: DeleteBy<Metric>): Promise<void> {
46
+ await super.deleteBy(deleteBy);
47
+
48
+ const cascadeQuery: Query<Metric> | null = this.buildMVCascadeQuery(
49
+ deleteBy.query,
50
+ );
51
+ if (!cascadeQuery) {
52
+ return;
53
+ }
54
+
55
+ if (!this.database) {
56
+ this.useDefaultDatabase();
57
+ }
58
+ const databaseName: string = this.database.getDatasourceOptions().database!;
59
+ const whereStatement: Statement =
60
+ this.statementGenerator.toWhereStatement(cascadeQuery);
61
+
62
+ const cascadeTargets: ReadonlyArray<AnalyticsTableName> = [
63
+ AnalyticsTableName.MetricItemAggMV1m,
64
+ AnalyticsTableName.MetricBaselineHourly,
65
+ ];
66
+
67
+ for (const tableName of cascadeTargets) {
68
+ try {
69
+ /*
70
+ * Lightweight delete — see toDeleteStatement() in
71
+ * AnalyticsDatabaseService for the rationale (avoids the
72
+ * ALTER mutations queue which is capped at 1000 per table).
73
+ */
74
+ const statement: Statement =
75
+ SQL`DELETE FROM ${databaseName}.${tableName} WHERE TRUE `.append(
76
+ whereStatement,
77
+ );
78
+ await this.execute(statement);
79
+ } catch (err) {
80
+ logger.error(
81
+ `Cascade delete into ${tableName} failed; dashboard widgets reading from this MV may temporarily show stale aggregated values for the deleted entity.`,
82
+ );
83
+ logger.error(err);
84
+ }
85
+ }
86
+ }
87
+
88
+ private buildMVCascadeQuery(query: Query<Metric>): Query<Metric> | null {
89
+ if (!query || typeof query !== "object") {
90
+ return null;
91
+ }
92
+
93
+ const queryRecord: Record<string, unknown> = query as unknown as Record<
94
+ string,
95
+ unknown
96
+ >;
97
+
98
+ /*
99
+ * Cascade only when the delete is scoped by serviceId. The MV sort
100
+ * key is (projectId, name, serviceId, bucketTime); without serviceId
101
+ * the DELETE would scan a huge swath of unrelated rows and risk
102
+ * removing data that belongs to other entities sharing the same
103
+ * project.
104
+ */
105
+ if (
106
+ queryRecord["serviceId"] === undefined ||
107
+ queryRecord["serviceId"] === null
108
+ ) {
109
+ return null;
110
+ }
111
+
112
+ /*
113
+ * Only project the keys the MV target tables actually expose.
114
+ * `time`, `attributes`, `serviceType`, and the metric-payload
115
+ * columns don't exist on the MV schema and would either fail
116
+ * where-statement generation or reference a missing column.
117
+ */
118
+ const allowedKeys: ReadonlyArray<string> = [
119
+ "projectId",
120
+ "name",
121
+ "serviceId",
122
+ ];
123
+ const out: Record<string, unknown> = {};
124
+ for (const key of allowedKeys) {
125
+ const value: unknown = queryRecord[key];
126
+ if (value !== undefined) {
127
+ out[key] = value;
128
+ }
129
+ }
130
+
131
+ return out as unknown as Query<Metric>;
132
+ }
133
+
21
134
  /**
22
135
  * Histogram-aware aggregation override.
23
136
  *
@@ -103,6 +103,16 @@ export class ProjectService extends DatabaseService<Model> {
103
103
  */
104
104
  private requireSsoForLoginCache: InMemoryTTLCache<boolean> =
105
105
  new InMemoryTTLCache(10_000);
106
+ /*
107
+ * Caches the current billing plan per project. `getCurrentPlan` is hit
108
+ * by `CommonAPI.getDatabaseCommonInteractionProps` on every
109
+ * authenticated request when billing is enabled — without caching,
110
+ * that's one Postgres findOneById per API call to a billable project.
111
+ * Plans change rarely (subscription create / cancel / change), so a
112
+ * 60s staleness window is acceptable.
113
+ */
114
+ private currentPlanCache: InMemoryTTLCache<CurrentPlan> =
115
+ new InMemoryTTLCache(10_000);
106
116
 
107
117
  public constructor() {
108
118
  super(Model);
@@ -1492,6 +1502,12 @@ export class ProjectService extends DatabaseService<Model> {
1492
1502
  return { plan: null, isSubscriptionUnpaid: false };
1493
1503
  }
1494
1504
 
1505
+ const cacheKey: string = projectId.toString();
1506
+ const cached: CurrentPlan | undefined = this.currentPlanCache.get(cacheKey);
1507
+ if (cached !== undefined) {
1508
+ return cached;
1509
+ }
1510
+
1495
1511
  const project: Model | null = await this.findOneById({
1496
1512
  id: projectId,
1497
1513
  select: {
@@ -1506,10 +1522,12 @@ export class ProjectService extends DatabaseService<Model> {
1506
1522
  });
1507
1523
 
1508
1524
  if (!project) {
1525
+ // Don't cache "not found" — let the caller surface a fresh error.
1509
1526
  throw new BadDataException("Project ID is invalid");
1510
1527
  }
1511
1528
 
1512
1529
  if (!project.paymentProviderPlanId) {
1530
+ // Don't cache "no plan" — the project may be mid-onboarding.
1513
1531
  throw new BadDataException("Project does not have any plans");
1514
1532
  }
1515
1533
 
@@ -1518,7 +1536,7 @@ export class ProjectService extends DatabaseService<Model> {
1518
1536
  getAllEnvVars(),
1519
1537
  );
1520
1538
 
1521
- return {
1539
+ const result: CurrentPlan = {
1522
1540
  plan: plan,
1523
1541
  isSubscriptionUnpaid:
1524
1542
  !BillingService.isSubscriptionActive(
@@ -1528,6 +1546,8 @@ export class ProjectService extends DatabaseService<Model> {
1528
1546
  project.paymentProviderMeteredSubscriptionStatus!,
1529
1547
  ),
1530
1548
  };
1549
+ this.currentPlanCache.set(cacheKey, result, 60_000);
1550
+ return result;
1531
1551
  }
1532
1552
 
1533
1553
  @CaptureSpan()
@@ -121,6 +121,7 @@ export default class Response {
121
121
  list: Array<BaseModel | AnalyticsDataModel>,
122
122
  count: PositiveNumber | number,
123
123
  modelType: { new (): BaseModel | AnalyticsDataModel },
124
+ options?: { hasMore?: boolean | undefined } | undefined,
124
125
  ): void {
125
126
  if (!(count instanceof PositiveNumber)) {
126
127
  count = new PositiveNumber(count);
@@ -144,7 +145,7 @@ export default class Response {
144
145
  );
145
146
  }
146
147
 
147
- return this.sendJsonArrayResponse(req, res, jsonArray, count);
148
+ return this.sendJsonArrayResponse(req, res, jsonArray, count, options);
148
149
  }
149
150
 
150
151
  @CaptureSpan()
@@ -194,6 +195,7 @@ export default class Response {
194
195
  res: ExpressResponse,
195
196
  list: Array<JSONObject>,
196
197
  count: PositiveNumber,
198
+ options?: { hasMore?: boolean | undefined } | undefined,
197
199
  ): void {
198
200
  const oneUptimeRequest: OneUptimeRequest = req as OneUptimeRequest;
199
201
  const oneUptimeResponse: OneUptimeResponse = res as OneUptimeResponse;
@@ -203,6 +205,7 @@ export default class Response {
203
205
  count: new PositiveNumber(0),
204
206
  skip: new PositiveNumber(0),
205
207
  limit: new PositiveNumber(0),
208
+ hasMore: options?.hasMore,
206
209
  });
207
210
 
208
211
  if (!list) {
@@ -10,6 +10,22 @@ import PermissionNamespace from "../../Types/Permission/PermissionNamespace";
10
10
  import CaptureSpan from "../Telemetry/CaptureSpan";
11
11
 
12
12
  export default class UserPermissionUtil {
13
+ /*
14
+ * Build the cache key for a (user, project) tenant-permission entry.
15
+ * The previous shape was `userId.toString() + projectId.toString()`
16
+ * with no separator — two distinct ObjectID pairs could in principle
17
+ * collide because plain concatenation has no boundary marker. Use a
18
+ * delimiter so the namespace is unambiguous, and route both the GET
19
+ * (here) and the SET (in `AccessTokenService.refreshUserTenant
20
+ * AccessPermission`) through this helper so they can't drift.
21
+ */
22
+ public static buildTenantPermissionCacheKey(
23
+ userId: ObjectID,
24
+ projectId: ObjectID,
25
+ ): string {
26
+ return `${userId.toString()}:${projectId.toString()}`;
27
+ }
28
+
13
29
  @CaptureSpan()
14
30
  public static async getUserTenantAccessPermissionFromCache(
15
31
  userId: ObjectID,
@@ -18,7 +34,7 @@ export default class UserPermissionUtil {
18
34
  const json: UserTenantAccessPermission | null =
19
35
  (await GlobalCache.getJSONObject(
20
36
  PermissionNamespace.ProjectPermission,
21
- userId.toString() + projectId.toString(),
37
+ this.buildTenantPermissionCacheKey(userId, projectId),
22
38
  )) as UserTenantAccessPermission;
23
39
 
24
40
  if (json) {
@@ -357,8 +357,8 @@ describe("AnalyticsDatabaseService", () => {
357
357
  });
358
358
 
359
359
  expect(statement.query).toBe(
360
- "ALTER TABLE {p0:Identifier}.{p1:Identifier}\n" +
361
- "DELETE WHERE TRUE <where-statement>",
360
+ "DELETE FROM {p0:Identifier}.{p1:Identifier}\n" +
361
+ "WHERE TRUE <where-statement>",
362
362
  );
363
363
  expect(statement.query_params).toStrictEqual({
364
364
  p0: "oneuptime",
@@ -69,6 +69,19 @@ export default class HTTPResponse<
69
69
  this._skip = v;
70
70
  }
71
71
 
72
+ /*
73
+ * Optional. Set by analytics list endpoints that skip the expensive
74
+ * COUNT(*) and instead over-fetch by one row. When defined, `count`
75
+ * is only a lower bound — UI should drive next/prev from `hasMore`.
76
+ */
77
+ private _hasMore: boolean | undefined = undefined;
78
+ public get hasMore(): boolean | undefined {
79
+ return this._hasMore;
80
+ }
81
+ public set hasMore(v: boolean | undefined) {
82
+ this._hasMore = v;
83
+ }
84
+
72
85
  public constructor(
73
86
  statusCode: number,
74
87
  data: JSONObject | Array<JSONObject>,
@@ -86,6 +99,9 @@ export default class HTTPResponse<
86
99
  this.count = data["count"] as number;
87
100
  this.skip = data["skip"] as number;
88
101
  this.limit = data["limit"] as number;
102
+ if (Object.keys(data).includes("hasMore")) {
103
+ this.hasMore = data["hasMore"] as boolean;
104
+ }
89
105
  this.jsonData = JSONFunctions.deserializeArray(data["data"] as JSONArray);
90
106
  } else if (Array.isArray(data)) {
91
107
  this.jsonData = JSONFunctions.deserializeArray(data as JSONArray);
@@ -9,4 +9,10 @@ export default interface ListResult<
9
9
  count: number;
10
10
  skip: number;
11
11
  limit: number;
12
+ /*
13
+ * Optional. When set, the analytics endpoint produced this result
14
+ * without a full COUNT(*) — `count` is only a lower bound. Drive
15
+ * pagination off `hasMore` rather than computing total pages.
16
+ */
17
+ hasMore?: boolean | undefined;
12
18
  }
@@ -2,6 +2,8 @@ enum CustomFieldType {
2
2
  Text = "Text",
3
3
  Number = "Number",
4
4
  Boolean = "Boolean",
5
+ Dropdown = "Dropdown",
6
+ MultiSelectDropdown = "MultiSelectDropdown",
5
7
  }
6
8
 
7
9
  export default CustomFieldType;
package/Types/Date.ts CHANGED
@@ -1554,7 +1554,15 @@ export default class OneUptimeDate {
1554
1554
  .toDate();
1555
1555
  }
1556
1556
 
1557
- return moment(trimmedDate).toDate();
1557
+ /*
1558
+ * moment.js throws on non-ISO formats like TLS cert dates ("Mar 31 00:00:00 2026 GMT").
1559
+ * Fall back to native Date which handles this format correctly.
1560
+ */
1561
+ try {
1562
+ return moment(trimmedDate).toDate();
1563
+ } catch (_err: unknown) {
1564
+ return new Date(trimmedDate);
1565
+ }
1558
1566
  }
1559
1567
 
1560
1568
  if (
package/Types/ListData.ts CHANGED
@@ -7,17 +7,27 @@ export default class ListData {
7
7
  count: PositiveNumber;
8
8
  skip: PositiveNumber;
9
9
  limit: PositiveNumber;
10
+ hasMore?: boolean | undefined;
10
11
  }) {
11
12
  this.data = obj.data;
12
13
  this.count = obj.count;
13
14
  this.skip = obj.skip;
14
15
  this.limit = obj.limit;
16
+ this.hasMore = obj.hasMore;
15
17
  }
16
18
 
17
19
  public data: JSONArray;
18
20
  public count: PositiveNumber;
19
21
  public skip: PositiveNumber;
20
22
  public limit: PositiveNumber;
23
+ /*
24
+ * When set, the response was produced without a full COUNT(*) — the
25
+ * server fetched LIMIT+1 rows and only knows whether at least one
26
+ * more page exists. `count` is still emitted as a lower-bound so
27
+ * older clients keep rendering, but UI that wants accurate "X of Y"
28
+ * should switch to `hasMore`-based pagination.
29
+ */
30
+ public hasMore?: boolean | undefined;
21
31
 
22
32
  public toJSON(): JSONObject {
23
33
  const json: JSONObject = {
@@ -27,6 +37,10 @@ export default class ListData {
27
37
  limit: this.limit.toNumber(),
28
38
  };
29
39
 
40
+ if (this.hasMore !== undefined) {
41
+ json["hasMore"] = this.hasMore;
42
+ }
43
+
30
44
  return json;
31
45
  }
32
46
  }
@@ -1,3 +1,4 @@
1
+ import ProbeAttempt from "../../Probe/ProbeAttempt";
1
2
  import DnsRecordType from "./DnsRecordType";
2
3
 
3
4
  export interface DnsRecordResponse {
@@ -13,4 +14,6 @@ export default interface DnsMonitorResponse {
13
14
  records: Array<DnsRecordResponse>;
14
15
  isDnssecValid?: boolean | undefined;
15
16
  isTimeout?: boolean | undefined;
17
+ probeAttempts?: Array<ProbeAttempt> | undefined;
18
+ totalAttempts?: number | undefined;
16
19
  }
@@ -1,3 +1,5 @@
1
+ import ProbeAttempt from "../../Probe/ProbeAttempt";
2
+
1
3
  export interface DnssecKeyRecord {
2
4
  flags: number;
3
5
  algorithm: number;
@@ -66,4 +68,7 @@ export default interface DnssecMonitorResponse {
66
68
 
67
69
  // Overall chain validity (DNSKEY exists, DS exists, RRSIG valid, AD across resolvers)
68
70
  isChainValid: boolean;
71
+
72
+ probeAttempts?: Array<ProbeAttempt> | undefined;
73
+ totalAttempts?: number | undefined;
69
74
  }
@@ -1,3 +1,5 @@
1
+ import ProbeAttempt from "../../Probe/ProbeAttempt";
2
+
1
3
  export default interface DomainMonitorResponse {
2
4
  isOnline: boolean;
3
5
  responseTimeInMs: number;
@@ -12,4 +14,6 @@ export default interface DomainMonitorResponse {
12
14
  dnssec?: string | undefined;
13
15
  domainStatus?: Array<string> | undefined;
14
16
  isTimeout?: boolean | undefined;
17
+ probeAttempts?: Array<ProbeAttempt> | undefined;
18
+ totalAttempts?: number | undefined;
15
19
  }
@@ -1,3 +1,5 @@
1
+ import ProbeAttempt from "../../Probe/ProbeAttempt";
2
+
1
3
  export interface ExternalStatusPageComponentStatus {
2
4
  name: string;
3
5
  status: string;
@@ -13,4 +15,6 @@ export default interface ExternalStatusPageMonitorResponse {
13
15
  failureCause: string;
14
16
  rawBody?: string | undefined;
15
17
  isTimeout?: boolean | undefined;
18
+ probeAttempts?: Array<ProbeAttempt> | undefined;
19
+ totalAttempts?: number | undefined;
16
20
  }
@@ -1,3 +1,4 @@
1
+ import ProbeAttempt from "../../Probe/ProbeAttempt";
1
2
  import SnmpDataType from "./SnmpDataType";
2
3
 
3
4
  export interface SnmpOidResponse {
@@ -13,4 +14,6 @@ export default interface SnmpMonitorResponse {
13
14
  failureCause: string;
14
15
  oidResponses: Array<SnmpOidResponse>;
15
16
  isTimeout?: boolean | undefined;
17
+ probeAttempts?: Array<ProbeAttempt> | undefined;
18
+ totalAttempts?: number | undefined;
16
19
  }
@@ -0,0 +1,9 @@
1
+ export default interface ProbeAttempt {
2
+ attemptNumber: number;
3
+ attemptedAt: Date;
4
+ responseReceivedAt: Date;
5
+ responseTimeInMs?: number | undefined;
6
+ responseCode?: number | undefined;
7
+ isOnline: boolean;
8
+ failureCause?: string | undefined;
9
+ }
@@ -14,6 +14,7 @@ import ExternalStatusPageMonitorResponse from "../Monitor/ExternalStatusPageMoni
14
14
  import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
15
15
  import ObjectID from "../ObjectID";
16
16
  import Port from "../Port";
17
+ import ProbeAttempt from "./ProbeAttempt";
17
18
  import RequestFailedDetails from "./RequestFailedDetails";
18
19
 
19
20
  export default interface ProbeMonitorResponse {
@@ -42,4 +43,6 @@ export default interface ProbeMonitorResponse {
42
43
  isTimeout?: boolean | undefined;
43
44
  ingestedAt?: Date | undefined;
44
45
  evaluationSummary?: MonitorEvaluationSummary | undefined;
46
+ probeAttempts?: Array<ProbeAttempt> | undefined;
47
+ totalAttempts?: number | undefined;
45
48
  }