@oneuptime/common 12.0.14 → 12.0.15

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 (195) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/StatusPage.ts +7 -4
  6. package/Server/API/StatusPageAPI.ts +29 -4
  7. package/Server/EnvironmentConfig.ts +7 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/SlackAuthorization.ts +43 -6
  12. package/Server/Middleware/UserAuthorization.ts +236 -26
  13. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  14. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  15. package/Server/Services/GlobalOidcService.ts +132 -0
  16. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  17. package/Server/Services/GlobalSsoService.ts +132 -0
  18. package/Server/Services/UserEmailService.ts +4 -4
  19. package/Server/Utils/CronTab.ts +73 -0
  20. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  21. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  22. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  23. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  24. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  25. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  26. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  27. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  28. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  29. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  30. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  31. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  32. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
  33. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  34. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  35. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  37. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  38. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  39. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  40. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  41. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
  42. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  43. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  44. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  45. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  46. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  47. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  48. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  49. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  50. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  51. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  52. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  53. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  54. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  55. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  56. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  57. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  58. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  59. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  60. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  61. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  62. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  63. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  65. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  66. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  67. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  68. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  69. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  70. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  71. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  72. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  73. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  74. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  75. package/Tests/Utils/Analytics.test.ts +187 -0
  76. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  77. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  78. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  79. package/Types/Marketing/MarketingConversion.ts +12 -0
  80. package/Types/Monitor/CriteriaFilter.ts +9 -0
  81. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  82. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  83. package/UI/Components/Forms/BasicForm.tsx +13 -1
  84. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  85. package/UI/Components/Forms/ModelForm.tsx +10 -1
  86. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  87. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  88. package/Utils/Analytics.ts +55 -2
  89. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  90. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  91. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  92. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  93. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  95. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  97. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  99. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  101. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  102. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  103. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +6 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  113. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  114. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  115. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  116. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  117. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  119. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  120. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  121. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  122. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  123. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  125. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +4 -4
  127. package/build/dist/Server/Utils/CronTab.js +66 -0
  128. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  129. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  130. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  131. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  132. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  133. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  134. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  135. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  136. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  137. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  138. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  139. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  140. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  141. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  142. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  144. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  145. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  146. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  147. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  148. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  149. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  150. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  151. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  152. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
  154. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  156. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  158. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  160. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  161. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  162. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  163. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  164. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  165. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  166. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  167. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  168. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  169. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  170. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  171. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  172. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  173. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  174. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  175. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  176. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  177. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  178. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  179. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  180. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  182. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  183. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  184. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  185. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  186. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  187. package/build/dist/Utils/Analytics.js +50 -2
  188. package/build/dist/Utils/Analytics.js.map +1 -1
  189. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  190. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  191. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  192. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  193. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  194. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  195. package/package.json +1 -1
