@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -7,6 +7,7 @@ import Typeof from "../../../Types/Typeof";
7
7
  import { FindOperator, Raw } from "typeorm";
8
8
  import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
9
9
  import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
10
+ import buildJSONColumnQuery, { JSONColumnQuery } from "./JSONColumnQuery";
10
11
 
11
12
  export default class QueryHelper {
12
13
  @CaptureSpan()
@@ -668,40 +669,25 @@ export default class QueryHelper {
668
669
  ) as FindWhereProperty<any>;
669
670
  }
670
671
 
672
+ /**
673
+ * Filter on the keys of a jsonb column — `customFields` above all, whose
674
+ * keys are the names a project gave its custom fields.
675
+ *
676
+ * The per-key predicates (equality, "is any of", contains, numeric
677
+ * comparison, is-empty, and their negations) live in JSONColumnQuery. Two
678
+ * things this used to get wrong and no longer does: the key was spliced
679
+ * straight into the SQL string, and every value was compared with `=`, so an
680
+ * operator such as `Includes` reached Postgres stringified.
681
+ */
671
682
  @CaptureSpan()
672
683
  public static queryJson(value: JSONObject): FindWhereProperty<any> {
673
- // seed random text
674
- const values: JSONObject = {};
675
-
676
- let queryText: string = "";
677
-
678
- if (typeof value === Typeof.String) {
679
- value = JSON.parse(value.toString());
680
- }
681
-
682
684
  if (value instanceof FindOperator) {
683
685
  return value;
684
686
  }
685
687
 
686
- const hasValue: boolean = value && Object.keys(value).length > 0;
687
-
688
- for (const key in value) {
689
- const temp: string = Text.generateRandomText(10);
690
-
691
- values[temp] = value[key];
692
-
693
- if (!queryText) {
694
- queryText = `(COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
695
- } else {
696
- queryText += ` AND COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
697
- }
698
- }
699
-
700
- if (hasValue) {
701
- queryText += ")";
702
- } else {
703
- queryText = "(COLUMN_NAME_ALIAS IS NULL OR COLUMN_NAME_ALIAS = '{}')";
704
- }
688
+ const jsonQuery: JSONColumnQuery = buildJSONColumnQuery({
689
+ value: value as JSONObject,
690
+ });
705
691
 
706
692
  return Raw((alias: string) => {
707
693
  /*
@@ -709,16 +695,15 @@ export default class QueryHelper {
709
695
  * we need to convert this to "tableName"."columnName"
710
696
  */
711
697
 
712
- alias = alias
698
+ const columnReference: string = alias
713
699
  .split(".")
714
700
  .map((item: string) => {
715
701
  return `"${item}"`;
716
702
  })
717
703
  .join(".");
718
704
 
719
- queryText = Text.replaceAll(queryText, "COLUMN_NAME_ALIAS", `${alias}`);
720
- return queryText;
721
- }, values);
705
+ return jsonQuery.toSql(columnReference);
706
+ }, jsonQuery.parameters);
722
707
  }
723
708
 
724
709
  @CaptureSpan()
