@oneuptime/common 12.0.7 → 12.0.8

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 (186) hide show
  1. package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
  2. package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
  3. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  4. package/Server/API/UserAPI.ts +16 -1
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  9. package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
  10. package/Server/Services/TelemetryEntityService.ts +114 -1
  11. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  12. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  13. package/Server/Types/Workflow/Workflow.ts +5 -0
  14. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  15. package/Server/Utils/VM/VMAPI.ts +56 -8
  16. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  17. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  18. package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
  19. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  20. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  21. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  22. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  23. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  24. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  25. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  26. package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
  27. package/Tests/Types/Workflow/Templates.test.ts +403 -0
  28. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  29. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  30. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  31. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  32. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  33. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  34. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  35. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  36. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  37. package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
  38. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
  39. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
  40. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
  41. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
  42. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  43. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  44. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  45. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
  46. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  47. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  48. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  49. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
  50. package/Types/SerializableObjectDictionary.ts +133 -39
  51. package/Types/Telemetry/EntitySource.ts +40 -0
  52. package/Types/Telemetry/EntityType.ts +27 -0
  53. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  54. package/Types/Workflow/Component.ts +29 -0
  55. package/Types/Workflow/Components/API.ts +35 -0
  56. package/Types/Workflow/Components/BaseModel.ts +7 -3
  57. package/Types/Workflow/StepTrace.ts +181 -0
  58. package/Types/Workflow/TemplateSyntax.ts +499 -0
  59. package/Types/Workflow/Templates.ts +316 -0
  60. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  61. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  62. package/UI/Components/Dictionary/Dictionary.tsx +23 -6
  63. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  64. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  65. package/UI/Components/Forms/Types/Field.ts +7 -0
  66. package/UI/Components/Forms/Validation.ts +63 -1
  67. package/UI/Components/Input/Input.tsx +31 -3
  68. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  69. package/UI/Components/Modal/Modal.tsx +37 -4
  70. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  71. package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
  72. package/UI/Components/Workflow/Component.tsx +28 -26
  73. package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
  74. package/UI/Components/Workflow/GraphLint.ts +591 -0
  75. package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
  76. package/UI/Components/Workflow/ModelSchema.ts +196 -0
  77. package/UI/Components/Workflow/RunForm.tsx +41 -7
  78. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  79. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  80. package/UI/Components/Workflow/Utils.ts +93 -1
  81. package/UI/Components/Workflow/Workflow.tsx +77 -4
  82. package/UI/Types/LayeredDismissal.ts +31 -0
  83. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
  86. package/Utils/Telemetry/EntityKey.ts +66 -0
  87. package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
  88. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
  90. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  92. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  93. package/build/dist/Server/API/UserAPI.js +16 -1
  94. package/build/dist/Server/API/UserAPI.js.map +1 -1
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  101. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  102. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
  104. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  105. package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
  106. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  107. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  108. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  109. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  110. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  111. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  112. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  113. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  114. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  115. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  116. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  117. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  118. package/build/dist/Types/Telemetry/EntityType.js +27 -0
  119. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  120. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  121. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  122. package/build/dist/Types/Workflow/Component.js +21 -0
  123. package/build/dist/Types/Workflow/Component.js.map +1 -1
  124. package/build/dist/Types/Workflow/Components/API.js +35 -0
  125. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  126. package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
  127. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  128. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  129. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  130. package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
  131. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  132. package/build/dist/Types/Workflow/Templates.js +218 -0
  133. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  134. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  135. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  136. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  137. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  138. package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
  139. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  141. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  142. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  143. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  144. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  145. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  146. package/build/dist/UI/Components/Input/Input.js +12 -4
  147. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  148. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  149. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  150. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  151. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  152. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  153. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  154. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
  155. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  156. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  157. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  158. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
  159. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
  161. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  162. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
  163. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  164. package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
  165. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  166. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  167. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  169. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  170. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  171. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  172. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  173. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  174. package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
  175. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  176. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  177. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  178. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  179. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  181. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
  183. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  184. package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
  185. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,248 @@
