@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,591 @@
1
+ /*
2
+ * Static checks over a workflow graph, run in the builder as the user edits.
3
+ *
4
+ * Every rule here corresponds to a way the runner (App/FeatureSet/Workflow/
5
+ * Services/RunWorkflow.ts) either throws at run time or — worse — carries on
6
+ * quietly with the wrong value. The point is to move those failures from "3am,
7
+ * in a log blob, after the trigger already fired" to "in the editor, before
8
+ * you save".
9
+ *
10
+ * Rules only fire on things that are knowable from the graph alone. Anything
11
+ * needing a network round trip (does this workflow variable exist?) is checked
12
+ * for shape but not for existence, because a false positive here puts a red
13
+ * badge on a workflow that runs perfectly.
14
+ */
15
+
16
+ import Dictionary from "../../../Types/Dictionary";
17
+ import { JSONObject, JSONValue } from "../../../Types/JSON";
18
+ import {
19
+ Argument,
20
+ ComponentInputType,
21
+ ComponentType,
22
+ NodeDataProp,
23
+ NodeType,
24
+ ReturnValue,
25
+ isJSON5ToleratedInputType,
26
+ } from "../../../Types/Workflow/Component";
27
+ import {
28
+ ParsedReferencePath,
29
+ ReferenceRootType,
30
+ TemplateExpression,
31
+ TemplateExpressionKind,
32
+ checkJSONSyntax,
33
+ looksLikeReferencePath,
34
+ parseReferencePath,
35
+ parseTemplateExpressions,
36
+ } from "../../../Types/Workflow/TemplateSyntax";
37
+
38
+ export enum WorkflowLintSeverity {
39
+ /** The workflow will fail, or silently do the wrong thing, as written. */
40
+ Error = "Error",
41
+ /** Suspicious but survivable — e.g. a step that can never be reached. */
42
+ Warning = "Warning",
43
+ }
44
+
45
+ export enum WorkflowLintRule {
46
+ MissingRequiredArgument = "MissingRequiredArgument",
47
+ InvalidJSON = "InvalidJSON",
48
+ ReferenceHasWhitespace = "ReferenceHasWhitespace",
49
+ UnknownReferenceRoot = "UnknownReferenceRoot",
50
+ UnknownReferencedComponent = "UnknownReferencedComponent",
51
+ UnknownReferencedReturnValue = "UnknownReferencedReturnValue",
52
+ SelfReference = "SelfReference",
53
+ ForwardReference = "ForwardReference",
54
+ ReferenceToUnreachableComponent = "ReferenceToUnreachableComponent",
55
+ DuplicateComponentId = "DuplicateComponentId",
56
+ ComponentIdContainsDot = "ComponentIdContainsDot",
57
+ UnreachableComponent = "UnreachableComponent",
58
+ NoTrigger = "NoTrigger",
59
+ }
60
+
61
+ export interface WorkflowLintIssue {
62
+ rule: WorkflowLintRule;
63
+ severity: WorkflowLintSeverity;
64
+ /** The react-flow node id, or null when the issue is about the graph itself. */
65
+ nodeId: string | null;
66
+ /** The step's user-facing id ("api-get-1"), when the issue belongs to a step. */
67
+ componentId: string | null;
68
+ /** The argument the issue is about, when it is about one. */
69
+ argumentId: string | null;
70
+ /** Shown to the builder, verbatim. */
71
+ message: string;
72
+ }
73
+
74
+ export interface WorkflowLintResult {
75
+ issues: Array<WorkflowLintIssue>;
76
+ /**
77
+ * One combined message per react-flow node id, ready to drop into
78
+ * NodeDataProp.error so the canvas shows its (already implemented, until now
79
+ * never populated) red badge.
80
+ */
81
+ errorsByNodeId: Dictionary<string>;
82
+ errorCount: number;
83
+ warningCount: number;
84
+ }
85
+
86
+ /** Minimal structural view of a react-flow node — keeps this file testable. */
87
+ export interface LintGraphNode {
88
+ id: string;
89
+ data: NodeDataProp;
90
+ }
91
+
92
+ /** Minimal structural view of a react-flow edge. */
93
+ export interface LintGraphEdge {
94
+ source: string;
95
+ target: string;
96
+ }
97
+
98
+ /*
99
+ * Argument types the builder stores as a JSON document in a code editor. A
100
+ * malformed one throws in RunWorkflow.getComponentArguments (for JSON / Query /
101
+ * Select) or in the component itself (JSONFunctions.parse in
102
+ * CreateOneBaseModel, ApiComponentUtils.sanitizeArgs, and friends), so all of
103
+ * them are worth checking up front.
104
+ */
105
+ const JSON_DOCUMENT_INPUT_TYPES: Array<ComponentInputType> = [
106
+ ComponentInputType.JSON,
107
+ ComponentInputType.JSONArray,
108
+ ComponentInputType.Query,
109
+ ComponentInputType.Select,
110
+ ComponentInputType.BaseModel,
111
+ ComponentInputType.BaseModelArray,
112
+ ComponentInputType.StringDictionary,
113
+ ];
114
+
115
+ type IsEmptyArgumentValueFunction = (value: JSONValue | undefined) => boolean;
116
+
117
+ /*
118
+ * `false` and `0` are values a builder deliberately set — only absence and the
119
+ * empty box count as missing. An empty object/array counts as missing too: the
120
+ * Dictionary editor writes `{}` for a field nobody filled in.
121
+ */
122
+ const isEmptyArgumentValue: IsEmptyArgumentValueFunction = (
123
+ value: JSONValue | undefined,
124
+ ): boolean => {
125
+ if (value === undefined || value === null) {
126
+ return true;
127
+ }
128
+
129
+ if (typeof value === "string") {
130
+ return value.trim() === "";
131
+ }
132
+
133
+ if (Array.isArray(value)) {
134
+ return value.length === 0;
135
+ }
136
+
137
+ if (typeof value === "object") {
138
+ return Object.keys(value as JSONObject).length === 0;
139
+ }
140
+
141
+ return false;
142
+ };
143
+
144
+ type IsRealNodeFunction = (node: LintGraphNode) => boolean;
145
+
146
+ const isRealNode: IsRealNodeFunction = (node: LintGraphNode): boolean => {
147
+ return Boolean(
148
+ node.data &&
149
+ node.data.nodeType !== NodeType.PlaceholderNode &&
150
+ node.data.metadata,
151
+ );
152
+ };
153
+
154
+ type BuildReachableSetFunction = (
155
+ startNodeIds: Array<string>,
156
+ adjacency: Dictionary<Array<string>>,
157
+ ) => Set<string>;
158
+
159
+ /** Every node id reachable by following edges forward from `startNodeIds`. */
160
+ const buildReachableSet: BuildReachableSetFunction = (
161
+ startNodeIds: Array<string>,
162
+ adjacency: Dictionary<Array<string>>,
163
+ ): Set<string> => {
164
+ const reachable: Set<string> = new Set<string>();
165
+ const queue: Array<string> = [...startNodeIds];
166
+
167
+ while (queue.length > 0) {
168
+ const current: string = queue.shift() as string;
169
+
170
+ if (reachable.has(current)) {
171
+ continue;
172
+ }
173
+
174
+ reachable.add(current);
175
+
176
+ for (const next of adjacency[current] || []) {
177
+ if (!reachable.has(next)) {
178
+ queue.push(next);
179
+ }
180
+ }
181
+ }
182
+
183
+ return reachable;
184
+ };
185
+
186
+ type CollectArgumentStringsFunction = (value: JSONValue) => Array<string>;
187
+
188
+ /*
189
+ * A reference can hide anywhere a string can: the Dictionary editor stores an
190
+ * object whose values are strings, and imported graphs can hold arrays. Walk
191
+ * the whole value so {{...}} inside a header value is linted too.
192
+ */
193
+ const collectArgumentStrings: CollectArgumentStringsFunction = (
194
+ value: JSONValue,
195
+ ): Array<string> => {
196
+ if (typeof value === "string") {
197
+ return [value];
198
+ }
199
+
200
+ if (Array.isArray(value)) {
201
+ return value.flatMap((item: JSONValue) => {
202
+ return collectArgumentStrings(item);
203
+ });
204
+ }
205
+
206
+ if (value && typeof value === "object") {
207
+ return Object.values(value as JSONObject).flatMap((item: JSONValue) => {
208
+ return collectArgumentStrings(item as JSONValue);
209
+ });
210
+ }
211
+
212
+ return [];
213
+ };
214
+
215
+ export type LintWorkflowGraphFunction = (graph: {
216
+ nodes: Array<LintGraphNode>;
217
+ edges: Array<LintGraphEdge>;
218
+ }) => WorkflowLintResult;
219
+
220
+ /**
221
+ * Check a workflow graph and report everything wrong with it.
222
+ */
223
+ export const lintWorkflowGraph: LintWorkflowGraphFunction = (graph: {
224
+ nodes: Array<LintGraphNode>;
225
+ edges: Array<LintGraphEdge>;
226
+ }): WorkflowLintResult => {
227
+ const issues: Array<WorkflowLintIssue> = [];
228
+ const nodes: Array<LintGraphNode> = (graph.nodes || []).filter(isRealNode);
229
+ const edges: Array<LintGraphEdge> = graph.edges || [];
230
+
231
+ type AddIssueFunction = (issue: WorkflowLintIssue) => void;
232
+
233
+ const addIssue: AddIssueFunction = (issue: WorkflowLintIssue): void => {
234
+ issues.push(issue);
235
+ };
236
+
237
+ /* ---- indexes ---------------------------------------------------------- */
238
+
239
+ // The runner keys its storage map on node.data.id, not the react-flow id.
240
+ const nodesByComponentId: Dictionary<LintGraphNode> = {};
241
+ const componentIdCounts: Dictionary<number> = {};
242
+
243
+ for (const node of nodes) {
244
+ const componentId: string = node.data.id;
245
+
246
+ if (!componentId) {
247
+ continue;
248
+ }
249
+
250
+ componentIdCounts[componentId] = (componentIdCounts[componentId] || 0) + 1;
251
+
252
+ if (!nodesByComponentId[componentId]) {
253
+ nodesByComponentId[componentId] = node;
254
+ }
255
+ }
256
+
257
+ const adjacency: Dictionary<Array<string>> = {};
258
+
259
+ for (const edge of edges) {
260
+ if (!adjacency[edge.source]) {
261
+ adjacency[edge.source] = [];
262
+ }
263
+ adjacency[edge.source]?.push(edge.target);
264
+ }
265
+
266
+ const triggerNodes: Array<LintGraphNode> = nodes.filter(
267
+ (node: LintGraphNode) => {
268
+ return node.data.componentType === ComponentType.Trigger;
269
+ },
270
+ );
271
+
272
+ const reachableFromTrigger: Set<string> = buildReachableSet(
273
+ triggerNodes.map((node: LintGraphNode) => {
274
+ return node.id;
275
+ }),
276
+ adjacency,
277
+ );
278
+
279
+ /*
280
+ * Strict descendants of a node, computed at most once per node — the
281
+ * reference rules ask for this per argument, and recomputing a traversal
282
+ * inside that loop makes linting quadratic on graphs that are already wide.
283
+ */
284
+ const descendantCache: Dictionary<Set<string>> = {};
285
+
286
+ type GetDescendantsFunction = (nodeId: string) => Set<string>;
287
+
288
+ const getDescendants: GetDescendantsFunction = (
289
+ nodeId: string,
290
+ ): Set<string> => {
291
+ let descendants: Set<string> | undefined = descendantCache[nodeId];
292
+
293
+ if (!descendants) {
294
+ descendants = buildReachableSet(adjacency[nodeId] || [], adjacency);
295
+ descendantCache[nodeId] = descendants;
296
+ }
297
+
298
+ return descendants;
299
+ };
300
+
301
+ /* ---- graph-wide rules -------------------------------------------------- */
302
+
303
+ if (nodes.length > 0 && triggerNodes.length === 0) {
304
+ addIssue({
305
+ rule: WorkflowLintRule.NoTrigger,
306
+ severity: WorkflowLintSeverity.Error,
307
+ nodeId: null,
308
+ componentId: null,
309
+ argumentId: null,
310
+ message:
311
+ "This workflow has no trigger, so it can never start. Add a trigger and connect it to the first step.",
312
+ });
313
+ }
314
+
315
+ /* ---- per-node rules ---------------------------------------------------- */
316
+
317
+ for (const node of nodes) {
318
+ const data: NodeDataProp = node.data;
319
+ const componentId: string = data.id;
320
+ const argumentValues: JSONObject = (data.arguments as JSONObject) || {};
321
+
322
+ type AddNodeIssueFunction = (params: {
323
+ rule: WorkflowLintRule;
324
+ severity: WorkflowLintSeverity;
325
+ argumentId?: string | undefined;
326
+ message: string;
327
+ }) => void;
328
+
329
+ const addNodeIssue: AddNodeIssueFunction = (params: {
330
+ rule: WorkflowLintRule;
331
+ severity: WorkflowLintSeverity;
332
+ argumentId?: string | undefined;
333
+ message: string;
334
+ }): void => {
335
+ addIssue({
336
+ rule: params.rule,
337
+ severity: params.severity,
338
+ nodeId: node.id,
339
+ componentId: componentId || null,
340
+ argumentId: params.argumentId || null,
341
+ message: params.message,
342
+ });
343
+ };
344
+
345
+ // -- identity ---------------------------------------------------------
346
+
347
+ if (componentId && (componentIdCounts[componentId] || 0) > 1) {
348
+ addNodeIssue({
349
+ rule: WorkflowLintRule.DuplicateComponentId,
350
+ severity: WorkflowLintSeverity.Error,
351
+ message: `More than one step has the id "${componentId}". Ids must be unique — otherwise the steps overwrite each other's results and any reference to "${componentId}" is ambiguous.`,
352
+ });
353
+ }
354
+
355
+ if (componentId && componentId.includes(".")) {
356
+ addNodeIssue({
357
+ rule: WorkflowLintRule.ComponentIdContainsDot,
358
+ severity: WorkflowLintSeverity.Error,
359
+ message: `The id "${componentId}" contains a dot. References are split on dots, so no other step can read this one's results. Use dashes instead.`,
360
+ });
361
+ }
362
+
363
+ // -- reachability ------------------------------------------------------
364
+
365
+ if (
366
+ data.componentType !== ComponentType.Trigger &&
367
+ triggerNodes.length > 0 &&
368
+ !reachableFromTrigger.has(node.id)
369
+ ) {
370
+ addNodeIssue({
371
+ rule: WorkflowLintRule.UnreachableComponent,
372
+ severity: WorkflowLintSeverity.Warning,
373
+ message:
374
+ "Nothing connects to this step from the trigger, so it will never run.",
375
+ });
376
+ }
377
+
378
+ // -- arguments ---------------------------------------------------------
379
+
380
+ const argumentMetadata: Array<Argument> = data.metadata.arguments || [];
381
+
382
+ for (const argument of argumentMetadata) {
383
+ const value: JSONValue | undefined = argumentValues[argument.id];
384
+
385
+ if (argument.required && isEmptyArgumentValue(value)) {
386
+ addNodeIssue({
387
+ rule: WorkflowLintRule.MissingRequiredArgument,
388
+ severity: WorkflowLintSeverity.Error,
389
+ argumentId: argument.id,
390
+ message: `"${argument.name}" is required but empty.`,
391
+ });
392
+ continue;
393
+ }
394
+
395
+ if (isEmptyArgumentValue(value)) {
396
+ continue;
397
+ }
398
+
399
+ if (JSON_DOCUMENT_INPUT_TYPES.includes(argument.type)) {
400
+ const jsonCheck: {
401
+ isValid: boolean;
402
+ errorMessage: string | null;
403
+ } = checkJSONSyntax(value, {
404
+ allowJSON5: isJSON5ToleratedInputType(argument.type),
405
+ });
406
+
407
+ if (!jsonCheck.isValid) {
408
+ addNodeIssue({
409
+ rule: WorkflowLintRule.InvalidJSON,
410
+ severity: WorkflowLintSeverity.Error,
411
+ argumentId: argument.id,
412
+ message: `"${argument.name}" is not valid JSON. ${jsonCheck.errorMessage}`,
413
+ });
414
+ }
415
+ }
416
+
417
+ // -- references ------------------------------------------------------
418
+
419
+ for (const text of collectArgumentStrings(value as JSONValue)) {
420
+ const expressions: Array<TemplateExpression> =
421
+ parseTemplateExpressions(text);
422
+
423
+ for (const expression of expressions) {
424
+ if (expression.kind !== TemplateExpressionKind.Reference) {
425
+ continue;
426
+ }
427
+
428
+ /*
429
+ * Inside a loop, VMAPI resolves against the current array element
430
+ * before the storage map, so a bare {{status}} is correct there and
431
+ * cannot be checked from here.
432
+ */
433
+ if (expression.isInsideEachBlock) {
434
+ continue;
435
+ }
436
+
437
+ /*
438
+ * At the top level the runtime looks the path up exactly as written
439
+ * (VMAPI.replaceValueInPlace passes the raw capture to deepFind), so
440
+ * a stray space makes the reference resolve to nothing and the braces
441
+ * are shipped as literal text. Nothing about the rendered workflow
442
+ * hints at it, which makes this worth its own message rather than
443
+ * folding it into "unknown reference".
444
+ */
445
+ /*
446
+ * Every argument is templated, including the JavaScript component's
447
+ * source, and JavaScript can legitimately produce a {{...}} match on
448
+ * one line. Only complain about captures that were plausibly meant
449
+ * to be references.
450
+ */
451
+ if (!looksLikeReferencePath(expression.innerRaw)) {
452
+ continue;
453
+ }
454
+
455
+ if (expression.innerRaw !== expression.inner) {
456
+ addNodeIssue({
457
+ rule: WorkflowLintRule.ReferenceHasWhitespace,
458
+ severity: WorkflowLintSeverity.Error,
459
+ argumentId: argument.id,
460
+ message: `"${argument.name}" contains ${expression.raw}, which has a space inside the braces. Spaces are not trimmed here, so this resolves to nothing — write it as {{${expression.inner}}}.`,
461
+ });
462
+ continue;
463
+ }
464
+
465
+ const parsed: ParsedReferencePath = parseReferencePath(
466
+ expression.inner,
467
+ );
468
+
469
+ if (parsed.rootType === ReferenceRootType.Unknown) {
470
+ addNodeIssue({
471
+ rule: WorkflowLintRule.UnknownReferenceRoot,
472
+ severity: WorkflowLintSeverity.Error,
473
+ argumentId: argument.id,
474
+ message: `"${argument.name}" refers to ${expression.raw}, but ${parsed.reason}. Nothing will be substituted and the text will be sent as-is.`,
475
+ });
476
+ continue;
477
+ }
478
+
479
+ if (parsed.rootType !== ReferenceRootType.ComponentReturnValue) {
480
+ // A variable reference — well-formed. Existence needs the API.
481
+ continue;
482
+ }
483
+
484
+ const referencedComponentId: string = parsed.componentId as string;
485
+ const referencedNode: LintGraphNode | undefined =
486
+ nodesByComponentId[referencedComponentId];
487
+
488
+ if (!referencedNode) {
489
+ addNodeIssue({
490
+ rule: WorkflowLintRule.UnknownReferencedComponent,
491
+ severity: WorkflowLintSeverity.Error,
492
+ argumentId: argument.id,
493
+ message: `"${argument.name}" refers to a step called "${referencedComponentId}", but no step in this workflow has that id.`,
494
+ });
495
+ continue;
496
+ }
497
+
498
+ const returnValueId: string = parsed.returnValueId as string;
499
+ const returnValues: Array<ReturnValue> =
500
+ referencedNode.data.metadata.returnValues || [];
501
+
502
+ const hasReturnValue: boolean = returnValues.some(
503
+ (returnValue: ReturnValue) => {
504
+ return returnValue.id === returnValueId;
505
+ },
506
+ );
507
+
508
+ if (!hasReturnValue) {
509
+ addNodeIssue({
510
+ rule: WorkflowLintRule.UnknownReferencedReturnValue,
511
+ severity: WorkflowLintSeverity.Error,
512
+ argumentId: argument.id,
513
+ message: `"${argument.name}" reads "${returnValueId}" from step "${referencedComponentId}", but that step does not return anything by that name.`,
514
+ });
515
+ continue;
516
+ }
517
+
518
+ if (referencedNode.id === node.id) {
519
+ addNodeIssue({
520
+ rule: WorkflowLintRule.SelfReference,
521
+ severity: WorkflowLintSeverity.Error,
522
+ argumentId: argument.id,
523
+ message: `"${argument.name}" refers to this step's own results, which do not exist yet while its settings are being read.`,
524
+ });
525
+ continue;
526
+ }
527
+
528
+ /*
529
+ * A step that is downstream of this one has definitively not run
530
+ * when this step's arguments are resolved, so the reference is
531
+ * always empty. (The converse is not true — the runner's queue is
532
+ * FIFO across branches, so a step on a parallel branch may well have
533
+ * run. Only the unambiguous direction is reported.)
534
+ */
535
+ if (getDescendants(node.id).has(referencedNode.id)) {
536
+ addNodeIssue({
537
+ rule: WorkflowLintRule.ForwardReference,
538
+ severity: WorkflowLintSeverity.Error,
539
+ argumentId: argument.id,
540
+ message: `"${argument.name}" reads results from "${referencedComponentId}", but that step runs after this one, so it has no results yet.`,
541
+ });
542
+ continue;
543
+ }
544
+
545
+ if (
546
+ triggerNodes.length > 0 &&
547
+ !reachableFromTrigger.has(referencedNode.id)
548
+ ) {
549
+ addNodeIssue({
550
+ rule: WorkflowLintRule.ReferenceToUnreachableComponent,
551
+ severity: WorkflowLintSeverity.Warning,
552
+ argumentId: argument.id,
553
+ message: `"${argument.name}" reads results from "${referencedComponentId}", which is not connected to the trigger and so never runs.`,
554
+ });
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ /* ---- roll up ----------------------------------------------------------- */
562
+
563
+ const errorsByNodeId: Dictionary<string> = {};
564
+ let errorCount: number = 0;
565
+ let warningCount: number = 0;
566
+
567
+ for (const issue of issues) {
568
+ if (issue.severity === WorkflowLintSeverity.Error) {
569
+ errorCount++;
570
+ } else {
571
+ warningCount++;
572
+ }
573
+
574
+ if (!issue.nodeId) {
575
+ continue;
576
+ }
577
+
578
+ errorsByNodeId[issue.nodeId] = errorsByNodeId[issue.nodeId]
579
+ ? `${errorsByNodeId[issue.nodeId]}\n${issue.message}`
580
+ : issue.message;
581
+ }
582
+
583
+ return {
584
+ issues: issues,
585
+ errorsByNodeId: errorsByNodeId,
586
+ errorCount: errorCount,
587
+ warningCount: warningCount,
588
+ };
589
+ };
590
+
591
+ export default lintWorkflowGraph;