@@ -0,0 +1,84 @@
1
+ import ColumnLength from "../../../Types/Database/ColumnLength";
2
+
3
+ /*
4
+ * ------------------------------------------------------------------
5
+ * TruncateColumnValue
6
+ *
7
+ * Per-value clamps for bounded varchar columns written by the bulk
8
+ * inventory upsert paths (Kubernetes / Docker / Docker Swarm / Podman /
9
+ * Proxmox / Ceph / IoT).
10
+ *
11
+ * Those services hand-build `INSERT ... ON CONFLICT` statements and
12
+ * flush them in chunks of 500 rows through
13
+ * `getRepository().manager.query(...)`, which bypasses the length
14
+ * validation DatabaseService applies to ordinary CRUD writes. A single
15
+ * value past its column limit raises 22001 ("value too long for type
16
+ * character varying(N)") and aborts the WHOLE multi-row INSERT, so one
17
+ * malformed resource silently drops up to 499 healthy inventory rows
18
+ * with it. Clamping per value keeps the blast radius at one field.
19
+ *
20
+ * Truncation is the last line of defense, not the primary fix: columns
21
+ * that legitimately carry long values (image references, Kubernetes
22
+ * DNS-subdomain names) should be widened in the model + a migration so
23
+ * these clamps effectively never fire.
24
+ * ------------------------------------------------------------------
25
+ */
26
+
27
+ export function truncateToLength(value: string, maxLength: number): string;
28
+ export function truncateToLength(
29
+ value: string | null | undefined,
30
+ maxLength: number,
31
+ ): string | null;
32
+ export function truncateToLength(
33
+ value: string | null | undefined,
34
+ maxLength: number,
35
+ ): string | null {
36
+ if (value === null || value === undefined) {
37
+ return null;
38
+ }
39
+
40
+ if (value.length <= maxLength) {
41
+ return value;
42
+ }
43
+
44
+ const clipped: string = value.substring(0, maxLength);
45
+
46
+ /*
47
+ * JS string length counts UTF-16 code units, so a cut at maxLength can
48
+ * land in the middle of a surrogate pair (any astral-plane character:
49
+ * emoji, rare CJK). The orphaned high surrogate is not valid UTF-8 —
50
+ * Node encodes it as U+FFFD on the way to the driver — so drop it.
51
+ * A well-formed string never ends a character on a high surrogate,
52
+ * which makes this check unambiguous. Postgres counts characters, not
53
+ * code units, so clipping by code units is always conservative: the
54
+ * result can only be shorter than the column bound, never longer.
55
+ */
56
+ const lastCodeUnit: number = clipped.charCodeAt(clipped.length - 1);
57
+ if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) {
58
+ return clipped.substring(0, clipped.length - 1);
59
+ }
60
+
61
+ return clipped;
62
+ }
63
+
64
+ /** Clamp to ColumnLength.ShortText — varchar(100). */
65
+ export function truncateShortText(value: string): string;
66
+ export function truncateShortText(
67
+ value: string | null | undefined,
68
+ ): string | null;
69
+ export function truncateShortText(
70
+ value: string | null | undefined,
71
+ ): string | null {
72
+ return truncateToLength(value, ColumnLength.ShortText);
73
+ }
74
+
75
+ /** Clamp to ColumnLength.LongText — varchar(500). */
76
+ export function truncateLongText(value: string): string;
77
+ export function truncateLongText(
78
+ value: string | null | undefined,
79
+ ): string | null;
80
+ export function truncateLongText(
81
+ value: string | null | undefined,
82
+ ): string | null {
83
+ return truncateToLength(value, ColumnLength.LongText);
84
+ }
@@ -192,6 +192,18 @@ export default class CompareCriteria {
192
192
  return null;
193
193
  }
194
194
 
195
+ /*
196
+ * parseInt("abc") returns NaN rather than throwing, and NaN is typeof
197
+ * "number" — so without this guard a non-numeric threshold would leak
198
+ * past callers that only check for `=== null`, leaving every numeric
199
+ * comparison silently false (value > NaN is always false). Treat an
200
+ * unparseable value the same as a missing one so the caller can ignore
201
+ * it instead of firing a broken criterion.
202
+ */
203
+ if (Number.isNaN(threshold as number)) {
204
+ return null;
205
+ }
206
+
195
207
  return threshold as number;
196
208
  }
197
209
 
