@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
@@ -0,0 +1,621 @@
1
+ import BadDataException from "../../../Types/Exception/BadDataException";
2
+ import Dictionary from "../../../Types/Dictionary";
3
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
4
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
5
+ import EqualToOrNull from "../../../Types/BaseDatabase/EqualToOrNull";
6
+ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
7
+ import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
8
+ import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
9
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
10
+ import Includes from "../../../Types/BaseDatabase/Includes";
11
+ import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
12
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
13
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
14
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
15
+ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
16
+ import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
17
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
18
+ import NotEqual from "../../../Types/BaseDatabase/NotEqual";
19
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
20
+ import Search from "../../../Types/BaseDatabase/Search";
21
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import Text from "../../../Types/Text";
24
+ import { JSONObject, JSONValue } from "../../../Types/JSON";
25
+
26
+ /*
27
+ * ---------------------------------------------------------------------------
28
+ * Querying keys inside a jsonb column
29
+ * ---------------------------------------------------------------------------
30
+ *
31
+ * `customFields` (and every other jsonb column) stores a flat object keyed by
32
+ * the custom field definition's *name*, so "incidents whose Team is Payments"
33
+ * means reaching inside the column for one key.
34
+ *
35
+ * Three properties of this module are load-bearing:
36
+ *
37
+ * 1. THE KEY IS A BOUND PARAMETER, never interpolated. A custom field's name
38
+ * is user-supplied text that arrives on the query from the browser, so
39
+ * splicing it into the SQL string — which is what this code used to do —
40
+ * let anyone who could name a key write SQL into the WHERE clause.
41
+ *
42
+ * 2. A VALUE MATCHES WHETHER IT IS STORED AS A SCALAR OR INSIDE AN ARRAY.
43
+ * A single-select dropdown stores `"Payments"`; the same field switched to
44
+ * multi-select stores `["Payments", "Billing"]`. Both have to answer yes to
45
+ * "Team is Payments", or a project that changed a field's type would find
46
+ * its old rows quietly missing from every filter.
47
+ *
48
+ * 3. A ROW MISSING THE KEY SATISFIES A NEGATIVE FILTER. "Team is not
49
+ * Payments" includes the incidents with no Team at all — which is what the
50
+ * equivalent ClickHouse attribute path already does (StatementGenerator),
51
+ * so the two read the same way in the product.
52
+ *
53
+ * Operators reuse the vocabulary the rest of the query layer already speaks
54
+ * (Search, Includes, NotEqual, GreaterThan, IsNull, ...), so a filter built in
55
+ * the browser serializes and reconstructs with no new wire format.
56
+ *
57
+ * Every predicate is composed from `JsonAccessor`, which is derived from the
58
+ * column reference only at render time — parameters are bound once, up front,
59
+ * so `toSql` can be called more than once without minting new placeholders.
60
+ */
61
+
62
+ /**
63
+ * The two ways to read one key out of a jsonb column. `->>` yields text, for
64
+ * comparison; `->` yields jsonb, for array containment and type inspection.
65
+ */
66
+ interface JsonAccessor {
67
+ /** `col ->> 'key'` — the value as text. NULL when the key is absent. */
68
+ asText: string;
69
+ /** `col -> 'key'` — the value as jsonb. NULL when the key is absent. */
70
+ asJson: string;
71
+ }
72
+
73
+ /** A WHERE fragment that still needs to be told which column it reads. */
74
+ type PredicateFragment = (accessor: JsonAccessor) => string;
75
+
76
+ export interface JSONColumnQuery {
77
+ /**
78
+ * Bound parameters for the emitted SQL. The names are opaque; nothing may
79
+ * depend on them beyond handing them to TypeORM's `Raw`.
80
+ */
81
+ parameters: Dictionary<JSONValue>;
82
+ /**
83
+ * Render the WHERE fragment for an already-quoted column reference such as
84
+ * `"Incident"."customFields"`.
85
+ */
86
+ toSql: (columnReference: string) => string;
87
+ }
88
+
89
+ export type ParameterNameGeneratorFunction = () => string;
90
+
91
+ /**
92
+ * Matches an optionally-signed decimal with optional surrounding whitespace.
93
+ * Guards the numeric cast: a jsonb value is arbitrary text, and
94
+ * `CAST('abc' AS NUMERIC)` aborts the whole query rather than skipping a row.
95
+ *
96
+ * This is load-bearing rather than defensive. A Number custom field is stored
97
+ * as whatever the text input produced — `"42"`, not `42` — so every numeric
98
+ * comparison runs through a cast, on rows whose value may be anything at all.
99
+ */
100
+ const NUMERIC_TEXT_REGEX: string = "^\\s*-?[0-9]+(\\.[0-9]+)?\\s*$";
101
+
102
+ /*
103
+ * Sanity bounds on an incoming JSON query. Nothing the product builds comes
104
+ * close to these — a project's custom fields are capped well below the key
105
+ * limit and a facet selection below the value limit — so exceeding one means
106
+ * a hand-built request, and answering it with a 400 is better than compiling
107
+ * a megabyte of SQL.
108
+ */
109
+ export const MAX_JSON_QUERY_KEYS: number = 50;
110
+ export const MAX_JSON_QUERY_VALUES_PER_KEY: number = 200;
111
+ export const MAX_JSON_QUERY_KEY_LENGTH: number = 500;
112
+
113
+ /**
114
+ * Escape the characters Postgres' LIKE grammar reserves, so a custom field
115
+ * value containing `%` filters for a literal `%` rather than for anything.
116
+ *
117
+ * `\` is LIKE's default escape character, so it has to be doubled first.
118
+ */
119
+ export type EscapeLikePatternFunction = (value: string) => string;
120
+
121
+ export const escapeLikePattern: EscapeLikePatternFunction = (
122
+ value: string,
123
+ ): string => {
124
+ return value.replace(/([\\%_])/g, "\\$1");
125
+ };
126
+
127
+ type IsScalarFunction = (value: unknown) => value is string | number | boolean;
128
+
129
+ const isScalar: IsScalarFunction = (
130
+ value: unknown,
131
+ ): value is string | number | boolean => {
132
+ return (
133
+ typeof value === "string" ||
134
+ typeof value === "number" ||
135
+ typeof value === "boolean"
136
+ );
137
+ };
138
+
139
+ type ScalarValue = string | number | boolean;
140
+
141
+ type ToScalarFunction = (value: unknown) => ScalarValue;
142
+
143
+ /**
144
+ * Reduce an operator's payload to something comparable against jsonb.
145
+ * ObjectID and friends stringify; primitives keep their type so the
146
+ * containment literal below is `[42]` rather than `["42"]`.
147
+ */
148
+ const toScalar: ToScalarFunction = (value: unknown): ScalarValue => {
149
+ if (isScalar(value)) {
150
+ return value;
151
+ }
152
+
153
+ if (value instanceof ObjectID) {
154
+ return value.toString();
155
+ }
156
+
157
+ if (value === null || value === undefined) {
158
+ return "";
159
+ }
160
+
161
+ return String(value);
162
+ };
163
+
164
+ type ToScalarArrayFunction = (values: unknown) => Array<ScalarValue>;
165
+
166
+ const toScalarArray: ToScalarArrayFunction = (
167
+ values: unknown,
168
+ ): Array<ScalarValue> => {
169
+ if (!Array.isArray(values)) {
170
+ return [];
171
+ }
172
+
173
+ return values.map((value: unknown) => {
174
+ return toScalar(value);
175
+ });
176
+ };
177
+
178
+ type IsNumericValueFunction = (value: unknown) => boolean;
179
+
180
+ const isNumericValue: IsNumericValueFunction = (value: unknown): boolean => {
181
+ if (typeof value === "number") {
182
+ return Number.isFinite(value);
183
+ }
184
+
185
+ if (typeof value === "string" && value.trim() !== "") {
186
+ return Number.isFinite(Number(value));
187
+ }
188
+
189
+ return false;
190
+ };
191
+
192
+ /**
193
+ * Builds one key's predicate. Every helper binds its parameters immediately
194
+ * and returns a fragment that only needs the column reference, so the caller
195
+ * owns a complete parameter bag before any SQL is rendered.
196
+ */
197
+ class KeyPredicateBuilder {
198
+ private parameters: Dictionary<JSONValue>;
199
+ private generateParameterName: ParameterNameGeneratorFunction;
200
+ private keyParameterName: string;
201
+
202
+ public constructor(data: {
203
+ parameters: Dictionary<JSONValue>;
204
+ generateParameterName: ParameterNameGeneratorFunction;
205
+ key: string;
206
+ }) {
207
+ this.parameters = data.parameters;
208
+ this.generateParameterName = data.generateParameterName;
209
+ this.keyParameterName = this.bind(data.key);
210
+ }
211
+
212
+ /** Register a value and return its `:name` placeholder. */
213
+ private bind(value: JSONValue): string {
214
+ const name: string = this.generateParameterName();
215
+ this.parameters[name] = value;
216
+ return `:${name}`;
217
+ }
218
+
219
+ /*
220
+ * The key is cast explicitly because `->` / `->>` are overloaded on
221
+ * (jsonb, text) and (jsonb, integer); an untyped bind parameter would leave
222
+ * Postgres unable to choose and fail with "operator is not unique".
223
+ */
224
+ public accessorFor(columnReference: string): JsonAccessor {
225
+ return {
226
+ asText: `${columnReference} ->> CAST(${this.keyParameterName} AS TEXT)`,
227
+ asJson: `${columnReference} -> CAST(${this.keyParameterName} AS TEXT)`,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * "The value at this key is `value`" — true for a scalar that equals it and
233
+ * for an array that contains it.
234
+ */
235
+ private equals(value: ScalarValue): PredicateFragment {
236
+ const textParameter: string = this.bind(String(value));
237
+ const jsonParameter: string = this.bind(JSON.stringify([value]));
238
+
239
+ return (accessor: JsonAccessor): string => {
240
+ return `(${accessor.asText} = CAST(${textParameter} AS TEXT) OR (jsonb_typeof(${accessor.asJson}) = 'array' AND ${accessor.asJson} @> CAST(${jsonParameter} AS JSONB)))`;
241
+ };
242
+ }
243
+
244
+ private join(
245
+ fragments: Array<PredicateFragment>,
246
+ operator: "AND" | "OR",
247
+ ): PredicateFragment {
248
+ return (accessor: JsonAccessor): string => {
249
+ return `(${fragments
250
+ .map((fragment: PredicateFragment) => {
251
+ return fragment(accessor);
252
+ })
253
+ .join(` ${operator} `)})`;
254
+ };
255
+ }
256
+
257
+ private anyOf(values: Array<ScalarValue>): PredicateFragment {
258
+ return this.join(
259
+ values.map((value: ScalarValue) => {
260
+ return this.equals(value);
261
+ }),
262
+ "OR",
263
+ );
264
+ }
265
+
266
+ private allOf(values: Array<ScalarValue>): PredicateFragment {
267
+ return this.join(
268
+ values.map((value: ScalarValue) => {
269
+ return this.equals(value);
270
+ }),
271
+ "AND",
272
+ );
273
+ }
274
+
275
+ /**
276
+ * `pattern` arrives with its wildcards already placed by the caller; the
277
+ * needle inside it has been escaped, so only the caller's own `%` are live.
278
+ *
279
+ * An array-valued key is matched against its JSON text (`["a", "b"]`), which
280
+ * is what makes "contains" work on a multi-select without a second branch.
281
+ */
282
+ private like(pattern: string): PredicateFragment {
283
+ const parameter: string = this.bind(pattern);
284
+
285
+ return (accessor: JsonAccessor): string => {
286
+ return `(${accessor.asText} ILIKE ${parameter})`;
287
+ };
288
+ }
289
+
290
+ /**
291
+ * The value at this key as NUMERIC, or NULL when it does not look like a
292
+ * number. A CASE rather than an AND guard because Postgres does not promise
293
+ * to evaluate the guard first, and one stray cast error aborts the query
294
+ * instead of skipping the row.
295
+ */
296
+ private numericExpression(accessor: JsonAccessor): string {
297
+ return `(CASE WHEN ${accessor.asText} ~ '${NUMERIC_TEXT_REGEX}' THEN CAST(${accessor.asText} AS NUMERIC) ELSE NULL END)`;
298
+ }
299
+
300
+ private compareNumeric(operator: string, value: unknown): PredicateFragment {
301
+ const parameter: string = this.bind(Number(toScalar(value)));
302
+
303
+ return (accessor: JsonAccessor): string => {
304
+ return `(${this.numericExpression(accessor)} ${operator} CAST(${parameter} AS NUMERIC))`;
305
+ };
306
+ }
307
+
308
+ private compareText(operator: string, value: unknown): PredicateFragment {
309
+ const parameter: string = this.bind(String(toScalar(value)));
310
+
311
+ return (accessor: JsonAccessor): string => {
312
+ return `(${accessor.asText} ${operator} CAST(${parameter} AS TEXT))`;
313
+ };
314
+ }
315
+
316
+ /**
317
+ * "No value here" — the key is absent, explicitly JSON null, an empty
318
+ * string, or an empty array. A multi-select cleared in the UI leaves `[]`
319
+ * behind rather than dropping the key, and a user reading "is empty" on the
320
+ * chip means all four.
321
+ */
322
+ private isEmpty(): PredicateFragment {
323
+ return (accessor: JsonAccessor): string => {
324
+ return `(${accessor.asJson} IS NULL OR jsonb_typeof(${accessor.asJson}) = 'null' OR ${accessor.asText} = '' OR (jsonb_typeof(${accessor.asJson}) = 'array' AND jsonb_array_length(${accessor.asJson}) = 0))`;
325
+ };
326
+ }
327
+
328
+ /*
329
+ * `IS NOT TRUE` rather than `NOT (...)`: a row missing the key compares to
330
+ * NULL, and `NOT NULL` is NULL — which would exclude exactly the rows a
331
+ * negative filter is asked about.
332
+ */
333
+ private negate(fragment: PredicateFragment): PredicateFragment {
334
+ return (accessor: JsonAccessor): string => {
335
+ return `(${fragment(accessor)} IS NOT TRUE)`;
336
+ };
337
+ }
338
+
339
+ /**
340
+ * A multi-value condition expands to one OR-ed predicate per value, so an
341
+ * unbounded list is an unbounded query. Refuse rather than compile it: the
342
+ * caller gets a 400 they can act on instead of a statement the database
343
+ * spends a minute planning.
344
+ */
345
+ private assertValueCountWithinLimit(value: unknown): void {
346
+ let count: number = 0;
347
+
348
+ if (Array.isArray(value)) {
349
+ count = value.length;
350
+ } else if (
351
+ value instanceof Includes ||
352
+ value instanceof IncludesAll ||
353
+ value instanceof IncludesNone
354
+ ) {
355
+ count = (value.values || []).length;
356
+ }
357
+
358
+ if (count > MAX_JSON_QUERY_VALUES_PER_KEY) {
359
+ throw new BadDataException(
360
+ `A JSON column filter cannot match against more than ${MAX_JSON_QUERY_VALUES_PER_KEY} values at once.`,
361
+ );
362
+ }
363
+ }
364
+
365
+ private containsJson(value: unknown): PredicateFragment {
366
+ const parameter: string = this.bind(JSON.stringify(value));
367
+
368
+ return (accessor: JsonAccessor): string => {
369
+ return `(${accessor.asJson} @> CAST(${parameter} AS JSONB))`;
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Build this key's predicate, or null when the condition constrains nothing
375
+ * — an "is any of" with nothing selected. Dropping it is right: narrowing to
376
+ * zero rows would hide the table behind a filter the user just cleared.
377
+ */
378
+ public build(value: unknown): PredicateFragment | null {
379
+ this.assertValueCountWithinLimit(value);
380
+
381
+ if (value instanceof IsNull) {
382
+ return this.isEmpty();
383
+ }
384
+
385
+ if (value instanceof NotNull) {
386
+ return this.negate(this.isEmpty());
387
+ }
388
+
389
+ if (value instanceof IncludesAll) {
390
+ const values: Array<ScalarValue> = toScalarArray(value.values);
391
+ return values.length === 0 ? null : this.allOf(values);
392
+ }
393
+
394
+ if (value instanceof IncludesNone) {
395
+ const values: Array<ScalarValue> = toScalarArray(value.values);
396
+ return values.length === 0 ? null : this.negate(this.anyOf(values));
397
+ }
398
+
399
+ if (value instanceof Includes) {
400
+ const values: Array<ScalarValue> = toScalarArray(value.values);
401
+ return values.length === 0 ? null : this.anyOf(values);
402
+ }
403
+
404
+ if (value instanceof NotEqual) {
405
+ return this.negate(this.equals(toScalar(value.value)));
406
+ }
407
+
408
+ if (value instanceof EqualToOrNull) {
409
+ return this.join(
410
+ [this.equals(toScalar(value.value)), this.isEmpty()],
411
+ "OR",
412
+ );
413
+ }
414
+
415
+ if (value instanceof EqualTo) {
416
+ return this.equals(toScalar(value.value));
417
+ }
418
+
419
+ if (value instanceof NotContains) {
420
+ return this.negate(
421
+ this.like(`%${escapeLikePattern(String(toScalar(value.value)))}%`),
422
+ );
423
+ }
424
+
425
+ if (value instanceof StartsWith) {
426
+ return this.like(`${escapeLikePattern(String(toScalar(value.value)))}%`);
427
+ }
428
+
429
+ if (value instanceof EndsWith) {
430
+ return this.like(`%${escapeLikePattern(String(toScalar(value.value)))}`);
431
+ }
432
+
433
+ if (value instanceof Search) {
434
+ return this.like(`%${escapeLikePattern(String(toScalar(value.value)))}%`);
435
+ }
436
+
437
+ if (value instanceof InBetween) {
438
+ const start: unknown = value.startValue;
439
+ const end: unknown = value.endValue;
440
+
441
+ if (isNumericValue(start) && isNumericValue(end)) {
442
+ return this.join(
443
+ [this.compareNumeric(">=", start), this.compareNumeric("<=", end)],
444
+ "AND",
445
+ );
446
+ }
447
+
448
+ return this.join(
449
+ [this.compareText(">=", start), this.compareText("<=", end)],
450
+ "AND",
451
+ );
452
+ }
453
+
454
+ if (value instanceof GreaterThanOrNull) {
455
+ return this.join(
456
+ [this.compareNumeric(">=", value.value), this.isEmpty()],
457
+ "OR",
458
+ );
459
+ }
460
+
461
+ if (value instanceof LessThanOrNull) {
462
+ return this.join(
463
+ [this.compareNumeric("<=", value.value), this.isEmpty()],
464
+ "OR",
465
+ );
466
+ }
467
+
468
+ if (value instanceof GreaterThanOrEqual) {
469
+ return this.compareNumeric(">=", value.value);
470
+ }
471
+
472
+ if (value instanceof GreaterThan) {
473
+ return this.compareNumeric(">", value.value);
474
+ }
475
+
476
+ if (value instanceof LessThanOrEqual) {
477
+ return this.compareNumeric("<=", value.value);
478
+ }
479
+
480
+ if (value instanceof LessThan) {
481
+ return this.compareNumeric("<", value.value);
482
+ }
483
+
484
+ /*
485
+ * A bare array reads as "is any of" — the same meaning QueryUtil gives a
486
+ * top-level array, which it wraps in an ANY.
487
+ */
488
+ if (Array.isArray(value)) {
489
+ const values: Array<ScalarValue> = toScalarArray(value);
490
+ return values.length === 0 ? null : this.anyOf(values);
491
+ }
492
+
493
+ if (isScalar(value)) {
494
+ return this.equals(value);
495
+ }
496
+
497
+ if (value === null || value === undefined) {
498
+ return this.isEmpty();
499
+ }
500
+
501
+ /*
502
+ * Anything left is a nested object. Containment is the only sensible
503
+ * reading, and a strict improvement on what this used to do — compare the
504
+ * column's text against "[object Object]".
505
+ */
506
+ return this.containsJson(value);
507
+ }
508
+ }
509
+
510
+ export type BuildJSONColumnQueryFunction = (data: {
511
+ value: JSONObject | string;
512
+ generateParameterName?: ParameterNameGeneratorFunction | undefined;
513
+ }) => JSONColumnQuery;
514
+
515
+ /**
516
+ * Turn `{ Team: Includes(["Payments"]), Region: "us-east-1" }` into one
517
+ * AND-joined WHERE fragment over a jsonb column, plus the parameters it binds.
518
+ *
519
+ * An empty object keeps its historical meaning — "this column holds nothing" —
520
+ * because that is what a JSON filter with every row cleared has always sent.
521
+ */
522
+ const buildJSONColumnQuery: BuildJSONColumnQueryFunction = (data: {
523
+ value: JSONObject | string;
524
+ generateParameterName?: ParameterNameGeneratorFunction | undefined;
525
+ }): JSONColumnQuery => {
526
+ const generateParameterName: ParameterNameGeneratorFunction =
527
+ data.generateParameterName ||
528
+ ((): string => {
529
+ return Text.generateRandomText(10);
530
+ });
531
+
532
+ let value: JSONObject = {};
533
+
534
+ if (typeof data.value === "string") {
535
+ try {
536
+ value = JSON.parse(data.value) as JSONObject;
537
+ } catch {
538
+ /*
539
+ * A JSON column query that is not parseable JSON cannot be turned into a
540
+ * predicate. Falling back to the empty object below means "this column
541
+ * holds nothing", which is at least a filter the caller can see the
542
+ * effect of — as opposed to a 500.
543
+ */
544
+ value = {};
545
+ }
546
+ } else {
547
+ value = data.value || {};
548
+ }
549
+
550
+ const parameters: Dictionary<JSONValue> = {};
551
+ const fragments: Array<(columnReference: string) => string> = [];
552
+
553
+ const keys: Array<string> = Object.keys(value || {});
554
+
555
+ if (keys.length > MAX_JSON_QUERY_KEYS) {
556
+ throw new BadDataException(
557
+ `A JSON column filter cannot carry more than ${MAX_JSON_QUERY_KEYS} keys.`,
558
+ );
559
+ }
560
+
561
+ for (const key of keys) {
562
+ if (typeof key !== "string" || key.length === 0) {
563
+ continue;
564
+ }
565
+
566
+ if (key.length > MAX_JSON_QUERY_KEY_LENGTH) {
567
+ throw new BadDataException(
568
+ `A JSON column filter key cannot be longer than ${MAX_JSON_QUERY_KEY_LENGTH} characters.`,
569
+ );
570
+ }
571
+
572
+ const builder: KeyPredicateBuilder = new KeyPredicateBuilder({
573
+ parameters: parameters,
574
+ generateParameterName: generateParameterName,
575
+ key: key,
576
+ });
577
+
578
+ const fragment: PredicateFragment | null = builder.build(
579
+ (value as JSONObject)[key],
580
+ );
581
+
582
+ if (!fragment) {
583
+ continue;
584
+ }
585
+
586
+ fragments.push((columnReference: string): string => {
587
+ return fragment(builder.accessorFor(columnReference));
588
+ });
589
+ }
590
+
591
+ return {
592
+ parameters: parameters,
593
+ toSql: (columnReference: string): string => {
594
+ if (keys.length === 0) {
595
+ /*
596
+ * Historical meaning of `{}`, kept verbatim: the column itself is
597
+ * empty. Changing it would silently retarget every saved filter that
598
+ * has ever sent an empty JSON filter.
599
+ */
600
+ return `(${columnReference} IS NULL OR ${columnReference} = '{}')`;
601
+ }
602
+
603
+ if (fragments.length === 0) {
604
+ /*
605
+ * Every condition turned out to constrain nothing. Matching everything
606
+ * is right; matching nothing would hide the whole table behind a
607
+ * filter the user had cleared.
608
+ */
609
+ return "(1 = 1)";
610
+ }
611
+
612
+ return `(${fragments
613
+ .map((fragment: (columnReference: string) => string) => {
614
+ return fragment(columnReference);
615
+ })
616
+ .join(" AND ")})`;
617
+ },
618
+ };
619
+ };
620
+
621
+ export default buildJSONColumnQuery;
@@ -35,9 +35,16 @@ export default class PublicPermission {
35
35
  return;
36
36
  }
37
37
 
38
- // this means the record is not publicly createable and the user is not logged in.
38
+ /*
39
+ * The record is not publicly accessible and the caller presented
40
+ * neither a session nor an API key. Name both, the way the analytics
41
+ * counterpart of this check already does — the old wording talked only
42
+ * about logging in, which reads as an RBAC failure on the named model
43
+ * to anyone authenticating by key, and repeatedly sent API callers off
44
+ * to audit permissions when their key had simply not arrived.
45
+ */
39
46
  throw new NotAuthenticatedException(
40
- `A user should be logged in to ${type} record of ${
47
+ `Authenticated user or a valid API key is needed to ${type} record of ${
41
48
  new modelType().singularName
42
49
  }.`,
43
50
  );