1
+ import TelemetryEntity from "../../../Models/DatabaseModels/TelemetryEntity";
2
+ import TelemetryEntityService from "../../../Server/Services/TelemetryEntityService";
3
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
4
+ import ObjectID from "../../../Types/ObjectID";
5
+ import EntitySource from "../../../Types/Telemetry/EntitySource";
6
+ import EntityType from "../../../Types/Telemetry/EntityType";
7
+ import { keyForManualEntity } from "../../../Utils/Telemetry/EntityKey";
8
+ import { describe, expect, test } from "@jest/globals";
9
+
10
+ /*
11
+ * TelemetryEntityService.onBeforeCreate is the whole of the manual-CI
12
+ * write path: it is what decides a create is manual, validates it, and
13
+ * derives the identity the registry needs from the two fields a human can
14
+ * supply. It also has to leave machine creates — ingest and the inventory
15
+ * mirror, which arrive with their keys already computed — completely alone.
16
+ *
17
+ * Every assertion below is one of those two duties.
18
+ */
19
+
20
+ const PROJECT_ID: ObjectID = new ObjectID(
21
+ "0f8b9c0d-e1a2-4b3c-8d5e-6f7a8b9c0d1e",
22
+ );
23
+
24
+ type CallCreate = (createBy: CreateBy<TelemetryEntity>) => Promise<unknown>;
25
+
26
+ const callOnBeforeCreate: CallCreate = (
27
+ createBy: CreateBy<TelemetryEntity>,
28
+ ): Promise<unknown> => {
29
+ return (
30
+ TelemetryEntityService as unknown as {
31
+ onBeforeCreate: (c: CreateBy<TelemetryEntity>) => Promise<unknown>;
32
+ }
33
+ ).onBeforeCreate(createBy);
34
+ };
35
+
36
+ /*
37
+ * Explicitly admits `undefined` per field. The suite needs to build creates
38
+ * with a field genuinely absent (that is what the validation tests are), and
39
+ * `Partial<TelemetryEntity>` rejects an explicit undefined under this
40
+ * project's `exactOptionalPropertyTypes`.
41
+ */
42
+ interface ManualCreateOverrides {
43
+ projectId?: ObjectID | undefined;
44
+ entityType?: EntityType | undefined;
45
+ displayName?: string | undefined;
46
+ description?: string | undefined;
47
+ }
48
+
49
+ function manualCreate(
50
+ overrides: ManualCreateOverrides = {},
51
+ ): CreateBy<TelemetryEntity> {
52
+ const data: TelemetryEntity = new TelemetryEntity();
53
+ data.projectId = PROJECT_ID;
54
+ data.entityType = EntityType.ExternalService;
55
+ data.displayName = "Stripe Payments API";
56
+ Object.assign(data, overrides);
57
+
58
+ return { data, props: { isRoot: true } } as CreateBy<TelemetryEntity>;
59
+ }
60
+
61
+ describe("manual entity creation", () => {
62
+ test("a create with no entity key is treated as manual", async () => {
63
+ const createBy: CreateBy<TelemetryEntity> = manualCreate();
64
+ await callOnBeforeCreate(createBy);
65
+
66
+ expect(createBy.data.source).toBe(EntitySource.Manual);
67
+ });
68
+
69
+ test("derives the entity key from project, type and name", async () => {
70
+ const createBy: CreateBy<TelemetryEntity> = manualCreate();
71
+ await callOnBeforeCreate(createBy);
72
+
73
+ expect(createBy.data.entityKey).toBe(
74
+ keyForManualEntity(
75
+ PROJECT_ID.toString(),
76
+ EntityType.ExternalService,
77
+ "Stripe Payments API",
78
+ ),
79
+ );
80
+ });
81
+
82
+ test("records the canonicalized name as the identifying attribute", async () => {
83
+ const createBy: CreateBy<TelemetryEntity> = manualCreate({
84
+ displayName: " Stripe Payments API ",
85
+ });
86
+ await callOnBeforeCreate(createBy);
87
+
88
+ expect(createBy.data.identifyingAttributes).toEqual({
89
+ "oneuptime.entity.name": "stripe payments api",
90
+ });
91
+ });
92
+
93
+ test("trims the stored display name but keeps its original casing", async () => {
94
+ const createBy: CreateBy<TelemetryEntity> = manualCreate({
95
+ displayName: " Stripe Payments API ",
96
+ });
97
+ await callOnBeforeCreate(createBy);
98
+
99
+ expect(createBy.data.displayName).toBe("Stripe Payments API");
100
+ });
101
+
102
+ test("two names differing only by case resolve to one identity", async () => {
103
+ const first: CreateBy<TelemetryEntity> = manualCreate({
104
+ displayName: "Stripe API",
105
+ });
106
+ const second: CreateBy<TelemetryEntity> = manualCreate({
107
+ displayName: "stripe api",
108
+ });
109
+
110
+ await callOnBeforeCreate(first);
111
+ await callOnBeforeCreate(second);
112
+
113
+ /*
114
+ * The unique index on (projectId, entityType, entityKey) then rejects
115
+ * the second as a duplicate rather than admitting a near-identical row.
116
+ */
117
+ expect(second.data.entityKey).toBe(first.data.entityKey);
118
+ });
119
+
120
+ test("stamps first and last seen so the row sorts with the rest", async () => {
121
+ const createBy: CreateBy<TelemetryEntity> = manualCreate();
122
+ await callOnBeforeCreate(createBy);
123
+
124
+ expect(createBy.data.firstSeenAt).toBeInstanceOf(Date);
125
+ expect(createBy.data.lastSeenAt).toBeInstanceOf(Date);
126
+ });
127
+
128
+ test("accepts every manually creatable type", async () => {
129
+ for (const entityType of [
130
+ EntityType.ExternalService,
131
+ EntityType.ExternalDatabase,
132
+ EntityType.Appliance,
133
+ ]) {
134
+ const createBy: CreateBy<TelemetryEntity> = manualCreate({ entityType });
135
+ await callOnBeforeCreate(createBy);
136
+
137
+ expect(createBy.data.entityType).toBe(entityType);
138
+ expect(createBy.data.entityKey).toBeTruthy();
139
+ }
140
+ });
141
+ });
142
+
143
+ describe("manual entity validation", () => {
144
+ test("rejects a type that OneUptime discovers on its own", async () => {
145
+ await expect(
146
+ callOnBeforeCreate(manualCreate({ entityType: EntityType.Service })),
147
+ ).rejects.toThrow(/cannot be created manually/);
148
+ });
149
+
150
+ test("rejects an inventory-mirrored type", async () => {
151
+ /*
152
+ * These arrive from the mirror with a precomputed key; a hand-made one
153
+ * would be a permanent duplicate of the row the mirror maintains.
154
+ */
155
+ await expect(
156
+ callOnBeforeCreate(
157
+ manualCreate({ entityType: EntityType.NetworkDevice }),
158
+ ),
159
+ ).rejects.toThrow(/cannot be created manually/);
160
+ });
161
+
162
+ test("rejects a missing name", async () => {
163
+ await expect(
164
+ callOnBeforeCreate(manualCreate({ displayName: undefined })),
165
+ ).rejects.toThrow(/Name is required/);
166
+ });
167
+
168
+ test("rejects a whitespace-only name", async () => {
169
+ await expect(
170
+ callOnBeforeCreate(manualCreate({ displayName: " " })),
171
+ ).rejects.toThrow(/Name is required/);
172
+ });
173
+
174
+ test("rejects a missing entity type", async () => {
175
+ await expect(
176
+ callOnBeforeCreate(manualCreate({ entityType: undefined })),
177
+ ).rejects.toThrow(/Entity Type is required/);
178
+ });
179
+
180
+ test("rejects a missing project", async () => {
181
+ await expect(
182
+ callOnBeforeCreate(manualCreate({ projectId: undefined })),
183
+ ).rejects.toThrow(/Project ID is required/);
184
+ });
185
+ });
186
+
187
+ describe("machine creates are left alone", () => {
188
+ test("a discovered create keeps its precomputed key and source", async () => {
189
+ const data: TelemetryEntity = new TelemetryEntity();
190
+ data.projectId = PROJECT_ID;
191
+ data.entityType = EntityType.KubernetesPod;
192
+ data.entityKey = "210dac24142f1baa";
193
+ data.source = EntitySource.Discovered;
194
+ data.displayName = "checkout-7d9f";
195
+ data.identifyingAttributes = { "k8s.pod.name": "checkout-7d9f" };
196
+
197
+ const createBy: CreateBy<TelemetryEntity> = {
198
+ data,
199
+ props: { isRoot: true },
200
+ } as CreateBy<TelemetryEntity>;
201
+
202
+ await callOnBeforeCreate(createBy);
203
+
204
+ expect(createBy.data.entityKey).toBe("210dac24142f1baa");
205
+ expect(createBy.data.source).toBe(EntitySource.Discovered);
206
+ expect(createBy.data.identifyingAttributes).toEqual({
207
+ "k8s.pod.name": "checkout-7d9f",
208
+ });
209
+ });
210
+
211
+ test("a discovered create of an otherwise un-creatable type is not rejected", async () => {
212
+ /*
213
+ * The manual-type restriction must apply to users, not to ingest — which
214
+ * legitimately creates services, pods and hosts.
215
+ */
216
+ const data: TelemetryEntity = new TelemetryEntity();
217
+ data.projectId = PROJECT_ID;
218
+ data.entityType = EntityType.Service;
219
+ data.entityKey = "abcdef0123456789";
220
+ data.source = EntitySource.Discovered;
221
+
222
+ await expect(
223
+ callOnBeforeCreate({
224
+ data,
225
+ props: { isRoot: true },
226
+ } as CreateBy<TelemetryEntity>),
227
+ ).resolves.toBeDefined();
228
+ });
229
+
230
+ test("an inventory mirror create keeps its precomputed key and source", async () => {
231
+ const data: TelemetryEntity = new TelemetryEntity();
232
+ data.projectId = PROJECT_ID;
233
+ data.entityType = EntityType.NetworkDevice;
234
+ data.entityKey = "9f8e7d6c5b4a3210";
235
+ data.source = EntitySource.Inventory;
236
+ data.displayName = "core-switch-1";
237
+
238
+ const createBy: CreateBy<TelemetryEntity> = {
239
+ data,
240
+ props: { isRoot: true },
241
+ } as CreateBy<TelemetryEntity>;
242
+
243
+ await callOnBeforeCreate(createBy);
244
+
245
+ expect(createBy.data.entityKey).toBe("9f8e7d6c5b4a3210");
246
+ expect(createBy.data.source).toBe(EntitySource.Inventory);
247
+ });
248
+ });
@@ -0,0 +1,192 @@
1
+ /*
2
+ * Request headers on the API components reach sanitizeArgs in three shapes: a
3
+ * JSON string (every workflow written before the key/value editor), an object
4
+ * (what the key/value editor produces), and whatever a {{...}} substitution
5
+ * left behind. Every consumer downstream casts the result to
6
+ * Dictionary<string> and spreads it into the outgoing request, so the shape
7
+ * has to be settled here.
8
+ */
9
+
10
+ jest.mock("../../../../../Utils/API", () => {
11
+ return {
12
+ __esModule: true,
13
+ default: {
14
+ get: jest.fn(),
15
+ post: jest.fn(),
16
+ put: jest.fn(),
17
+ patch: jest.fn(),
18
+ delete: jest.fn(),
19
+ },
20
+ };
21
+ });
22
+
23
+ import ApiGet from "../../../../../Server/Types/Workflow/Components/API/Get";
24
+ import { ApiComponentUtils } from "../../../../../Server/Types/Workflow/Components/API/Utils";
25
+ import ComponentCode, {
26
+ RunOptions,
27
+ } from "../../../../../Server/Types/Workflow/ComponentCode";
28
+ import Exception from "../../../../../Types/Exception/Exception";
29
+ import { JSONObject } from "../../../../../Types/JSON";
30
+ import ObjectID from "../../../../../Types/ObjectID";
31
+ import IconProp from "../../../../../Types/Icon/IconProp";
32
+ import ComponentMetadata, {
33
+ Argument,
34
+ ComponentInputType,
35
+ ComponentType,
36
+ } from "../../../../../Types/Workflow/Component";
37
+ import APIComponents from "../../../../../Types/Workflow/Components/API";
38
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
39
+ import dns from "dns";
40
+
41
+ type MakeOptionsFunction = () => RunOptions;
42
+
43
+ const makeOptions: MakeOptionsFunction = (): RunOptions => {
44
+ return {
45
+ log: jest.fn() as RunOptions["log"],
46
+ workflowLogId: ObjectID.generate(),
47
+ workflowId: ObjectID.generate(),
48
+ projectId: ObjectID.generate(),
49
+ onError: ((exception: Exception): Exception => {
50
+ return exception;
51
+ }) as RunOptions["onError"],
52
+ executeWorkflow: async (): Promise<void> => {},
53
+ };
54
+ };
55
+
56
+ type SanitizeFunction = (headers: unknown) => Promise<JSONObject>;
57
+
58
+ const sanitizeHeaders: SanitizeFunction = async (
59
+ headers: unknown,
60
+ ): Promise<JSONObject> => {
61
+ const component: ComponentCode = new ApiGet();
62
+
63
+ const result: { args: JSONObject } = await ApiComponentUtils.sanitizeArgs(
64
+ component.getMetadata(),
65
+ {
66
+ url: "https://api.example.com/thing",
67
+ "request-headers": headers as JSONObject,
68
+ },
69
+ makeOptions(),
70
+ );
71
+
72
+ return result.args["request-headers"] as JSONObject;
73
+ };
74
+
75
+ beforeEach(() => {
76
+ jest.clearAllMocks();
77
+ jest
78
+ .spyOn(dns.promises, "lookup")
79
+ .mockResolvedValue([{ address: "93.184.216.34", family: 4 }] as never);
80
+ });
81
+
82
+ afterEach(() => {
83
+ jest.restoreAllMocks();
84
+ });
85
+
86
+ describe("ApiComponentUtils.sanitizeArgs — header shapes", () => {
87
+ test("parses the legacy JSON-string shape", async () => {
88
+ await expect(
89
+ sanitizeHeaders('{"Authorization": "Bearer abc"}'),
90
+ ).resolves.toEqual({
91
+ Authorization: "Bearer abc",
92
+ });
93
+ });
94
+
95
+ test("parses a JSON5 string, which has always been accepted here", async () => {
96
+ await expect(
97
+ sanitizeHeaders("{Authorization: 'Bearer abc'}"),
98
+ ).resolves.toEqual({
99
+ Authorization: "Bearer abc",
100
+ });
101
+ });
102
+
103
+ test("passes the object shape through", async () => {
104
+ await expect(
105
+ sanitizeHeaders({ Authorization: "Bearer abc" }),
106
+ ).resolves.toEqual({ Authorization: "Bearer abc" });
107
+ });
108
+
109
+ test("flattens numbers and booleans the key/value editor can emit", async () => {
110
+ await expect(
111
+ sanitizeHeaders({ "X-Retries": 3, "X-Debug": true }),
112
+ ).resolves.toEqual({
113
+ "X-Retries": "3",
114
+ "X-Debug": "true",
115
+ });
116
+ });
117
+
118
+ test("drops null and undefined values instead of sending 'null'", async () => {
119
+ await expect(
120
+ sanitizeHeaders({ "X-A": "keep", "X-B": null }),
121
+ ).resolves.toEqual({ "X-A": "keep" });
122
+ });
123
+
124
+ test("serializes a nested value rather than sending [object Object]", async () => {
125
+ await expect(sanitizeHeaders({ "X-Meta": { a: 1 } })).resolves.toEqual({
126
+ "X-Meta": '{"a":1}',
127
+ });
128
+ });
129
+
130
+ test("rejects an array, which would spread into per-index junk headers", async () => {
131
+ await expect(sanitizeHeaders("[1, 2]")).rejects.toThrow(
132
+ /Request headers must be a JSON object/,
133
+ );
134
+ });
135
+
136
+ test("rejects a bare scalar", async () => {
137
+ await expect(sanitizeHeaders("42")).rejects.toThrow(
138
+ /Request headers must be a JSON object/,
139
+ );
140
+ });
141
+
142
+ test("leaves the request alone when no headers were given", async () => {
143
+ const component: ComponentCode = new ApiGet();
144
+
145
+ const result: { args: JSONObject } = await ApiComponentUtils.sanitizeArgs(
146
+ component.getMetadata(),
147
+ { url: "https://api.example.com/thing" },
148
+ makeOptions(),
149
+ );
150
+
151
+ expect(result.args["request-headers"]).toBeUndefined();
152
+ });
153
+ });
154
+
155
+ describe("API component metadata — headers are treated as secret", () => {
156
+ /*
157
+ * The key/value grid makes pasting a bearer token into a header the obvious
158
+ * thing to do. Without isSensitive the resolved value is written verbatim
159
+ * into the WorkflowLog for anyone who can read the project.
160
+ */
161
+ test("every API component marks request-headers sensitive", () => {
162
+ const componentsWithHeaders: Array<ComponentMetadata> =
163
+ APIComponents.filter((component: ComponentMetadata) => {
164
+ return component.arguments.some((arg: Argument) => {
165
+ return arg.id === "request-headers";
166
+ });
167
+ });
168
+
169
+ expect(componentsWithHeaders.length).toBe(5);
170
+
171
+ for (const component of componentsWithHeaders) {
172
+ const headerArgument: Argument = component.arguments.find(
173
+ (arg: Argument) => {
174
+ return arg.id === "request-headers";
175
+ },
176
+ ) as Argument;
177
+
178
+ expect(headerArgument.isSensitive).toBe(true);
179
+ expect(headerArgument.type).toBe(ComponentInputType.StringDictionary);
180
+ }
181
+ });
182
+
183
+ test("the components are the five request verbs", () => {
184
+ expect(
185
+ APIComponents.filter((component: ComponentMetadata) => {
186
+ return component.componentType === ComponentType.Component;
187
+ }).length,
188
+ ).toBeGreaterThanOrEqual(5);
189
+
190
+ expect(APIComponents[0]?.iconProp).toBe(IconProp.Globe);
191
+ });
192
+ });
@@ -0,0 +1,152 @@
1
+ import QuerySettingsHelper, {
2
+ getQuerySettings,
3
+ DEFAULT_MAX_MEMORY_USAGE_IN_BYTES,
4
+ DEFAULT_MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY_IN_BYTES,
5
+ DEFAULT_MAX_BYTES_BEFORE_EXTERNAL_SORT_IN_BYTES,
6
+ } from "../../../../Server/Utils/AnalyticsDatabase/QuerySettingsHelper";
7
+
8
+ /*
9
+ * Unit tests for getQuerySettings — the builder for the trailing
10
+ * ` SETTINGS ...` clause appended verbatim to every ClickHouse read query.
11
+ *
12
+ * The clause is NOT parameterized; it is concatenated straight into SQL, so
13
+ * the exact emitted text matters. These tests lock down:
14
+ * - the always-present per-query memory ceiling and spill thresholds,
15
+ * - correct quoting (strings single-quoted, numbers bare),
16
+ * - the conditional execution-time / overflow-mode settings, and
17
+ * - the ordering of the emitted parts.
18
+ */
19
+
20
+ describe("getQuerySettings", () => {
21
+ test("always emits the memory ceiling and spill thresholds by default", () => {
22
+ const clause: string = getQuerySettings();
23
+
24
+ expect(clause).toBe(
25
+ ` SETTINGS max_memory_usage = ${DEFAULT_MAX_MEMORY_USAGE_IN_BYTES}, ` +
26
+ `max_bytes_before_external_group_by = ${DEFAULT_MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY_IN_BYTES}, ` +
27
+ `max_bytes_before_external_sort = ${DEFAULT_MAX_BYTES_BEFORE_EXTERNAL_SORT_IN_BYTES}`,
28
+ );
29
+ });
30
+
31
+ test("produces the same output whether called with no args or undefined", () => {
32
+ expect(getQuerySettings()).toBe(getQuerySettings(undefined));
33
+ });
34
+
35
+ test("always starts with the ` SETTINGS ` keyword and a leading space", () => {
36
+ expect(getQuerySettings().startsWith(" SETTINGS ")).toBe(true);
37
+ expect(
38
+ getQuerySettings({ maxExecutionTimeInSeconds: 30 }).startsWith(
39
+ " SETTINGS ",
40
+ ),
41
+ ).toBe(true);
42
+ });
43
+
44
+ test("omits execution-time settings when not provided", () => {
45
+ const clause: string = getQuerySettings();
46
+
47
+ expect(clause).not.toContain("max_execution_time");
48
+ expect(clause).not.toContain("timeout_overflow_mode");
49
+ });
50
+
51
+ test("includes max_execution_time when provided", () => {
52
+ const clause: string = getQuerySettings({ maxExecutionTimeInSeconds: 45 });
53
+
54
+ expect(clause).toContain("max_execution_time = 45");
55
+ // Execution time is emitted before the memory settings.
56
+ expect(clause.indexOf("max_execution_time")).toBeLessThan(
57
+ clause.indexOf("max_memory_usage"),
58
+ );
59
+ });
60
+
61
+ test("single-quotes the timeout overflow mode", () => {
62
+ expect(getQuerySettings({ timeoutOverflowMode: "break" })).toContain(
63
+ "timeout_overflow_mode = 'break'",
64
+ );
65
+ expect(getQuerySettings({ timeoutOverflowMode: "throw" })).toContain(
66
+ "timeout_overflow_mode = 'throw'",
67
+ );
68
+ });
69
+
70
+ test("emits execution time before overflow mode before memory settings", () => {
71
+ const clause: string = getQuerySettings({
72
+ maxExecutionTimeInSeconds: 10,
73
+ timeoutOverflowMode: "break",
74
+ });
75
+
76
+ const timePos: number = clause.indexOf("max_execution_time");
77
+ const overflowPos: number = clause.indexOf("timeout_overflow_mode");
78
+ const memoryPos: number = clause.indexOf("max_memory_usage");
79
+
80
+ expect(timePos).toBeGreaterThan(-1);
81
+ expect(timePos).toBeLessThan(overflowPos);
82
+ expect(overflowPos).toBeLessThan(memoryPos);
83
+ });
84
+
85
+ test("honours overrides for the memory ceiling and spill thresholds", () => {
86
+ const clause: string = getQuerySettings({
87
+ maxMemoryUsageInBytes: 100,
88
+ maxBytesBeforeExternalGroupByInBytes: 50,
89
+ maxBytesBeforeExternalSortInBytes: 25,
90
+ });
91
+
92
+ expect(clause).toContain("max_memory_usage = 100");
93
+ expect(clause).toContain("max_bytes_before_external_group_by = 50");
94
+ expect(clause).toContain("max_bytes_before_external_sort = 25");
95
+ // The defaults must not leak through when overridden.
96
+ expect(clause).not.toContain(`${DEFAULT_MAX_MEMORY_USAGE_IN_BYTES}`);
97
+ });
98
+
99
+ test("emits numeric additional settings without quotes", () => {
100
+ const clause: string = getQuerySettings({
101
+ additionalSettings: { max_threads: 4 },
102
+ });
103
+
104
+ expect(clause).toContain("max_threads = 4");
105
+ expect(clause).not.toContain("max_threads = '4'");
106
+ });
107
+
108
+ test("single-quotes string additional settings", () => {
109
+ const clause: string = getQuerySettings({
110
+ additionalSettings: { optimize_aggregation_in_order: "1" },
111
+ });
112
+
113
+ expect(clause).toContain("optimize_aggregation_in_order = '1'");
114
+ });
115
+
116
+ test("appends additional settings after the memory settings", () => {
117
+ const clause: string = getQuerySettings({
118
+ additionalSettings: { optimize_use_projections: 1 },
119
+ });
120
+
121
+ expect(clause.indexOf("max_bytes_before_external_sort")).toBeLessThan(
122
+ clause.indexOf("optimize_use_projections"),
123
+ );
124
+ });
125
+
126
+ test("joins every setting with a comma-and-space separator", () => {
127
+ const clause: string = getQuerySettings({
128
+ maxExecutionTimeInSeconds: 5,
129
+ timeoutOverflowMode: "throw",
130
+ additionalSettings: { max_threads: 2 },
131
+ });
132
+
133
+ /*
134
+ * The clause after " SETTINGS " is a comma-separated list; splitting it
135
+ * must yield one entry per emitted setting (2 conditional + 3 memory + 1
136
+ * extra).
137
+ */
138
+ const body: string = clause.replace(" SETTINGS ", "");
139
+ expect(body.split(", ").length).toBe(6);
140
+ });
141
+
142
+ test("class static method delegates to the standalone function", () => {
143
+ const options: { maxExecutionTimeInSeconds: number } = {
144
+ maxExecutionTimeInSeconds: 12,
145
+ };
146
+
147
+ expect(QuerySettingsHelper.getQuerySettings(options)).toBe(
148
+ getQuerySettings(options),
149
+ );
150
+ expect(QuerySettingsHelper.getQuerySettings()).toBe(getQuerySettings());
151
+ });
152
+ });