@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,379 @@
1
+ import {
2
+ JSONSyntaxCheckResult,
3
+ ParsedReferencePath,
4
+ ReferenceRootType,
5
+ TemplateExpression,
6
+ TemplateExpressionKind,
7
+ checkJSONSyntax,
8
+ hasEachBlock,
9
+ looksLikeReferencePath,
10
+ maskTemplateExpressions,
11
+ parseReferencePath,
12
+ parseTemplateExpressions,
13
+ } from "../../../Types/Workflow/TemplateSyntax";
14
+ import { describe, expect, test } from "@jest/globals";
15
+
16
+ describe("parseTemplateExpressions", () => {
17
+ test("finds nothing in text with no expressions", () => {
18
+ expect(parseTemplateExpressions("just some text")).toEqual([]);
19
+ expect(parseTemplateExpressions("")).toEqual([]);
20
+ });
21
+
22
+ test("is non-greedy, so adjacent expressions stay separate", () => {
23
+ const found: Array<TemplateExpression> = parseTemplateExpressions(
24
+ "{{local.variables.a}} and {{local.variables.b}}",
25
+ );
26
+
27
+ expect(found).toHaveLength(2);
28
+ expect(found[0]?.inner).toBe("local.variables.a");
29
+ expect(found[1]?.inner).toBe("local.variables.b");
30
+ });
31
+
32
+ test("records the exact source offsets", () => {
33
+ const found: Array<TemplateExpression> =
34
+ parseTemplateExpressions("ab{{x}}cd");
35
+
36
+ expect(found[0]?.startIndex).toBe(2);
37
+ expect(found[0]?.endIndex).toBe(7);
38
+ expect(found[0]?.raw).toBe("{{x}}");
39
+ });
40
+
41
+ test("keeps the untrimmed capture alongside the trimmed one", () => {
42
+ const found: Array<TemplateExpression> = parseTemplateExpressions(
43
+ "{{ local.variables.a }}",
44
+ );
45
+
46
+ expect(found[0]?.inner).toBe("local.variables.a");
47
+ expect(found[0]?.innerRaw).toBe(" local.variables.a ");
48
+ });
49
+
50
+ test("classifies each kind of expression", () => {
51
+ const found: Array<TemplateExpression> = parseTemplateExpressions(
52
+ "{{#each local.components.a.returnValues.items}}{{@index}}{{this}}{{name}}{{/each}}",
53
+ );
54
+
55
+ expect(
56
+ found.map((expression: TemplateExpression) => {
57
+ return expression.kind;
58
+ }),
59
+ ).toEqual([
60
+ TemplateExpressionKind.EachOpen,
61
+ TemplateExpressionKind.LoopIndex,
62
+ TemplateExpressionKind.LoopThis,
63
+ TemplateExpressionKind.Reference,
64
+ TemplateExpressionKind.EachClose,
65
+ ]);
66
+ });
67
+
68
+ test("marks loop bodies as inside a block, and the tags themselves as not", () => {
69
+ const found: Array<TemplateExpression> = parseTemplateExpressions(
70
+ "{{outer}}{{#each items}}{{inner}}{{/each}}{{after}}",
71
+ );
72
+
73
+ const byInner: Record<string, boolean> = {};
74
+
75
+ for (const expression of found) {
76
+ byInner[expression.inner] = expression.isInsideEachBlock;
77
+ }
78
+
79
+ expect(byInner["outer"]).toBe(false);
80
+ expect(byInner["#each items"]).toBe(false);
81
+ expect(byInner["inner"]).toBe(true);
82
+ expect(byInner["/each"]).toBe(false);
83
+ expect(byInner["after"]).toBe(false);
84
+ });
85
+
86
+ test("tracks nesting depth across nested loops", () => {
87
+ const found: Array<TemplateExpression> = parseTemplateExpressions(
88
+ "{{#each a}}{{one}}{{#each b}}{{two}}{{/each}}{{three}}{{/each}}{{four}}",
89
+ );
90
+
91
+ const byInner: Record<string, boolean> = {};
92
+
93
+ for (const expression of found) {
94
+ byInner[expression.inner] = expression.isInsideEachBlock;
95
+ }
96
+
97
+ expect(byInner["one"]).toBe(true);
98
+ expect(byInner["two"]).toBe(true);
99
+ expect(byInner["three"]).toBe(true);
100
+ expect(byInner["four"]).toBe(false);
101
+ });
102
+
103
+ test("does not carry loop state between calls", () => {
104
+ parseTemplateExpressions("{{#each a}}{{x}}");
105
+
106
+ const found: Array<TemplateExpression> = parseTemplateExpressions("{{y}}");
107
+
108
+ expect(found[0]?.isInsideEachBlock).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe("hasEachBlock", () => {
113
+ test("detects opening and closing tags", () => {
114
+ expect(hasEachBlock("{{#each items}}x{{/each}}")).toBe(true);
115
+ expect(hasEachBlock("{{#each items}}")).toBe(true);
116
+ expect(hasEachBlock("{{/each}}")).toBe(true);
117
+ });
118
+
119
+ test("is false for ordinary references and empty input", () => {
120
+ expect(hasEachBlock("{{local.variables.a}}")).toBe(false);
121
+ expect(hasEachBlock("")).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe("maskTemplateExpressions", () => {
126
+ test("substitutes a token that is legal as a value, in a string, and as a key", () => {
127
+ expect(maskTemplateExpressions('{"n": {{a}}}')).toBe('{"n": 1}');
128
+ expect(maskTemplateExpressions('{"s": "{{a}}"}')).toBe('{"s": "1"}');
129
+ expect(maskTemplateExpressions('{"{{a}}": 2}')).toBe('{"1": 2}');
130
+ });
131
+
132
+ test("leaves text without expressions alone", () => {
133
+ expect(maskTemplateExpressions('{"a": 1}')).toBe('{"a": 1}');
134
+ });
135
+ });
136
+
137
+ describe("checkJSONSyntax", () => {
138
+ test("accepts ordinary JSON", () => {
139
+ expect(checkJSONSyntax('{"a": 1}').isValid).toBe(true);
140
+ expect(checkJSONSyntax("[1, 2, 3]").isValid).toBe(true);
141
+ });
142
+
143
+ test("rejects the mistakes it exists to catch", () => {
144
+ const trailingComma: JSONSyntaxCheckResult = checkJSONSyntax('{"a": 1,}');
145
+
146
+ expect(trailingComma.isValid).toBe(false);
147
+ expect(trailingComma.wasSkipped).toBe(false);
148
+ expect(trailingComma.errorMessage).toBeTruthy();
149
+
150
+ expect(checkJSONSyntax('{"a": 1').isValid).toBe(false);
151
+ expect(checkJSONSyntax("{a: 1}").isValid).toBe(false);
152
+ expect(checkJSONSyntax("{'a': 1}").isValid).toBe(false);
153
+ expect(checkJSONSyntax("not json at all").isValid).toBe(false);
154
+ });
155
+
156
+ test("carries the parser's own message, unframed", () => {
157
+ const result: JSONSyntaxCheckResult = checkJSONSyntax('{"a": 1,}');
158
+
159
+ expect(result.errorMessage).not.toMatch(/is not valid JSON/);
160
+ expect(result.errorMessage).toMatch(/JSON|token|position/i);
161
+ });
162
+
163
+ /*
164
+ * The false-positive cases. Each of these is something a workflow author
165
+ * legitimately writes, and flagging any of them would disable Save on a
166
+ * workflow that runs correctly.
167
+ */
168
+ test("accepts a whole-field reference, which is not JSON as written", () => {
169
+ expect(
170
+ checkJSONSyntax("{{local.components.api-get-1.returnValues.body}}")
171
+ .isValid,
172
+ ).toBe(true);
173
+ });
174
+
175
+ test("accepts a reference standing in for a bare JSON value", () => {
176
+ expect(
177
+ checkJSONSyntax('{"retries": {{local.variables.count}}}').isValid,
178
+ ).toBe(true);
179
+ });
180
+
181
+ test("accepts a reference inside a string literal", () => {
182
+ expect(
183
+ checkJSONSyntax('{"token": "Bearer {{local.variables.apiKey}}"}').isValid,
184
+ ).toBe(true);
185
+ });
186
+
187
+ test("accepts a reference used as an object key", () => {
188
+ expect(checkJSONSyntax('{"{{local.variables.k}}": 1}').isValid).toBe(true);
189
+ });
190
+
191
+ test("accepts a reference inside an array", () => {
192
+ expect(checkJSONSyntax('[{{local.variables.a}}, "b"]').isValid).toBe(true);
193
+ });
194
+
195
+ test("declines to judge anything containing a loop", () => {
196
+ const looped: JSONSyntaxCheckResult = checkJSONSyntax(
197
+ '[{{#each local.components.a.returnValues.items}}{"id":"{{this}}"},{{/each}}]',
198
+ );
199
+
200
+ expect(looped.isValid).toBe(true);
201
+ expect(looped.wasSkipped).toBe(true);
202
+ });
203
+
204
+ test("skips values that are not strings", () => {
205
+ expect(checkJSONSyntax({ a: 1 }).wasSkipped).toBe(true);
206
+ expect(checkJSONSyntax(["a"]).wasSkipped).toBe(true);
207
+ expect(checkJSONSyntax(12).wasSkipped).toBe(true);
208
+ expect(checkJSONSyntax(true).wasSkipped).toBe(true);
209
+ expect(checkJSONSyntax(null).wasSkipped).toBe(true);
210
+ expect(checkJSONSyntax(undefined).wasSkipped).toBe(true);
211
+ });
212
+
213
+ test("skips empty values, leaving the required-field message to stand", () => {
214
+ expect(checkJSONSyntax("").wasSkipped).toBe(true);
215
+ expect(checkJSONSyntax(" ").wasSkipped).toBe(true);
216
+ expect(checkJSONSyntax("\n").wasSkipped).toBe(true);
217
+ });
218
+
219
+ test("judges by JSON5's rules when asked, matching the lenient parsers", () => {
220
+ expect(checkJSONSyntax("{a: 1}", { allowJSON5: true }).isValid).toBe(true);
221
+ expect(checkJSONSyntax("{'a': 1}", { allowJSON5: true }).isValid).toBe(
222
+ true,
223
+ );
224
+ expect(checkJSONSyntax('{"a": 1,}', { allowJSON5: true }).isValid).toBe(
225
+ true,
226
+ );
227
+ });
228
+
229
+ test("still rejects unparseable text under JSON5", () => {
230
+ expect(checkJSONSyntax("{oops", { allowJSON5: true }).isValid).toBe(false);
231
+ });
232
+
233
+ test("keeps templates working under JSON5 too", () => {
234
+ expect(
235
+ checkJSONSyntax('{a: "{{local.variables.x}}"}', { allowJSON5: true })
236
+ .isValid,
237
+ ).toBe(true);
238
+ });
239
+ });
240
+
241
+ describe("looksLikeReferencePath", () => {
242
+ test("accepts real reference paths", () => {
243
+ expect(looksLikeReferencePath("local.variables.apiKey")).toBe(true);
244
+ expect(
245
+ looksLikeReferencePath(
246
+ "local.components.api-get-1.returnValues.response-body",
247
+ ),
248
+ ).toBe(true);
249
+ expect(looksLikeReferencePath("global.variables.My Key")).toBe(true);
250
+ expect(looksLikeReferencePath("local.componets.x.returnValues.y")).toBe(
251
+ true,
252
+ );
253
+ });
254
+
255
+ test("accepts array accessors", () => {
256
+ expect(looksLikeReferencePath("a.items[0].name")).toBe(true);
257
+ expect(looksLikeReferencePath("a.items[last]")).toBe(true);
258
+ });
259
+
260
+ test("rejects captures that are really JavaScript", () => {
261
+ expect(looksLikeReferencePath(" return 1; ")).toBe(false);
262
+ expect(looksLikeReferencePath("a = b")).toBe(false);
263
+ expect(looksLikeReferencePath('x: "y"')).toBe(false);
264
+ expect(looksLikeReferencePath("f(x)")).toBe(false);
265
+ expect(looksLikeReferencePath("a + b")).toBe(false);
266
+ });
267
+
268
+ test("rejects an empty capture", () => {
269
+ expect(looksLikeReferencePath("")).toBe(false);
270
+ expect(looksLikeReferencePath(" ")).toBe(false);
271
+ });
272
+ });
273
+
274
+ describe("parseReferencePath", () => {
275
+ test("reads a local variable", () => {
276
+ const parsed: ParsedReferencePath = parseReferencePath(
277
+ "local.variables.apiKey",
278
+ );
279
+
280
+ expect(parsed.rootType).toBe(ReferenceRootType.LocalVariable);
281
+ expect(parsed.variableName).toBe("apiKey");
282
+ });
283
+
284
+ test("reads a global variable", () => {
285
+ const parsed: ParsedReferencePath = parseReferencePath(
286
+ "global.variables.region",
287
+ );
288
+
289
+ expect(parsed.rootType).toBe(ReferenceRootType.GlobalVariable);
290
+ expect(parsed.variableName).toBe("region");
291
+ });
292
+
293
+ test("reads a component return value, exactly as the picker writes it", () => {
294
+ const parsed: ParsedReferencePath = parseReferencePath(
295
+ "local.components.api-get-1.returnValues.response-body",
296
+ );
297
+
298
+ expect(parsed.rootType).toBe(ReferenceRootType.ComponentReturnValue);
299
+ expect(parsed.componentId).toBe("api-get-1");
300
+ expect(parsed.returnValueId).toBe("response-body");
301
+ });
302
+
303
+ test("stops at the return value and ignores the drill-in below it", () => {
304
+ const parsed: ParsedReferencePath = parseReferencePath(
305
+ "local.components.api-get-1.returnValues.response-body.data.items[0].id",
306
+ );
307
+
308
+ expect(parsed.rootType).toBe(ReferenceRootType.ComponentReturnValue);
309
+ expect(parsed.componentId).toBe("api-get-1");
310
+ expect(parsed.returnValueId).toBe("response-body");
311
+ });
312
+
313
+ test("strips array accessors when naming the parts", () => {
314
+ const parsed: ParsedReferencePath = parseReferencePath(
315
+ "local.components.api-get-1.returnValues.items[last]",
316
+ );
317
+
318
+ expect(parsed.returnValueId).toBe("items");
319
+ });
320
+
321
+ test("rejects an unknown root", () => {
322
+ const parsed: ParsedReferencePath = parseReferencePath("componets.x");
323
+
324
+ expect(parsed.rootType).toBe(ReferenceRootType.Unknown);
325
+ expect(parsed.reason).toMatch(/local|global/);
326
+ });
327
+
328
+ test("rejects a misspelled second segment", () => {
329
+ expect(
330
+ parseReferencePath("local.component.x.returnValues.y").rootType,
331
+ ).toBe(ReferenceRootType.Unknown);
332
+ expect(parseReferencePath("global.variable.x").rootType).toBe(
333
+ ReferenceRootType.Unknown,
334
+ );
335
+ });
336
+
337
+ test("rejects a component path that does not say returnValues", () => {
338
+ const parsed: ParsedReferencePath = parseReferencePath(
339
+ "local.components.api-get-1.body",
340
+ );
341
+
342
+ expect(parsed.rootType).toBe(ReferenceRootType.Unknown);
343
+ expect(parsed.reason).toMatch(/returnValues/);
344
+ });
345
+
346
+ test("rejects truncated paths", () => {
347
+ expect(parseReferencePath("local.variables").rootType).toBe(
348
+ ReferenceRootType.Unknown,
349
+ );
350
+ expect(parseReferencePath("local.components").rootType).toBe(
351
+ ReferenceRootType.Unknown,
352
+ );
353
+ expect(
354
+ parseReferencePath("local.components.api-get-1.returnValues").rootType,
355
+ ).toBe(ReferenceRootType.Unknown);
356
+ });
357
+
358
+ test("rejects an empty part between two dots, which resolves to nothing", () => {
359
+ const parsed: ParsedReferencePath = parseReferencePath(
360
+ "local..variables.apiKey",
361
+ );
362
+
363
+ expect(parsed.rootType).toBe(ReferenceRootType.Unknown);
364
+ expect(parsed.reason).toMatch(/empty part/);
365
+ });
366
+
367
+ test("rejects an empty path", () => {
368
+ expect(parseReferencePath("").rootType).toBe(ReferenceRootType.Unknown);
369
+ });
370
+
371
+ test("allows spaces and hyphens inside a name, since only dots are structural", () => {
372
+ expect(parseReferencePath("global.variables.My Key").variableName).toBe(
373
+ "My Key",
374
+ );
375
+ expect(parseReferencePath("local.variables.api-key").variableName).toBe(
376
+ "api-key",
377
+ );
378
+ });
379
+ });