@@ -0,0 +1,119 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import RunnerInstallInstructions from "../../../../App/FeatureSet/Dashboard/src/Components/Runner/InstallInstructions";
6
+ import ObjectID from "../../../Types/ObjectID";
7
+
8
+ /*
9
+ * The setup card renders the docker run command with the Runner's secret key
10
+ * embedded in it. Reading that key is restricted to Project Owner, Project
11
+ * Admin and Runbook Admin (see the ColumnAccessControl on Runner.key), so for
12
+ * a Member or a Viewer the field simply is not in the API response.
13
+ *
14
+ * The Runners table asked for the key and passed `(item.key as string) || ""`
15
+ * straight through, so those users were handed a command ending in
16
+ * ONEUPTIME_RUNNER_KEY= with nothing after it — which copies cleanly, runs on
17
+ * the host, and fails there with an authentication error. These tests pin that
18
+ * no command is ever rendered without a key.
19
+ */
20
+
21
+ const RUNNER_ID: ObjectID = new ObjectID("abc-123");
22
+
23
+ type RenderWithKeyFunction = (key: string) => HTMLElement;
24
+
25
+ const renderWithKey: RenderWithKeyFunction = (key: string): HTMLElement => {
26
+ const { container } = render(
27
+ <RunnerInstallInstructions runnerId={RUNNER_ID} runnerKey={key} />,
28
+ );
29
+
30
+ return container;
31
+ };
32
+
33
+ describe("RunnerInstallInstructions", () => {
34
+ describe("when the key is readable", () => {
35
+ test("renders the docker run command", () => {
36
+ const container: HTMLElement = renderWithKey("super-secret-key");
37
+
38
+ expect(container.textContent).toContain("docker run");
39
+ expect(container.textContent).toContain("oneuptime/runner:release");
40
+ });
41
+
42
+ test("embeds the Runner id and key", () => {
43
+ const container: HTMLElement = renderWithKey("super-secret-key");
44
+
45
+ expect(container.textContent).toContain("ONEUPTIME_RUNNER_ID=abc-123");
46
+ expect(container.textContent).toContain(
47
+ "ONEUPTIME_RUNNER_KEY=super-secret-key",
48
+ );
49
+ });
50
+
51
+ test("does not render the permission warning", () => {
52
+ renderWithKey("super-secret-key");
53
+
54
+ expect(
55
+ screen.queryByText(/do not have permission/i),
56
+ ).not.toBeInTheDocument();
57
+ });
58
+
59
+ /*
60
+ * The container needs outbound HTTPS only; saying so here is what stops
61
+ * someone opening an inbound hole for it.
62
+ */
63
+ test("states that the Runner needs no inbound connections", () => {
64
+ const container: HTMLElement = renderWithKey("super-secret-key");
65
+
66
+ expect(container.textContent).toContain(
67
+ "does not accept inbound connections",
68
+ );
69
+ });
70
+
71
+ /*
72
+ * Added because "I ran the command and it still says Never connected" is
73
+ * the obvious next confusion once the never-connected state exists.
74
+ */
75
+ test("explains that the first heartbeat takes up to a minute", () => {
76
+ const container: HTMLElement = renderWithKey("super-secret-key");
77
+
78
+ expect(container.textContent).toContain("every 60 seconds");
79
+ expect(container.textContent).toContain("Connected");
80
+ });
81
+ });
82
+
83
+ describe("when the key is not readable", () => {
84
+ test("renders the permission warning instead", () => {
85
+ renderWithKey("");
86
+
87
+ expect(
88
+ screen.getByText(
89
+ "You do not have permission to view this Runner's key",
90
+ ),
91
+ ).toBeInTheDocument();
92
+ });
93
+
94
+ /*
95
+ * The load-bearing assertion: no command at all, rather than a command
96
+ * that is silently broken.
97
+ */
98
+ test("renders no docker command at all", () => {
99
+ const container: HTMLElement = renderWithKey("");
100
+
101
+ expect(container.textContent).not.toContain("docker run");
102
+ expect(container.textContent).not.toContain("ONEUPTIME_RUNNER_KEY");
103
+ });
104
+
105
+ test("never emits an empty ONEUPTIME_RUNNER_KEY=", () => {
106
+ const container: HTMLElement = renderWithKey("");
107
+
108
+ expect(container.textContent).not.toMatch(/ONEUPTIME_RUNNER_KEY=\s/);
109
+ });
110
+
111
+ test("says who can supply the command", () => {
112
+ const container: HTMLElement = renderWithKey("");
113
+
114
+ expect(container.textContent).toContain("Project Owner");
115
+ expect(container.textContent).toContain("Project Admin");
116
+ expect(container.textContent).toContain("Runbook Admin");
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,274 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+ import { describe, expect, test } from "@jest/globals";
5
+ import RunnerStatusElement from "../../../../App/FeatureSet/Dashboard/src/Components/Runner/RunnerStatus";
6
+ import Runner, {
7
+ RunnerConnectionStatus,
8
+ } from "../../../Models/DatabaseModels/Runner";
9
+ import OneUptimeDate from "../../../Types/Date";
10
+ import { JSONObject } from "../../../Types/JSON";
11
+
12
+ /*
13
+ * The one component every Runner connection indicator in the dashboard now
14
+ * renders through. Three call sites used to hand-roll this, and all three read
15
+ * the persisted connectionStatus column — which is written on create and on
16
+ * the first heartbeat and never again. A Runner that died months ago still has
17
+ * it set to "connected".
18
+ *
19
+ * So the assertions that matter most here are the ones proving the rendered
20
+ * status tracks lastAlive and IGNORES connectionStatus, in both directions.
21
+ */
22
+
23
+ type RenderRunnerFunction = (
24
+ runner: Runner | JSONObject,
25
+ showLastSeen?: boolean,
26
+ ) => void;
27
+
28
+ const renderRunner: RenderRunnerFunction = (
29
+ runner: Runner | JSONObject,
30
+ showLastSeen?: boolean,
31
+ ): void => {
32
+ render(
33
+ showLastSeen === undefined ? (
34
+ <RunnerStatusElement runner={runner} />
35
+ ) : (
36
+ <RunnerStatusElement runner={runner} showLastSeen={showLastSeen} />
37
+ ),
38
+ );
39
+ };
40
+
41
+ type MinutesAgoFunction = (minutes: number) => Date;
42
+
43
+ const minutesAgo: MinutesAgoFunction = (minutes: number): Date => {
44
+ return OneUptimeDate.getSomeMinutesAgo(minutes);
45
+ };
46
+
47
+ type StatusTextFunction = () => string;
48
+
49
+ /*
50
+ * Statusbubble renders role="status" with aria-label "Status: <text>", so the
51
+ * accessible name is the assertion surface — it is what a screen reader
52
+ * announces, not just what happens to be in a div.
53
+ */
54
+ const statusText: StatusTextFunction = (): string => {
55
+ return screen.getByRole("status").getAttribute("aria-label") || "";
56
+ };
57
+
58
+ describe("RunnerStatusElement", () => {
59
+ describe("never connected", () => {
60
+ test("a Runner with no lastAlive reads 'Never connected'", () => {
61
+ renderRunner({ lastAlive: null });
62
+
63
+ expect(screen.getByText("Never connected")).toBeInTheDocument();
64
+ expect(statusText()).toBe("Status: Never connected");
65
+ });
66
+
67
+ test("a Runner with lastAlive absent entirely reads 'Never connected'", () => {
68
+ renderRunner({});
69
+
70
+ expect(screen.getByText("Never connected")).toBeInTheDocument();
71
+ });
72
+
73
+ /*
74
+ * The whole point of the third state. A Runner created ten seconds ago
75
+ * whose container has not been started is not a failure, and must not be
76
+ * dressed as one — it used to render the same red "Disconnected" as a
77
+ * Runner that crashed in production.
78
+ */
79
+ test("it is not called Disconnected", () => {
80
+ renderRunner({ lastAlive: null });
81
+
82
+ expect(screen.queryByText("Disconnected")).not.toBeInTheDocument();
83
+ expect(statusText()).not.toContain("Disconnected");
84
+ });
85
+
86
+ test("it is grey, not red", () => {
87
+ const { container } = render(
88
+ <RunnerStatusElement runner={{ lastAlive: null }} />,
89
+ );
90
+
91
+ const html: string = container.innerHTML;
92
+
93
+ // Gray500 #6b7280, Red #fd625e.
94
+ expect(html).toContain("107, 114, 128");
95
+ expect(html).not.toContain("253, 98, 94");
96
+ });
97
+
98
+ test("it does not animate", () => {
99
+ const { container } = render(
100
+ <RunnerStatusElement runner={{ lastAlive: null }} />,
101
+ );
102
+
103
+ expect(container.innerHTML).not.toContain("animate-ping");
104
+ });
105
+ });
106
+
107
+ describe("connected", () => {
108
+ test("a heartbeat right now reads 'Connected'", () => {
109
+ renderRunner({ lastAlive: OneUptimeDate.getCurrentDate() });
110
+
111
+ expect(screen.getByText("Connected")).toBeInTheDocument();
112
+ expect(statusText()).toBe("Status: Connected");
113
+ });
114
+
115
+ test("a heartbeat four minutes ago still reads 'Connected'", () => {
116
+ renderRunner({ lastAlive: minutesAgo(4) });
117
+
118
+ expect(screen.getByText("Connected")).toBeInTheDocument();
119
+ });
120
+
121
+ test("it is green and animating", () => {
122
+ const { container } = render(
123
+ <RunnerStatusElement
124
+ runner={{ lastAlive: OneUptimeDate.getCurrentDate() }}
125
+ />,
126
+ );
127
+
128
+ // Green #2ab57d.
129
+ expect(container.innerHTML).toContain("42, 181, 125");
130
+ expect(container.innerHTML).toContain("animate-ping");
131
+ });
132
+ });
133
+
134
+ describe("disconnected", () => {
135
+ test("a heartbeat an hour ago reads 'Disconnected'", () => {
136
+ renderRunner({ lastAlive: minutesAgo(60) });
137
+
138
+ expect(statusText()).toContain("Disconnected");
139
+ });
140
+
141
+ /*
142
+ * "How long has it been gone" is the entire question when a Runner drops,
143
+ * and the detail card has no other column carrying it.
144
+ */
145
+ test("it appends how long ago the last heartbeat was", () => {
146
+ renderRunner({ lastAlive: minutesAgo(60) });
147
+
148
+ expect(statusText()).toBe("Status: Disconnected · an hour ago");
149
+ });
150
+
151
+ test("a day-old heartbeat says a day ago", () => {
152
+ renderRunner({ lastAlive: minutesAgo(60 * 24) });
153
+
154
+ expect(statusText()).toBe("Status: Disconnected · a day ago");
155
+ });
156
+
157
+ /*
158
+ * In the Runners table a Last Seen column sits directly beside this one,
159
+ * so repeating the relative time would be noise.
160
+ */
161
+ test("showLastSeen={false} suppresses the relative time", () => {
162
+ renderRunner({ lastAlive: minutesAgo(60) }, false);
163
+
164
+ expect(statusText()).toBe("Status: Disconnected");
165
+ });
166
+
167
+ test("showLastSeen={true} is the same as the default", () => {
168
+ renderRunner({ lastAlive: minutesAgo(60) }, true);
169
+
170
+ expect(statusText()).toBe("Status: Disconnected · an hour ago");
171
+ });
172
+
173
+ test("it is red and does not animate", () => {
174
+ const { container } = render(
175
+ <RunnerStatusElement runner={{ lastAlive: minutesAgo(60) }} />,
176
+ );
177
+
178
+ // Red #fd625e.
179
+ expect(container.innerHTML).toContain("253, 98, 94");
180
+ expect(container.innerHTML).not.toContain("animate-ping");
181
+ });
182
+ });
183
+
184
+ /*
185
+ * The regression suite. Each of these is a shape the old inline
186
+ * implementations got wrong.
187
+ */
188
+ describe("connectionStatus is ignored", () => {
189
+ test("stale lastAlive reads Disconnected even when connectionStatus says Connected", () => {
190
+ renderRunner({
191
+ connectionStatus: RunnerConnectionStatus.Connected,
192
+ lastAlive: minutesAgo(60 * 24 * 90),
193
+ });
194
+
195
+ expect(statusText()).toContain("Disconnected");
196
+ expect(screen.queryByText("Connected")).not.toBeInTheDocument();
197
+ });
198
+
199
+ test("fresh lastAlive reads Connected even when connectionStatus says Disconnected", () => {
200
+ renderRunner({
201
+ connectionStatus: RunnerConnectionStatus.Disconnected,
202
+ lastAlive: OneUptimeDate.getCurrentDate(),
203
+ });
204
+
205
+ expect(statusText()).toBe("Status: Connected");
206
+ });
207
+
208
+ /*
209
+ * Exactly what RunnerService.onBeforeCreate produces: connectionStatus
210
+ * defaulted to Disconnected, lastAlive still null. The user has not
211
+ * failed at anything yet — they have not run the Docker command.
212
+ */
213
+ test("a freshly created Runner reads Never connected, not Disconnected", () => {
214
+ renderRunner({
215
+ connectionStatus: RunnerConnectionStatus.Disconnected,
216
+ lastAlive: null,
217
+ });
218
+
219
+ expect(statusText()).toBe("Status: Never connected");
220
+ });
221
+
222
+ test("connectionStatus alone never produces a Connected bubble", () => {
223
+ renderRunner({ connectionStatus: RunnerConnectionStatus.Connected });
224
+
225
+ expect(statusText()).toBe("Status: Never connected");
226
+ });
227
+ });
228
+
229
+ describe("input shapes", () => {
230
+ /*
231
+ * The table hands over a hydrated model; ModelDetail's getElement hands
232
+ * over the same. The API layer hands over a plain object with an ISO
233
+ * string. All three have to work.
234
+ */
235
+ test("accepts a hydrated Runner model", () => {
236
+ const runner: Runner = new Runner();
237
+ runner.lastAlive = OneUptimeDate.getCurrentDate();
238
+
239
+ renderRunner(runner);
240
+
241
+ expect(statusText()).toBe("Status: Connected");
242
+ });
243
+
244
+ test("accepts a hydrated Runner model that never connected", () => {
245
+ renderRunner(new Runner());
246
+
247
+ expect(statusText()).toBe("Status: Never connected");
248
+ });
249
+
250
+ test("accepts an ISO string lastAlive", () => {
251
+ renderRunner({ lastAlive: new Date().toISOString() });
252
+
253
+ expect(statusText()).toBe("Status: Connected");
254
+ });
255
+
256
+ test("accepts a stale ISO string lastAlive", () => {
257
+ renderRunner({ lastAlive: minutesAgo(60).toISOString() });
258
+
259
+ expect(statusText()).toContain("Disconnected");
260
+ });
261
+
262
+ /*
263
+ * An unparseable timestamp must not be rendered as a healthy Runner. It
264
+ * falls to Disconnected, and the appended relative time is suppressed
265
+ * rather than printed as "Invalid date".
266
+ */
267
+ test("an unparseable lastAlive is Disconnected and prints no garbage time", () => {
268
+ renderRunner({ lastAlive: "not-a-date" });
269
+
270
+ expect(statusText()).toContain("Disconnected");
271
+ expect(statusText()).not.toContain("Invalid");
272
+ });
273
+ });
274
+ });