@@ -0,0 +1,551 @@
1
+ import QueryHelper from "../../../../Server/Types/Database/QueryHelper";
2
+ import ObjectID from "../../../../Types/ObjectID";
3
+ import { FindOperator } from "typeorm";
4
+ import { describe, expect, it } from "@jest/globals";
5
+
6
+ /*
7
+ * QueryHelper's many-to-many builders turn a request like "teams tagged with
8
+ * any of these labels" into a correlated subquery over the join table, and its
9
+ * array builders (all / any / notIn / notInOrNull / equalToOrNull) each have a
10
+ * dedicated empty-input branch that decides whether an empty list fails OPEN
11
+ * (matches everything) or CLOSED (matches nothing).
12
+ *
13
+ * None of that is visible from the operator's JSON: TypeORM's Raw keeps the SQL
14
+ * behind a `getSql(alias)` generator and the bound values behind
15
+ * `objectLiteralParameters`. So every test here reaches through the FindOperator
16
+ * to those two surfaces. Two properties recur and both are load-bearing:
17
+ *
18
+ * 1. IDENTIFIERS ARE ESCAPED, VALUES ARE BOUND. The join table / column names
19
+ * are embedded in the SQL text (doubling any `"`), while the ids the caller
20
+ * is filtering on must appear ONLY in the parameter bag — never in the SQL
21
+ * string, or an id would be an injection vector.
22
+ *
23
+ * 2. EMPTY MEANS A SENTINEL, NOT A CRASH. An empty `IN ()` is a Postgres
24
+ * syntax error, so each builder short-circuits an empty list to a constant
25
+ * predicate whose direction (TRUE=TRUE vs TRUE=FALSE) is the point.
26
+ */
27
+
28
+ interface RawOperator {
29
+ type: string;
30
+ objectLiteralParameters: Record<string, unknown>;
31
+ /* TypeORM renders the SQL through this, given the column alias. */
32
+ getSql: (aliasPath: string) => string;
33
+ }
34
+
35
+ type AsRawFunction = (operator: unknown) => RawOperator;
36
+
37
+ const asRaw: AsRawFunction = (operator: unknown): RawOperator => {
38
+ return operator as unknown as RawOperator;
39
+ };
40
+
41
+ /* The alias a many-to-many filter is applied to: the owner's primary id. */
42
+ const OWNER_ID_ALIAS: string = '"Team"."_id"';
43
+
44
+ describe("QueryHelper.anyOfEntitiesInManyToMany", () => {
45
+ it("emits an IN-subquery over the join table, correlated on the relation column", () => {
46
+ const operator: RawOperator = asRaw(
47
+ QueryHelper.anyOfEntitiesInManyToMany({
48
+ values: ["label-1", "label-2"],
49
+ joinTableName: "team_label",
50
+ ownerColumnName: "teamId",
51
+ relationColumnName: "labelId",
52
+ }),
53
+ );
54
+
55
+ expect(operator).toBeInstanceOf(FindOperator);
56
+ expect(operator.type).toBe("raw");
57
+
58
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
59
+
60
+ /* Owner id is IN the set of owners linked to any requested relation id. */
61
+ expect(sql).toContain(`(${OWNER_ID_ALIAS} IN (SELECT`);
62
+ expect(sql).not.toContain("NOT IN");
63
+ expect(sql).toContain('SELECT "team_label"."teamId"');
64
+ expect(sql).toContain('FROM "team_label"');
65
+ expect(sql).toContain('WHERE "team_label"."labelId" IN (:...');
66
+ });
67
+
68
+ it("binds the relation ids as one array parameter, leaving none in the SQL text", () => {
69
+ const operator: RawOperator = asRaw(
70
+ QueryHelper.anyOfEntitiesInManyToMany({
71
+ values: ["label-1", "label-2"],
72
+ joinTableName: "team_label",
73
+ ownerColumnName: "teamId",
74
+ relationColumnName: "labelId",
75
+ }),
76
+ );
77
+
78
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
79
+
80
+ expect(sql).not.toContain("label-1");
81
+ expect(sql).not.toContain("label-2");
82
+
83
+ const boundValues: Array<unknown> = Object.values(
84
+ operator.objectLiteralParameters,
85
+ );
86
+ expect(boundValues).toEqual([["label-1", "label-2"]]);
87
+ });
88
+
89
+ it("stringifies ObjectID values and never leaks them into the SQL", () => {
90
+ const relationId: ObjectID = ObjectID.generate();
91
+
92
+ const operator: RawOperator = asRaw(
93
+ QueryHelper.anyOfEntitiesInManyToMany({
94
+ values: [relationId],
95
+ joinTableName: "team_label",
96
+ ownerColumnName: "teamId",
97
+ relationColumnName: "labelId",
98
+ }),
99
+ );
100
+
101
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
102
+
103
+ expect(sql).not.toContain(relationId.toString());
104
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
105
+ [relationId.toString()],
106
+ ]);
107
+ });
108
+
109
+ it("fails CLOSED on an empty list: a constant-false predicate with no parameters", () => {
110
+ const operator: RawOperator = asRaw(
111
+ QueryHelper.anyOfEntitiesInManyToMany({
112
+ values: [],
113
+ joinTableName: "team_label",
114
+ ownerColumnName: "teamId",
115
+ relationColumnName: "labelId",
116
+ }),
117
+ );
118
+
119
+ /*
120
+ * "linked to any of nothing" is nobody, so the operator must match no rows
121
+ * rather than compile an illegal `IN ()`.
122
+ */
123
+ expect(operator.getSql(OWNER_ID_ALIAS)).toBe("TRUE = FALSE");
124
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
125
+ });
126
+
127
+ it("doubles embedded double-quotes in the join-table and column identifiers", () => {
128
+ const operator: RawOperator = asRaw(
129
+ QueryHelper.anyOfEntitiesInManyToMany({
130
+ values: ["label-1"],
131
+ joinTableName: 'weird"table',
132
+ ownerColumnName: 'owner"col',
133
+ relationColumnName: 'rel"col',
134
+ }),
135
+ );
136
+
137
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
138
+
139
+ /* A raw `"` inside an identifier would end the quoted name early. */
140
+ expect(sql).toContain('"weird""table"');
141
+ expect(sql).toContain('"owner""col"');
142
+ expect(sql).toContain('"rel""col"');
143
+ });
144
+ });
145
+
146
+ describe("QueryHelper.noneEntitiesInManyToMany", () => {
147
+ it("emits a NOT IN-subquery over the join table", () => {
148
+ const operator: RawOperator = asRaw(
149
+ QueryHelper.noneEntitiesInManyToMany({
150
+ values: ["label-1", "label-2"],
151
+ joinTableName: "team_label",
152
+ ownerColumnName: "teamId",
153
+ relationColumnName: "labelId",
154
+ }),
155
+ );
156
+
157
+ expect(operator.type).toBe("raw");
158
+
159
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
160
+
161
+ expect(sql).toContain(`(${OWNER_ID_ALIAS} NOT IN (SELECT`);
162
+ expect(sql).toContain('WHERE "team_label"."labelId" IN (:...');
163
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
164
+ ["label-1", "label-2"],
165
+ ]);
166
+ });
167
+
168
+ it("fails OPEN on an empty list: a constant-true predicate with no parameters", () => {
169
+ const operator: RawOperator = asRaw(
170
+ QueryHelper.noneEntitiesInManyToMany({
171
+ values: [],
172
+ joinTableName: "team_label",
173
+ ownerColumnName: "teamId",
174
+ relationColumnName: "labelId",
175
+ }),
176
+ );
177
+
178
+ /*
179
+ * "linked to none of nothing" is everybody, so this is the one value-taking
180
+ * many-to-many builder whose empty case matches ALL rows.
181
+ */
182
+ expect(operator.getSql(OWNER_ID_ALIAS)).toBe("TRUE = TRUE");
183
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
184
+ });
185
+
186
+ it("escapes identifiers and keeps the filtered ids out of the SQL text", () => {
187
+ const operator: RawOperator = asRaw(
188
+ QueryHelper.noneEntitiesInManyToMany({
189
+ values: ["label-1"],
190
+ joinTableName: 'a"b',
191
+ ownerColumnName: "teamId",
192
+ relationColumnName: "labelId",
193
+ }),
194
+ );
195
+
196
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
197
+
198
+ expect(sql).toContain('"a""b"');
199
+ expect(sql).not.toContain("label-1");
200
+ });
201
+ });
202
+
203
+ describe("QueryHelper.allEntitiesInManyToMany", () => {
204
+ it("groups by owner and requires a DISTINCT relation count of every requested id", () => {
205
+ const operator: RawOperator = asRaw(
206
+ QueryHelper.allEntitiesInManyToMany({
207
+ values: ["label-1", "label-2", "label-3"],
208
+ joinTableName: "team_label",
209
+ ownerColumnName: "teamId",
210
+ relationColumnName: "labelId",
211
+ }),
212
+ );
213
+
214
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
215
+
216
+ expect(sql).toContain(`(${OWNER_ID_ALIAS} IN (SELECT`);
217
+ expect(sql).toContain('GROUP BY "team_label"."teamId"');
218
+ expect(sql).toContain('HAVING COUNT(DISTINCT "team_label"."labelId") >= :');
219
+ });
220
+
221
+ it("binds both the relation-id array and the required count (= number of values)", () => {
222
+ const operator: RawOperator = asRaw(
223
+ QueryHelper.allEntitiesInManyToMany({
224
+ values: ["label-1", "label-2", "label-3"],
225
+ joinTableName: "team_label",
226
+ ownerColumnName: "teamId",
227
+ relationColumnName: "labelId",
228
+ }),
229
+ );
230
+
231
+ const boundValues: Array<unknown> = Object.values(
232
+ operator.objectLiteralParameters,
233
+ );
234
+
235
+ /* One parameter is the id array; the other is the HAVING threshold. */
236
+ expect(boundValues).toEqual(
237
+ expect.arrayContaining([["label-1", "label-2", "label-3"], 3]),
238
+ );
239
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(2);
240
+ });
241
+
242
+ it("counts duplicate values verbatim — the threshold is the list length, not the distinct size", () => {
243
+ const operator: RawOperator = asRaw(
244
+ QueryHelper.allEntitiesInManyToMany({
245
+ values: ["label-1", "label-1"],
246
+ joinTableName: "team_label",
247
+ ownerColumnName: "teamId",
248
+ relationColumnName: "labelId",
249
+ }),
250
+ );
251
+
252
+ const boundValues: Array<unknown> = Object.values(
253
+ operator.objectLiteralParameters,
254
+ );
255
+
256
+ expect(boundValues).toEqual(
257
+ expect.arrayContaining([["label-1", "label-1"], 2]),
258
+ );
259
+ });
260
+
261
+ it("fails CLOSED on an empty list: a constant-false predicate with no parameters", () => {
262
+ const operator: RawOperator = asRaw(
263
+ QueryHelper.allEntitiesInManyToMany({
264
+ values: [],
265
+ joinTableName: "team_label",
266
+ ownerColumnName: "teamId",
267
+ relationColumnName: "labelId",
268
+ }),
269
+ );
270
+
271
+ expect(operator.getSql(OWNER_ID_ALIAS)).toBe("TRUE = FALSE");
272
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
273
+ });
274
+ });
275
+
276
+ describe("QueryHelper.anyEntitiesInManyToMany / noEntitiesInManyToMany", () => {
277
+ it("anyEntitiesInManyToMany matches owners that have at least one non-null join row", () => {
278
+ const operator: RawOperator = asRaw(
279
+ QueryHelper.anyEntitiesInManyToMany({
280
+ joinTableName: "team_label",
281
+ ownerColumnName: "teamId",
282
+ }),
283
+ );
284
+
285
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
286
+
287
+ expect(sql).toContain(`(${OWNER_ID_ALIAS} IN (SELECT`);
288
+ expect(sql).toContain('WHERE "team_label"."teamId" IS NOT NULL');
289
+ expect(sql).not.toContain("NOT IN");
290
+ /* This overload takes no values, so it binds nothing. */
291
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
292
+ });
293
+
294
+ it("noEntitiesInManyToMany matches owners with no join rows at all", () => {
295
+ const operator: RawOperator = asRaw(
296
+ QueryHelper.noEntitiesInManyToMany({
297
+ joinTableName: "team_label",
298
+ ownerColumnName: "teamId",
299
+ }),
300
+ );
301
+
302
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
303
+
304
+ expect(sql).toContain(`(${OWNER_ID_ALIAS} NOT IN (SELECT`);
305
+ expect(sql).toContain('WHERE "team_label"."teamId" IS NOT NULL');
306
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
307
+ });
308
+
309
+ it("neither presence-check references a relation column — only the owner column", () => {
310
+ const anyOperator: RawOperator = asRaw(
311
+ QueryHelper.anyEntitiesInManyToMany({
312
+ joinTableName: "team_label",
313
+ ownerColumnName: "teamId",
314
+ }),
315
+ );
316
+ const noneOperator: RawOperator = asRaw(
317
+ QueryHelper.noEntitiesInManyToMany({
318
+ joinTableName: "team_label",
319
+ ownerColumnName: "teamId",
320
+ }),
321
+ );
322
+
323
+ /* A presence check is purely about the owner side of the join. */
324
+ expect(anyOperator.getSql(OWNER_ID_ALIAS)).not.toContain("labelId");
325
+ expect(noneOperator.getSql(OWNER_ID_ALIAS)).not.toContain("labelId");
326
+ });
327
+
328
+ it("escapes identifiers in the value-free presence checks too", () => {
329
+ const operator: RawOperator = asRaw(
330
+ QueryHelper.anyEntitiesInManyToMany({
331
+ joinTableName: 'j"t',
332
+ ownerColumnName: 'o"c',
333
+ }),
334
+ );
335
+
336
+ const sql: string = operator.getSql(OWNER_ID_ALIAS);
337
+
338
+ expect(sql).toContain('"j""t"');
339
+ expect(sql).toContain('"o""c"');
340
+ });
341
+ });
342
+
343
+ describe("QueryHelper many-to-many — fail-open vs fail-closed on empty input", () => {
344
+ it("directs each empty case to the correct sentinel", () => {
345
+ const emptyValueData: {
346
+ values: Array<string>;
347
+ joinTableName: string;
348
+ ownerColumnName: string;
349
+ relationColumnName: string;
350
+ } = {
351
+ values: [],
352
+ joinTableName: "team_label",
353
+ ownerColumnName: "teamId",
354
+ relationColumnName: "labelId",
355
+ };
356
+
357
+ /*
358
+ * "any of" and "all of" nothing select nobody (fail closed); "none of"
359
+ * nothing selects everybody (fail open). Locking all three together makes
360
+ * an accidental flip of one obvious.
361
+ */
362
+ expect(
363
+ asRaw(QueryHelper.anyOfEntitiesInManyToMany(emptyValueData)).getSql(
364
+ OWNER_ID_ALIAS,
365
+ ),
366
+ ).toBe("TRUE = FALSE");
367
+ expect(
368
+ asRaw(QueryHelper.allEntitiesInManyToMany(emptyValueData)).getSql(
369
+ OWNER_ID_ALIAS,
370
+ ),
371
+ ).toBe("TRUE = FALSE");
372
+ expect(
373
+ asRaw(QueryHelper.noneEntitiesInManyToMany(emptyValueData)).getSql(
374
+ OWNER_ID_ALIAS,
375
+ ),
376
+ ).toBe("TRUE = TRUE");
377
+ });
378
+
379
+ it("mints a fresh parameter name per call so two filters can share one query", () => {
380
+ const first: RawOperator = asRaw(
381
+ QueryHelper.anyOfEntitiesInManyToMany({
382
+ values: ["a"],
383
+ joinTableName: "team_label",
384
+ ownerColumnName: "teamId",
385
+ relationColumnName: "labelId",
386
+ }),
387
+ );
388
+ const second: RawOperator = asRaw(
389
+ QueryHelper.anyOfEntitiesInManyToMany({
390
+ values: ["b"],
391
+ joinTableName: "team_label",
392
+ ownerColumnName: "teamId",
393
+ relationColumnName: "labelId",
394
+ }),
395
+ );
396
+
397
+ expect(Object.keys(first.objectLiteralParameters)[0]).not.toBe(
398
+ Object.keys(second.objectLiteralParameters)[0],
399
+ );
400
+ });
401
+ });
402
+
403
+ describe("QueryHelper.all — empty values fail closed", () => {
404
+ it("compiles a non-empty list into an `= ALL(:param)` predicate", () => {
405
+ const operator: RawOperator = asRaw(QueryHelper.all(["a", "b"]));
406
+
407
+ const sql: string = operator.getSql('"Alert"."monitorId"');
408
+
409
+ expect(sql).toContain('("Alert"."monitorId" = ALL(:');
410
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
411
+ ["a", "b"],
412
+ ]);
413
+ });
414
+
415
+ it("stringifies ObjectID members before binding them", () => {
416
+ const id: ObjectID = ObjectID.generate();
417
+
418
+ const operator: RawOperator = asRaw(QueryHelper.all([id]));
419
+
420
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
421
+ [id.toString()],
422
+ ]);
423
+ });
424
+
425
+ it("returns the constant-false sentinel with no parameters for an empty list", () => {
426
+ const operator: RawOperator = asRaw(QueryHelper.all([]));
427
+
428
+ expect(operator.getSql('"Alert"."monitorId"')).toBe("TRUE = FALSE");
429
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
430
+ });
431
+ });
432
+
433
+ describe("QueryHelper.any / in — empty values fail closed", () => {
434
+ it("compiles a non-empty list into an `IN (:...param)` predicate", () => {
435
+ const operator: RawOperator = asRaw(QueryHelper.any(["a", "b"]));
436
+
437
+ const sql: string = operator.getSql('"Alert"."monitorId"');
438
+
439
+ expect(sql).toContain('("Alert"."monitorId" IN (:...');
440
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
441
+ ["a", "b"],
442
+ ]);
443
+ });
444
+
445
+ it("coerces numeric members to strings (any delegates to the private in())", () => {
446
+ const operator: RawOperator = asRaw(QueryHelper.any([1, 2, 3]));
447
+
448
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
449
+ ["1", "2", "3"],
450
+ ]);
451
+ });
452
+
453
+ it("returns the constant-false sentinel with no parameters for an empty list", () => {
454
+ const operator: RawOperator = asRaw(QueryHelper.any([]));
455
+
456
+ expect(operator.getSql('"Alert"."monitorId"')).toBe("TRUE = FALSE");
457
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
458
+ });
459
+ });
460
+
461
+ describe("QueryHelper.notIn / notInOrNull — empty values fail open", () => {
462
+ it("compiles a non-empty notIn into a `NOT IN (:...param)` predicate", () => {
463
+ const operator: RawOperator = asRaw(QueryHelper.notIn(["a", "b"]));
464
+
465
+ const sql: string = operator.getSql('"Alert"."monitorId"');
466
+
467
+ expect(sql).toContain('("Alert"."monitorId" NOT IN (:...');
468
+ expect(sql).not.toContain("IS NULL");
469
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
470
+ ["a", "b"],
471
+ ]);
472
+ });
473
+
474
+ it("notInOrNull adds an `IS NULL` arm so unset rows are kept", () => {
475
+ const operator: RawOperator = asRaw(QueryHelper.notInOrNull(["a"]));
476
+
477
+ const sql: string = operator.getSql('"Alert"."monitorId"');
478
+
479
+ expect(sql).toContain("NOT IN (:...");
480
+ expect(sql).toContain('or "Alert"."monitorId" IS NULL');
481
+ });
482
+
483
+ it("both fail OPEN on an empty list: constant-true, no parameters", () => {
484
+ const notInOperator: RawOperator = asRaw(QueryHelper.notIn([]));
485
+ const notInOrNullOperator: RawOperator = asRaw(QueryHelper.notInOrNull([]));
486
+
487
+ /*
488
+ * "not in nothing" excludes nothing, so an empty exclusion list must keep
489
+ * every row rather than emit an illegal `NOT IN ()`.
490
+ */
491
+ expect(notInOperator.getSql('"Alert"."monitorId"')).toBe("TRUE = TRUE");
492
+ expect(notInOrNullOperator.getSql('"Alert"."monitorId"')).toBe(
493
+ "TRUE = TRUE",
494
+ );
495
+ expect(Object.keys(notInOperator.objectLiteralParameters)).toHaveLength(0);
496
+ expect(
497
+ Object.keys(notInOrNullOperator.objectLiteralParameters),
498
+ ).toHaveLength(0);
499
+ });
500
+ });
501
+
502
+ describe("QueryHelper.equalToOrNull — single, array, and empty-array shapes", () => {
503
+ it("ORs a single value against an `IS NULL` check", () => {
504
+ const operator: RawOperator = asRaw(QueryHelper.equalToOrNull("v"));
505
+
506
+ const sql: string = operator.getSql('"Alert"."monitorId"');
507
+
508
+ expect(sql).toContain('"Alert"."monitorId" = :');
509
+ expect(sql).toContain('or "Alert"."monitorId" IS NULL)');
510
+ expect(Object.values(operator.objectLiteralParameters)).toEqual(["v"]);
511
+ });
512
+
513
+ it("binds one distinct parameter per array member, OR-joined, then IS NULL", () => {
514
+ const operator: RawOperator = asRaw(QueryHelper.equalToOrNull(["a", "b"]));
515
+
516
+ const sql: string = operator.getSql('"Alert"."monitorId"');
517
+
518
+ /* Two members mean two equality arms plus the trailing null arm. */
519
+ expect(sql.match(/[=] :/g) || []).toHaveLength(2);
520
+ expect(sql).toContain("IS NULL)");
521
+
522
+ const boundValues: Array<unknown> = Object.values(
523
+ operator.objectLiteralParameters,
524
+ );
525
+ expect(boundValues).toEqual(expect.arrayContaining(["a", "b"]));
526
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(2);
527
+ });
528
+
529
+ it("stringifies ObjectID members of the array", () => {
530
+ const id: ObjectID = ObjectID.generate();
531
+
532
+ const operator: RawOperator = asRaw(QueryHelper.equalToOrNull([id]));
533
+
534
+ expect(Object.values(operator.objectLiteralParameters)).toEqual([
535
+ id.toString(),
536
+ ]);
537
+ });
538
+
539
+ it("an empty array degrades to a null-only check that binds nothing", () => {
540
+ const operator: RawOperator = asRaw(QueryHelper.equalToOrNull([]));
541
+
542
+ const sql: string = operator.getSql('"Alert"."monitorId"');
543
+
544
+ /*
545
+ * With no members the equality arms collapse away; what survives is the
546
+ * IS NULL tail, and no value is ever bound.
547
+ */
548
+ expect(sql).toContain("IS NULL)");
549
+ expect(Object.keys(operator.objectLiteralParameters)).toHaveLength(0);
550
+ });
551
+ });