@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,287 @@
1
+ /*
2
+ * The two pure decisions behind the column row editor: whether a stored value
3
+ * can be shown as rows at all, and what the rows serialize back to.
4
+ *
5
+ * The serialization side matters most — the string this produces is handed
6
+ * straight to the server, where JSONFunctions.deserialize has to turn the
7
+ * operator wrappers back into the QueryHelper objects TypeORM understands.
8
+ */
9
+
10
+ import {
11
+ ModelColumnEditorMode,
12
+ buildColumnValueJson,
13
+ classifyColumnValueCompatibility,
14
+ } from "../../../../UI/Components/Workflow/ModelColumnEditor";
15
+ import { ModelSchemaColumn } from "../../../../UI/Components/Workflow/ModelSchema";
16
+ import Dictionary from "../../../../Types/Dictionary";
17
+ import { DictionaryEntryValue } from "../../../../UI/Components/Dictionary/DictionaryFilterOperator";
18
+ import { JSONObject, ObjectType } from "../../../../Types/JSON";
19
+ import GreaterThan from "../../../../Types/BaseDatabase/GreaterThan";
20
+ import Includes from "../../../../Types/BaseDatabase/Includes";
21
+ import IsNull from "../../../../Types/BaseDatabase/IsNull";
22
+ import NotEqual from "../../../../Types/BaseDatabase/NotEqual";
23
+ import Search from "../../../../Types/BaseDatabase/Search";
24
+ import { describe, expect, test } from "@jest/globals";
25
+
26
+ type MakeColumnFunction = (
27
+ id: string,
28
+ type?: string | undefined,
29
+ ) => ModelSchemaColumn;
30
+
31
+ const makeColumn: MakeColumnFunction = (
32
+ id: string,
33
+ type?: string | undefined,
34
+ ): ModelSchemaColumn => {
35
+ return {
36
+ id: id,
37
+ title: id,
38
+ type: type || "ShortText",
39
+ isRelation: false,
40
+ };
41
+ };
42
+
43
+ const COLUMNS: Array<ModelSchemaColumn> = [
44
+ makeColumn("_id", "ObjectID"),
45
+ makeColumn("name"),
46
+ makeColumn("createdAt", "Date"),
47
+ makeColumn("isEnabled", "Boolean"),
48
+ ];
49
+
50
+ describe("classifyColumnValueCompatibility — plain values", () => {
51
+ test("an empty query is representable", () => {
52
+ expect(
53
+ classifyColumnValueCompatibility({}, COLUMNS, ModelColumnEditorMode.Query)
54
+ .compatible,
55
+ ).toBe(true);
56
+ });
57
+
58
+ test("scalar values are representable", () => {
59
+ const result: { compatible: boolean; reasons: Array<string> } =
60
+ classifyColumnValueCompatibility(
61
+ { name: "abc", isEnabled: true },
62
+ COLUMNS,
63
+ ModelColumnEditorMode.Query,
64
+ );
65
+
66
+ expect(result.compatible).toBe(true);
67
+ expect(result.reasons).toEqual([]);
68
+ });
69
+
70
+ test("a null value is representable", () => {
71
+ expect(
72
+ classifyColumnValueCompatibility(
73
+ { name: null } as unknown as JSONObject,
74
+ COLUMNS,
75
+ ModelColumnEditorMode.Query,
76
+ ).compatible,
77
+ ).toBe(true);
78
+ });
79
+
80
+ test("text that is not a JSON object is not representable", () => {
81
+ const result: { compatible: boolean; reasons: Array<string> } =
82
+ classifyColumnValueCompatibility(
83
+ null,
84
+ COLUMNS,
85
+ ModelColumnEditorMode.Query,
86
+ );
87
+
88
+ expect(result.compatible).toBe(false);
89
+ expect(result.reasons[0]).toMatch(/isn't a JSON object/);
90
+ });
91
+
92
+ /*
93
+ * An unknown column is exactly the "id" versus "_id" mistake. Say so, but
94
+ * keep the rows — the builder needs to see the row to fix it.
95
+ */
96
+ test("an unknown column is reported without locking the editor", () => {
97
+ const result: { compatible: boolean; reasons: Array<string> } =
98
+ classifyColumnValueCompatibility(
99
+ { id: "abc" },
100
+ COLUMNS,
101
+ ModelColumnEditorMode.Query,
102
+ );
103
+
104
+ expect(result.compatible).toBe(true);
105
+ expect(result.reasons[0]).toMatch(/"id" isn't a known column/);
106
+ });
107
+
108
+ test("columns are not checked when the schema failed to load", () => {
109
+ const result: { compatible: boolean; reasons: Array<string> } =
110
+ classifyColumnValueCompatibility(
111
+ { anything: "abc" },
112
+ [],
113
+ ModelColumnEditorMode.Query,
114
+ );
115
+
116
+ expect(result.compatible).toBe(true);
117
+ expect(result.reasons).toEqual([]);
118
+ });
119
+ });
120
+
121
+ describe("classifyColumnValueCompatibility — operators", () => {
122
+ test("a query shows the operators the rows support", () => {
123
+ const supported: Array<string> = [
124
+ ObjectType.EqualTo,
125
+ ObjectType.NotEqual,
126
+ ObjectType.Search,
127
+ ObjectType.NotContains,
128
+ ObjectType.StartsWith,
129
+ ObjectType.EndsWith,
130
+ ObjectType.GreaterThan,
131
+ ObjectType.GreaterThanOrEqual,
132
+ ObjectType.LessThan,
133
+ ObjectType.LessThanOrEqual,
134
+ ObjectType.IsNull,
135
+ ObjectType.NotNull,
136
+ ObjectType.Includes,
137
+ ObjectType.IncludesNone,
138
+ ];
139
+
140
+ for (const objectType of supported) {
141
+ const result: { compatible: boolean } = classifyColumnValueCompatibility(
142
+ { name: { _type: objectType, value: "x" } },
143
+ COLUMNS,
144
+ ModelColumnEditorMode.Query,
145
+ );
146
+
147
+ expect(result.compatible).toBe(true);
148
+ }
149
+ });
150
+
151
+ test("an operator the rows cannot show keeps the JSON editor", () => {
152
+ const result: { compatible: boolean; reasons: Array<string> } =
153
+ classifyColumnValueCompatibility(
154
+ { createdAt: { _type: ObjectType.InBetween, value: "x" } },
155
+ COLUMNS,
156
+ ModelColumnEditorMode.Query,
157
+ );
158
+
159
+ expect(result.compatible).toBe(false);
160
+ expect(result.reasons[0]).toMatch(/InBetween/);
161
+ });
162
+
163
+ /*
164
+ * A record is a set of values to write. An operator there would be nonsense,
165
+ * so it belongs in the JSON editor where the builder can see what they have.
166
+ */
167
+ test("an operator in a record is not representable", () => {
168
+ const result: { compatible: boolean; reasons: Array<string> } =
169
+ classifyColumnValueCompatibility(
170
+ { name: { _type: ObjectType.NotEqual, value: "x" } },
171
+ COLUMNS,
172
+ ModelColumnEditorMode.Record,
173
+ );
174
+
175
+ expect(result.compatible).toBe(false);
176
+ });
177
+
178
+ test("a nested relation query keeps the JSON editor", () => {
179
+ const result: { compatible: boolean; reasons: Array<string> } =
180
+ classifyColumnValueCompatibility(
181
+ { project: { _id: "abc" } },
182
+ COLUMNS,
183
+ ModelColumnEditorMode.Query,
184
+ );
185
+
186
+ expect(result.compatible).toBe(false);
187
+ expect(result.reasons.join(" ")).toMatch(/nested record/);
188
+ });
189
+
190
+ test("a bare array keeps the JSON editor and suggests the operator", () => {
191
+ const result: { compatible: boolean; reasons: Array<string> } =
192
+ classifyColumnValueCompatibility(
193
+ { name: ["a", "b"] } as unknown as JSONObject,
194
+ COLUMNS,
195
+ ModelColumnEditorMode.Query,
196
+ );
197
+
198
+ expect(result.compatible).toBe(false);
199
+ expect(result.reasons[0]).toMatch(/is any of/);
200
+ });
201
+ });
202
+
203
+ describe("buildColumnValueJson", () => {
204
+ test("an empty set collapses to empty text, so required still fires", () => {
205
+ expect(buildColumnValueJson({})).toBe("");
206
+ });
207
+
208
+ test("a row with no column name is dropped", () => {
209
+ const rows: Dictionary<DictionaryEntryValue> = {
210
+ "": "orphan",
211
+ } as Dictionary<DictionaryEntryValue>;
212
+
213
+ expect(buildColumnValueJson(rows)).toBe("");
214
+ });
215
+
216
+ test("plain values serialize as plain JSON", () => {
217
+ const rows: Dictionary<DictionaryEntryValue> = {
218
+ name: "abc",
219
+ isEnabled: true,
220
+ } as Dictionary<DictionaryEntryValue>;
221
+
222
+ expect(JSON.parse(buildColumnValueJson(rows))).toEqual({
223
+ name: "abc",
224
+ isEnabled: true,
225
+ });
226
+ });
227
+
228
+ /*
229
+ * The contract with the server: every operator wrapper has to come out as
230
+ * {_type, value}, because that is the only shape JSONFunctions.deserialize
231
+ * turns back into a QueryHelper object.
232
+ */
233
+ test("an operator serializes to the shape the server rehydrates", () => {
234
+ const rows: Dictionary<DictionaryEntryValue> = {
235
+ name: new NotEqual<string>("abc"),
236
+ } as unknown as Dictionary<DictionaryEntryValue>;
237
+
238
+ expect(JSON.parse(buildColumnValueJson(rows))).toEqual({
239
+ name: { _type: ObjectType.NotEqual, value: "abc" },
240
+ });
241
+ });
242
+
243
+ test("every operator the editor offers round-trips through JSON", () => {
244
+ const rows: Dictionary<DictionaryEntryValue> = {
245
+ a: new NotEqual<string>("x"),
246
+ b: new Search("y"),
247
+ c: new GreaterThan<number>(5),
248
+ d: new IsNull(),
249
+ e: new Includes(["p", "q"]),
250
+ } as unknown as Dictionary<DictionaryEntryValue>;
251
+
252
+ const parsed: JSONObject = JSON.parse(buildColumnValueJson(rows));
253
+
254
+ expect((parsed["a"] as JSONObject)["_type"]).toBe(ObjectType.NotEqual);
255
+ expect((parsed["b"] as JSONObject)["_type"]).toBe(ObjectType.Search);
256
+ expect((parsed["c"] as JSONObject)["_type"]).toBe(ObjectType.GreaterThan);
257
+ expect((parsed["d"] as JSONObject)["_type"]).toBe(ObjectType.IsNull);
258
+ expect((parsed["e"] as JSONObject)["_type"]).toBe(ObjectType.Includes);
259
+ });
260
+
261
+ test("a template placeholder survives as an ordinary string value", () => {
262
+ const rows: Dictionary<DictionaryEntryValue> = {
263
+ _id: "{{local.components.api-get-1.returnValues.response-body}}",
264
+ } as Dictionary<DictionaryEntryValue>;
265
+
266
+ expect(JSON.parse(buildColumnValueJson(rows))).toEqual({
267
+ _id: "{{local.components.api-get-1.returnValues.response-body}}",
268
+ });
269
+ });
270
+
271
+ test("what it emits is readable again by the compatibility check", () => {
272
+ const rows: Dictionary<DictionaryEntryValue> = {
273
+ name: new NotEqual<string>("abc"),
274
+ isEnabled: true,
275
+ } as unknown as Dictionary<DictionaryEntryValue>;
276
+
277
+ const roundTripped: JSONObject = JSON.parse(buildColumnValueJson(rows));
278
+
279
+ expect(
280
+ classifyColumnValueCompatibility(
281
+ roundTripped,
282
+ COLUMNS,
283
+ ModelColumnEditorMode.Query,
284
+ ).compatible,
285
+ ).toBe(true);
286
+ });
287
+ });
@@ -0,0 +1,185 @@
1
+ /*
2
+ * The contract that matters: what the row editor writes into a workflow
3
+ * argument has to survive the trip to the server and come back out as the
4
+ * QueryHelper objects TypeORM understands.
5
+ *
6
+ * The builder writes a JSON string. RunWorkflow.getComponentArguments parses
7
+ * it, and FindManyBaseModel (and every other database component) hands the
8
+ * result to JSONFunctions.deserialize before it reaches the query builder.
9
+ * This walks that whole path, so a change to either end that breaks the other
10
+ * fails here rather than at run time.
11
+ */
12
+
13
+ import {
14
+ ModelColumnEditorMode,
15
+ buildColumnValueJson,
16
+ classifyColumnValueCompatibility,
17
+ } from "../../../../UI/Components/Workflow/ModelColumnEditor";
18
+ import Dictionary from "../../../../Types/Dictionary";
19
+ import { DictionaryEntryValue } from "../../../../UI/Components/Dictionary/DictionaryFilterOperator";
20
+ import { JSONObject } from "../../../../Types/JSON";
21
+ import JSONFunctions from "../../../../Types/JSONFunctions";
22
+ import EqualToOrNull from "../../../../Types/BaseDatabase/EqualToOrNull";
23
+ import GreaterThan from "../../../../Types/BaseDatabase/GreaterThan";
24
+ import GreaterThanOrEqual from "../../../../Types/BaseDatabase/GreaterThanOrEqual";
25
+ import Includes from "../../../../Types/BaseDatabase/Includes";
26
+ import IncludesNone from "../../../../Types/BaseDatabase/IncludesNone";
27
+ import IsNull from "../../../../Types/BaseDatabase/IsNull";
28
+ import LessThan from "../../../../Types/BaseDatabase/LessThan";
29
+ import NotEqual from "../../../../Types/BaseDatabase/NotEqual";
30
+ import NotNull from "../../../../Types/BaseDatabase/NotNull";
31
+ import Search from "../../../../Types/BaseDatabase/Search";
32
+ import StartsWith from "../../../../Types/BaseDatabase/StartsWith";
33
+ import { describe, expect, test } from "@jest/globals";
34
+
35
+ type RoundTripFunction = (rows: Dictionary<DictionaryEntryValue>) => JSONObject;
36
+
37
+ /**
38
+ * Builder rows -> stored string -> parsed by the runner -> deserialized by the
39
+ * component. Exactly the path a real run takes.
40
+ */
41
+ const roundTrip: RoundTripFunction = (
42
+ rows: Dictionary<DictionaryEntryValue>,
43
+ ): JSONObject => {
44
+ const stored: string = buildColumnValueJson(rows);
45
+ const parsedByRunner: JSONObject = JSON.parse(stored);
46
+
47
+ return JSONFunctions.deserialize(parsedByRunner) as JSONObject;
48
+ };
49
+
50
+ describe("row editor output survives the server's deserialization", () => {
51
+ test("a plain equality stays a plain value", () => {
52
+ const result: JSONObject = roundTrip({
53
+ name: "acme",
54
+ } as Dictionary<DictionaryEntryValue>);
55
+
56
+ expect(result["name"]).toBe("acme");
57
+ });
58
+
59
+ test("a not-equal comes back as a NotEqual", () => {
60
+ const result: JSONObject = roundTrip({
61
+ name: new NotEqual<string>("acme"),
62
+ } as unknown as Dictionary<DictionaryEntryValue>);
63
+
64
+ expect(result["name"]).toBeInstanceOf(NotEqual);
65
+ expect((result["name"] as NotEqual<string>).value).toBe("acme");
66
+ });
67
+
68
+ test("a contains comes back as a Search", () => {
69
+ const result: JSONObject = roundTrip({
70
+ name: new Search("acm"),
71
+ } as unknown as Dictionary<DictionaryEntryValue>);
72
+
73
+ expect(result["name"]).toBeInstanceOf(Search);
74
+ });
75
+
76
+ test("a starts-with comes back as a StartsWith", () => {
77
+ const result: JSONObject = roundTrip({
78
+ name: new StartsWith("ac"),
79
+ } as unknown as Dictionary<DictionaryEntryValue>);
80
+
81
+ expect(result["name"]).toBeInstanceOf(StartsWith);
82
+ });
83
+
84
+ test("numeric comparisons keep their type and their number", () => {
85
+ const result: JSONObject = roundTrip({
86
+ a: new GreaterThan<number>(5),
87
+ b: new GreaterThanOrEqual<number>(6),
88
+ c: new LessThan<number>(7),
89
+ } as unknown as Dictionary<DictionaryEntryValue>);
90
+
91
+ expect(result["a"]).toBeInstanceOf(GreaterThan);
92
+ expect((result["a"] as GreaterThan<number>).value).toBe(5);
93
+ expect(result["b"]).toBeInstanceOf(GreaterThanOrEqual);
94
+ expect(result["c"]).toBeInstanceOf(LessThan);
95
+ });
96
+
97
+ test("the null checks come back as IsNull and NotNull", () => {
98
+ const result: JSONObject = roundTrip({
99
+ a: new IsNull(),
100
+ b: new NotNull(),
101
+ } as unknown as Dictionary<DictionaryEntryValue>);
102
+
103
+ expect(result["a"]).toBeInstanceOf(IsNull);
104
+ expect(result["b"]).toBeInstanceOf(NotNull);
105
+ });
106
+
107
+ test("membership operators keep their whole list", () => {
108
+ const result: JSONObject = roundTrip({
109
+ a: new Includes(["x", "y"]),
110
+ b: new IncludesNone(["z"]),
111
+ } as unknown as Dictionary<DictionaryEntryValue>);
112
+
113
+ expect(result["a"]).toBeInstanceOf(Includes);
114
+ expect((result["a"] as Includes).values).toEqual(["x", "y"]);
115
+ expect(result["b"]).toBeInstanceOf(IncludesNone);
116
+ });
117
+
118
+ test("equal-to-or-null survives", () => {
119
+ const result: JSONObject = roundTrip({
120
+ a: new EqualToOrNull<string>("x"),
121
+ } as unknown as Dictionary<DictionaryEntryValue>);
122
+
123
+ expect(result["a"]).toBeInstanceOf(EqualToOrNull);
124
+ });
125
+
126
+ test("several conditions in one query all survive together", () => {
127
+ const result: JSONObject = roundTrip({
128
+ name: new Search("acme"),
129
+ isEnabled: true,
130
+ count: new GreaterThan<number>(3),
131
+ deletedAt: new IsNull(),
132
+ } as unknown as Dictionary<DictionaryEntryValue>);
133
+
134
+ expect(Object.keys(result).sort()).toEqual([
135
+ "count",
136
+ "deletedAt",
137
+ "isEnabled",
138
+ "name",
139
+ ]);
140
+ expect(result["isEnabled"]).toBe(true);
141
+ expect(result["name"]).toBeInstanceOf(Search);
142
+ });
143
+
144
+ /*
145
+ * A template is substituted before the component ever sees the value, so at
146
+ * this point it is just a string — it must not be mistaken for structure.
147
+ */
148
+ test("a template placeholder passes through untouched", () => {
149
+ const result: JSONObject = roundTrip({
150
+ _id: "{{local.components.api-get-1.returnValues.response-body}}",
151
+ } as Dictionary<DictionaryEntryValue>);
152
+
153
+ expect(result["_id"]).toBe(
154
+ "{{local.components.api-get-1.returnValues.response-body}}",
155
+ );
156
+ });
157
+
158
+ test("a record of values to write deserializes to plain values", () => {
159
+ const stored: string = buildColumnValueJson({
160
+ name: "acme",
161
+ isEnabled: true,
162
+ count: 4,
163
+ } as Dictionary<DictionaryEntryValue>);
164
+
165
+ expect(JSONFunctions.deserialize(JSON.parse(stored))).toEqual({
166
+ name: "acme",
167
+ isEnabled: true,
168
+ count: 4,
169
+ });
170
+ });
171
+
172
+ test("what the editor emits, the editor can read back", () => {
173
+ const rows: Dictionary<DictionaryEntryValue> = {
174
+ name: new Search("acme"),
175
+ count: new GreaterThan<number>(3),
176
+ } as unknown as Dictionary<DictionaryEntryValue>;
177
+
178
+ const stored: JSONObject = JSON.parse(buildColumnValueJson(rows));
179
+
180
+ expect(
181
+ classifyColumnValueCompatibility(stored, [], ModelColumnEditorMode.Query)
182
+ .compatible,
183
+ ).toBe(true);
184
+ });
185
+ });
@@ -0,0 +1,174 @@
1
+ jest.mock("../../../../UI/Utils/API/API", () => {
2
+ return {
3
+ __esModule: true,
4
+ default: {
5
+ get: jest.fn(),
6
+ getFriendlyMessage: jest.fn((err: unknown) => {
7
+ return String(err);
8
+ }),
9
+ },
10
+ };
11
+ });
12
+
13
+ import {
14
+ ModelSchemaColumn,
15
+ fetchModelSchema,
16
+ findColumn,
17
+ isBooleanColumn,
18
+ isNumericColumn,
19
+ isScalarColumn,
20
+ } from "../../../../UI/Components/Workflow/ModelSchema";
21
+ import API from "../../../../UI/Utils/API/API";
22
+ import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
23
+ import { afterEach, describe, expect, test } from "@jest/globals";
24
+
25
+ type MakeColumnFunction = (
26
+ overrides: Partial<ModelSchemaColumn>,
27
+ ) => ModelSchemaColumn;
28
+
29
+ const makeColumn: MakeColumnFunction = (
30
+ overrides: Partial<ModelSchemaColumn>,
31
+ ): ModelSchemaColumn => {
32
+ return {
33
+ id: "name",
34
+ title: "Name",
35
+ type: "ShortText",
36
+ isRelation: false,
37
+ ...overrides,
38
+ };
39
+ };
40
+
41
+ describe("isScalarColumn", () => {
42
+ test("accepts the column types a single input can hold", () => {
43
+ const scalarTypes: Array<string> = [
44
+ "ShortText",
45
+ "LongText",
46
+ "ObjectID",
47
+ "Number",
48
+ "Boolean",
49
+ "Date",
50
+ "Email",
51
+ "URL",
52
+ "Markdown",
53
+ ];
54
+
55
+ for (const type of scalarTypes) {
56
+ expect(isScalarColumn(makeColumn({ type: type }))).toBe(true);
57
+ }
58
+ });
59
+
60
+ test("rejects a relation, however it is typed", () => {
61
+ expect(
62
+ isScalarColumn(makeColumn({ type: "ShortText", isRelation: true })),
63
+ ).toBe(false);
64
+ expect(
65
+ isScalarColumn(makeColumn({ type: "Entity", isRelation: true })),
66
+ ).toBe(false);
67
+ });
68
+
69
+ test("rejects types a single box cannot hold", () => {
70
+ expect(isScalarColumn(makeColumn({ type: "JSON" }))).toBe(false);
71
+ expect(isScalarColumn(makeColumn({ type: "Buffer" }))).toBe(false);
72
+ expect(isScalarColumn(makeColumn({ type: "EntityArray" }))).toBe(false);
73
+ });
74
+
75
+ test("rejects a type it has never heard of", () => {
76
+ expect(isScalarColumn(makeColumn({ type: "SomethingNew" }))).toBe(false);
77
+ });
78
+ });
79
+
80
+ describe("isNumericColumn", () => {
81
+ test("is true for the two numeric types", () => {
82
+ expect(isNumericColumn(makeColumn({ type: "Number" }))).toBe(true);
83
+ expect(isNumericColumn(makeColumn({ type: "PositiveNumber" }))).toBe(true);
84
+ });
85
+
86
+ test("is false for everything else", () => {
87
+ expect(isNumericColumn(makeColumn({ type: "ShortText" }))).toBe(false);
88
+ expect(isNumericColumn(makeColumn({ type: "Boolean" }))).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe("isBooleanColumn", () => {
93
+ test("is true only for Boolean", () => {
94
+ expect(isBooleanColumn(makeColumn({ type: "Boolean" }))).toBe(true);
95
+ expect(isBooleanColumn(makeColumn({ type: "ShortText" }))).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe("findColumn", () => {
100
+ const columns: Array<ModelSchemaColumn> = [
101
+ makeColumn({ id: "_id" }),
102
+ makeColumn({ id: "name" }),
103
+ ];
104
+
105
+ test("finds a column by id", () => {
106
+ expect(findColumn(columns, "_id")?.id).toBe("_id");
107
+ });
108
+
109
+ test("returns nothing for an id that isn't there", () => {
110
+ expect(findColumn(columns, "id")).toBeUndefined();
111
+ });
112
+
113
+ test("is case sensitive, as column names are", () => {
114
+ expect(findColumn(columns, "Name")).toBeUndefined();
115
+ });
116
+
117
+ test("copes with an empty list", () => {
118
+ expect(findColumn([], "name")).toBeUndefined();
119
+ });
120
+ });
121
+
122
+ describe("fetchModelSchema", () => {
123
+ afterEach(() => {
124
+ jest.clearAllMocks();
125
+ });
126
+
127
+ type MockedGetFunction = (value: unknown) => void;
128
+
129
+ const mockGet: MockedGetFunction = (value: unknown): void => {
130
+ (API.get as unknown as jest.Mock).mockResolvedValue(value);
131
+ };
132
+
133
+ test("returns the columns the endpoint sent", async () => {
134
+ mockGet({
135
+ data: { tableName: "Monitor", columns: [makeColumn({ id: "_id" })] },
136
+ });
137
+
138
+ const columns: Array<ModelSchemaColumn> = await fetchModelSchema("Monitor");
139
+
140
+ expect(columns).toHaveLength(1);
141
+ expect(columns[0]?.id).toBe("_id");
142
+ });
143
+
144
+ test("returns an empty list when the endpoint sends no columns", async () => {
145
+ mockGet({ data: { tableName: "Monitor" } });
146
+
147
+ await expect(fetchModelSchema("Monitor")).resolves.toEqual([]);
148
+ });
149
+
150
+ test("throws when the endpoint answers with an error", async () => {
151
+ const errorResponse: HTTPErrorResponse = new HTTPErrorResponse(
152
+ 500,
153
+ { message: "boom" },
154
+ {},
155
+ );
156
+
157
+ mockGet(errorResponse);
158
+
159
+ await expect(fetchModelSchema("Monitor")).rejects.toBe(errorResponse);
160
+ });
161
+
162
+ test("escapes the table name into the path", async () => {
163
+ mockGet({ data: { columns: [] } });
164
+
165
+ await fetchModelSchema("Weird/Name");
166
+
167
+ const call: { url: { toString: () => string } } = (
168
+ API.get as unknown as jest.Mock
169
+ ).mock.calls[0][0] as { url: { toString: () => string } };
170
+
171
+ expect(call.url.toString()).toContain("Weird%2FName");
172
+ expect(call.url.toString()).not.toContain("Weird/Name");
173
+ });
174
+ });