@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.
- package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
- package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Server/API/UserAPI.ts +16 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
- package/Server/Services/TelemetryEntityService.ts +114 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
- package/Tests/Types/Workflow/Templates.test.ts +403 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +27 -0
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +7 -3
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +499 -0
- package/Types/Workflow/Templates.ts +316 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +23 -6
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Input/Input.tsx +31 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
- package/UI/Components/Workflow/GraphLint.ts +591 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
- package/UI/Components/Workflow/ModelSchema.ts +196 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/Workflow.tsx +77 -4
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
- package/Utils/Telemetry/EntityKey.ts +66 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +27 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +218 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +12 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Parsing helpers for the {{...}} template syntax that workflow arguments use.
|
|
3
|
+
*
|
|
4
|
+
* The runtime implementation lives in Common/Server/Utils/VM/VMAPI.ts
|
|
5
|
+
* (replaceValueInPlace, expandEachLoops, deepFind). Everything in this file
|
|
6
|
+
* mirrors that behaviour so the builder can warn about a broken reference
|
|
7
|
+
* before a run does — VMAPI deliberately skips a reference it cannot resolve
|
|
8
|
+
* ("Skip replacement if the variable is not found in the storageMap"), which
|
|
9
|
+
* means a typo like {{local.componets.api-get-1.returnValues.response-body}}
|
|
10
|
+
* is sent as literal text in the outgoing request and the run still reports
|
|
11
|
+
* Success. That silence is the bug this module exists to end.
|
|
12
|
+
*
|
|
13
|
+
* This lives in Common/Types (not Common/UI) because both the builder and the
|
|
14
|
+
* server-side runner need it, and Common/Types has no UI or server deps.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import JSON5 from "json5";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The exact expression matcher the runtime uses (VMAPI.ts: `/{{(.*?)}}/g`).
|
|
21
|
+
* Non-greedy, so `{{a}} and {{b}}` yields two matches rather than one.
|
|
22
|
+
* Built fresh on each use because a global RegExp carries lastIndex state.
|
|
23
|
+
*/
|
|
24
|
+
export type GetTemplateExpressionRegexFunction = () => RegExp;
|
|
25
|
+
|
|
26
|
+
export const getTemplateExpressionRegex: GetTemplateExpressionRegexFunction =
|
|
27
|
+
(): RegExp => {
|
|
28
|
+
return /{{(.*?)}}/g;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export enum TemplateExpressionKind {
|
|
32
|
+
/** A value reference, e.g. {{local.variables.apiKey}}. */
|
|
33
|
+
Reference = "Reference",
|
|
34
|
+
/** The opening tag of a loop, e.g. {{#each local.components.x.returnValues.items}}. */
|
|
35
|
+
EachOpen = "EachOpen",
|
|
36
|
+
/** The closing tag of a loop: {{/each}}. */
|
|
37
|
+
EachClose = "EachClose",
|
|
38
|
+
/** The loop index helper: {{@index}}. */
|
|
39
|
+
LoopIndex = "LoopIndex",
|
|
40
|
+
/** The current primitive element inside a loop: {{this}}. */
|
|
41
|
+
LoopThis = "LoopThis",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TemplateExpression {
|
|
45
|
+
/** The full matched text, including the braces. */
|
|
46
|
+
raw: string;
|
|
47
|
+
/** The text between the braces, trimmed. */
|
|
48
|
+
inner: string;
|
|
49
|
+
/**
|
|
50
|
+
* The text between the braces exactly as written. Worth keeping separate,
|
|
51
|
+
* because the runtime is inconsistent about trimming: at the top level
|
|
52
|
+
* VMAPI.replaceValueInPlace hands the raw capture to deepFind, so
|
|
53
|
+
* `{{ local.variables.x }}` resolves nothing and leaves the braces in the
|
|
54
|
+
* output, while inside an {{#each}} body replaceLoopVariables trims first and
|
|
55
|
+
* the same spacing is harmless.
|
|
56
|
+
*/
|
|
57
|
+
innerRaw: string;
|
|
58
|
+
kind: TemplateExpressionKind;
|
|
59
|
+
/** Index of the first `{` in the source string. */
|
|
60
|
+
startIndex: number;
|
|
61
|
+
/** Index one past the final `}` in the source string. */
|
|
62
|
+
endIndex: number;
|
|
63
|
+
/**
|
|
64
|
+
* True when this expression sits between an {{#each}} and its {{/each}}.
|
|
65
|
+
* Inside a loop, VMAPI resolves a reference against the current array
|
|
66
|
+
* element first and only then against the storage map
|
|
67
|
+
* (VMAPI.replaceLoopVariables), so a bare {{status}} is legitimate there and
|
|
68
|
+
* must never be reported as unknown.
|
|
69
|
+
*/
|
|
70
|
+
isInsideEachBlock: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ClassifyExpressionFunction = (inner: string) => TemplateExpressionKind;
|
|
74
|
+
|
|
75
|
+
const classifyExpression: ClassifyExpressionFunction = (
|
|
76
|
+
inner: string,
|
|
77
|
+
): TemplateExpressionKind => {
|
|
78
|
+
if (inner.startsWith("#each")) {
|
|
79
|
+
return TemplateExpressionKind.EachOpen;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (inner === "/each") {
|
|
83
|
+
return TemplateExpressionKind.EachClose;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (inner === "@index") {
|
|
87
|
+
return TemplateExpressionKind.LoopIndex;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (inner === "this") {
|
|
91
|
+
return TemplateExpressionKind.LoopThis;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return TemplateExpressionKind.Reference;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ParseTemplateExpressionsFunction = (
|
|
98
|
+
value: string,
|
|
99
|
+
) => Array<TemplateExpression>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Every {{...}} expression in `value`, in source order, each tagged with what
|
|
103
|
+
* it is and whether it sits inside a loop body.
|
|
104
|
+
*/
|
|
105
|
+
export const parseTemplateExpressions: ParseTemplateExpressionsFunction = (
|
|
106
|
+
value: string,
|
|
107
|
+
): Array<TemplateExpression> => {
|
|
108
|
+
if (!value || typeof value !== "string") {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const expressions: Array<TemplateExpression> = [];
|
|
113
|
+
const regex: RegExp = getTemplateExpressionRegex();
|
|
114
|
+
let match: RegExpExecArray | null = null;
|
|
115
|
+
let eachDepth: number = 0;
|
|
116
|
+
|
|
117
|
+
while ((match = regex.exec(value)) !== null) {
|
|
118
|
+
const raw: string = match[0];
|
|
119
|
+
const innerRaw: string = match[1] || "";
|
|
120
|
+
const inner: string = innerRaw.trim();
|
|
121
|
+
const kind: TemplateExpressionKind = classifyExpression(inner);
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* A closing tag is not itself "inside" the block it closes, and an opening
|
|
125
|
+
* tag is not inside the block it opens — but the array path on an opening
|
|
126
|
+
* tag IS resolved against the enclosing scope, so nested opens still count
|
|
127
|
+
* as inside their parent.
|
|
128
|
+
*/
|
|
129
|
+
if (kind === TemplateExpressionKind.EachClose) {
|
|
130
|
+
eachDepth = Math.max(0, eachDepth - 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
expressions.push({
|
|
134
|
+
raw: raw,
|
|
135
|
+
inner: inner,
|
|
136
|
+
innerRaw: innerRaw,
|
|
137
|
+
kind: kind,
|
|
138
|
+
startIndex: match.index,
|
|
139
|
+
endIndex: match.index + raw.length,
|
|
140
|
+
isInsideEachBlock: eachDepth > 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (kind === TemplateExpressionKind.EachOpen) {
|
|
144
|
+
eachDepth++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return expressions;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type HasEachBlockFunction = (value: string) => boolean;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* True when the value contains a loop. A loop expands to a variable number of
|
|
155
|
+
* repetitions of its body, so the shape of the final string is not knowable
|
|
156
|
+
* statically — callers that check structure (e.g. JSON validity) must bail out.
|
|
157
|
+
*/
|
|
158
|
+
export const hasEachBlock: HasEachBlockFunction = (value: string): boolean => {
|
|
159
|
+
if (!value || typeof value !== "string") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return value.includes("{{#each") || value.includes("{{/each}}");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* Substituted into a JSON document in place of every {{...}} expression before
|
|
168
|
+
* the document is parsed. `1` is chosen because it is simultaneously a legal
|
|
169
|
+
* JSON value (`{"n": {{x}}}` -> `{"n": 1}`), legal inside a string literal
|
|
170
|
+
* (`{"s": "{{x}}"}` -> `{"s": "1"}`), and legal as an object key
|
|
171
|
+
* (`{"{{x}}": 2}` -> `{"1": 2}`). Any of those three is a real thing people
|
|
172
|
+
* write, and all three have to survive masking or the check reports a
|
|
173
|
+
* syntax error where none exists.
|
|
174
|
+
*/
|
|
175
|
+
const TEMPLATE_MASK_TOKEN: string = "1";
|
|
176
|
+
|
|
177
|
+
export type MaskTemplateExpressionsFunction = (value: string) => string;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Replace every {{...}} expression with a neutral literal, so what remains can
|
|
181
|
+
* be checked for JSON syntax without the (unknown, run-time) substituted
|
|
182
|
+
* values.
|
|
183
|
+
*/
|
|
184
|
+
export const maskTemplateExpressions: MaskTemplateExpressionsFunction = (
|
|
185
|
+
value: string,
|
|
186
|
+
): string => {
|
|
187
|
+
if (!value || typeof value !== "string") {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return value.replace(getTemplateExpressionRegex(), TEMPLATE_MASK_TOKEN);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export interface JSONSyntaxCheckResult {
|
|
195
|
+
/**
|
|
196
|
+
* False only when the value is definitely malformed. A value this module
|
|
197
|
+
* cannot decide about (a loop, a non-string, an empty box) is reported
|
|
198
|
+
* valid — the builder must never block on a guess.
|
|
199
|
+
*/
|
|
200
|
+
isValid: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* The JSON parser's own message ("Unexpected token } in JSON at position
|
|
203
|
+
* 24"), with no framing around it — callers phrase the sentence, since a
|
|
204
|
+
* form field and a canvas badge word it differently. Null when valid.
|
|
205
|
+
*/
|
|
206
|
+
errorMessage: string | null;
|
|
207
|
+
/**
|
|
208
|
+
* True when the check was skipped rather than passed. Callers that want to
|
|
209
|
+
* explain themselves ("not checked: contains a loop") can use this.
|
|
210
|
+
*/
|
|
211
|
+
wasSkipped: boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface JSONSyntaxCheckOptions {
|
|
215
|
+
/**
|
|
216
|
+
* Judge the value by JSON5's rules instead of JSON's — unquoted keys, single
|
|
217
|
+
* quotes and trailing commas allowed. Set this wherever the code that will
|
|
218
|
+
* actually read the value uses JSONFunctions.parse, so the check cannot be
|
|
219
|
+
* stricter than the thing it is predicting.
|
|
220
|
+
*/
|
|
221
|
+
allowJSON5?: boolean | undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type CheckJSONSyntaxFunction = (
|
|
225
|
+
value: unknown,
|
|
226
|
+
options?: JSONSyntaxCheckOptions | undefined,
|
|
227
|
+
) => JSONSyntaxCheckResult;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Is `value` a JSON document, once its {{...}} placeholders are accounted for?
|
|
231
|
+
*
|
|
232
|
+
* Deliberately permissive. It returns invalid only for a string that cannot
|
|
233
|
+
* become JSON no matter what the placeholders resolve to, because a false
|
|
234
|
+
* positive here disables the Save button on a workflow that actually works.
|
|
235
|
+
*/
|
|
236
|
+
export const checkJSONSyntax: CheckJSONSyntaxFunction = (
|
|
237
|
+
value: unknown,
|
|
238
|
+
options?: JSONSyntaxCheckOptions | undefined,
|
|
239
|
+
): JSONSyntaxCheckResult => {
|
|
240
|
+
const skipped: JSONSyntaxCheckResult = {
|
|
241
|
+
isValid: true,
|
|
242
|
+
errorMessage: null,
|
|
243
|
+
wasSkipped: true,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* Not a string: the field already holds a parsed object (the Dictionary
|
|
248
|
+
* editor and imported graphs both produce these). Nothing to parse.
|
|
249
|
+
*/
|
|
250
|
+
if (typeof value !== "string") {
|
|
251
|
+
return skipped;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Empty is the job of the required-field check, not this one.
|
|
255
|
+
if (value.trim() === "") {
|
|
256
|
+
return skipped;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
* A loop emits its body once per array element, so the document's structure
|
|
261
|
+
* depends on data that only exists at run time. Checking the unexpanded text
|
|
262
|
+
* would flag every correct loop ever written.
|
|
263
|
+
*/
|
|
264
|
+
if (hasEachBlock(value)) {
|
|
265
|
+
return skipped;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const masked: string = maskTemplateExpressions(value);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
if (options?.allowJSON5) {
|
|
272
|
+
JSON5.parse(masked);
|
|
273
|
+
} else {
|
|
274
|
+
JSON.parse(masked);
|
|
275
|
+
}
|
|
276
|
+
return { isValid: true, errorMessage: null, wasSkipped: false };
|
|
277
|
+
} catch (err: unknown) {
|
|
278
|
+
const parserMessage: string =
|
|
279
|
+
err instanceof Error ? err.message : "Invalid JSON.";
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
isValid: false,
|
|
283
|
+
errorMessage: parserMessage,
|
|
284
|
+
wasSkipped: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/* ------------------------------------------------------------------------ */
|
|
290
|
+
/* Reference paths */
|
|
291
|
+
/* ------------------------------------------------------------------------ */
|
|
292
|
+
|
|
293
|
+
export enum ReferenceRootType {
|
|
294
|
+
LocalVariable = "LocalVariable",
|
|
295
|
+
GlobalVariable = "GlobalVariable",
|
|
296
|
+
ComponentReturnValue = "ComponentReturnValue",
|
|
297
|
+
/** Well-formed-looking but not a root the runtime storage map has. */
|
|
298
|
+
Unknown = "Unknown",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface ParsedReferencePath {
|
|
302
|
+
rootType: ReferenceRootType;
|
|
303
|
+
/** Populated for LocalVariable / GlobalVariable. */
|
|
304
|
+
variableName?: string | undefined;
|
|
305
|
+
/** Populated for ComponentReturnValue: the component's user-facing id. */
|
|
306
|
+
componentId?: string | undefined;
|
|
307
|
+
/** Populated for ComponentReturnValue: the return value's id. */
|
|
308
|
+
returnValueId?: string | undefined;
|
|
309
|
+
/**
|
|
310
|
+
* Why this path could not be understood. Only set when rootType is Unknown,
|
|
311
|
+
* and phrased for a builder to read.
|
|
312
|
+
*/
|
|
313
|
+
reason?: string | undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
type StripArrayAccessorFunction = (segment: string) => string;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* `items[0]` -> `items`. VMUtil.deepFind splits a path on "." and then looks
|
|
320
|
+
* for a `[index]` suffix on each segment, so the name to match against
|
|
321
|
+
* metadata is whatever precedes the bracket.
|
|
322
|
+
*/
|
|
323
|
+
const stripArrayAccessor: StripArrayAccessorFunction = (
|
|
324
|
+
segment: string,
|
|
325
|
+
): string => {
|
|
326
|
+
const bracketIndex: number = segment.indexOf("[");
|
|
327
|
+
|
|
328
|
+
if (bracketIndex === -1) {
|
|
329
|
+
return segment;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return segment.slice(0, bracketIndex);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/*
|
|
336
|
+
* Characters that appear in code but never in a reference path. A workflow's
|
|
337
|
+
* JavaScript component is templated like every other argument
|
|
338
|
+
* (RunWorkflow.getComponentArguments runs on all of them), so its source is
|
|
339
|
+
* scanned by the same regex — and JavaScript really can contain `{{` and `}}`
|
|
340
|
+
* on one line, e.g. `if (x) {{ return 1; }}`. Anything carrying these is code
|
|
341
|
+
* that happened to match, not a reference somebody typo'd, and reporting it
|
|
342
|
+
* would put a red badge on a component that works.
|
|
343
|
+
*/
|
|
344
|
+
const NON_REFERENCE_CHARACTERS: RegExp = /[{}()[\];=+*/&|!<>"'`,]/;
|
|
345
|
+
|
|
346
|
+
export type LooksLikeReferencePathFunction = (capture: string) => boolean;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Whether a template capture was plausibly *meant* to be a value reference.
|
|
350
|
+
* Used to decide whether an unrecognized path is worth complaining about.
|
|
351
|
+
*
|
|
352
|
+
* Array accessors are stripped first, since `items[0]` is a legitimate path
|
|
353
|
+
* segment even though it contains brackets.
|
|
354
|
+
*/
|
|
355
|
+
export const looksLikeReferencePath: LooksLikeReferencePathFunction = (
|
|
356
|
+
capture: string,
|
|
357
|
+
): boolean => {
|
|
358
|
+
const trimmed: string = capture.trim();
|
|
359
|
+
|
|
360
|
+
if (trimmed === "") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const withoutAccessors: string = trimmed.replace(/\[(\d+|last)\]/g, "");
|
|
365
|
+
|
|
366
|
+
return !NON_REFERENCE_CHARACTERS.test(withoutAccessors);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export type ParseReferencePathFunction = (path: string) => ParsedReferencePath;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Break a reference path into the parts the builder can check against the
|
|
373
|
+
* graph. Mirrors the StorageMap shape built in RunWorkflow.getVariables:
|
|
374
|
+
*
|
|
375
|
+
* local.variables.<name>
|
|
376
|
+
* local.components.<componentId>.returnValues.<returnValueId>[...deeper]
|
|
377
|
+
* global.variables.<name>
|
|
378
|
+
*
|
|
379
|
+
* Anything past the return value id is a drill-in to whatever that value
|
|
380
|
+
* happens to contain at run time, so it is not inspected here.
|
|
381
|
+
*/
|
|
382
|
+
export const parseReferencePath: ParseReferencePathFunction = (
|
|
383
|
+
path: string,
|
|
384
|
+
): ParsedReferencePath => {
|
|
385
|
+
if (path.trim() === "") {
|
|
386
|
+
return {
|
|
387
|
+
rootType: ReferenceRootType.Unknown,
|
|
388
|
+
reason: "the reference is empty",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const rawSegments: Array<string> = path.split(".").map((segment: string) => {
|
|
393
|
+
return segment.trim();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/*
|
|
397
|
+
* deepFind splits on "." and bails on the first empty key, so a doubled dot
|
|
398
|
+
* silently resolves to nothing. Catch it here rather than filtering the
|
|
399
|
+
* empties out, which would make "local..variables.x" look perfectly fine.
|
|
400
|
+
*/
|
|
401
|
+
if (
|
|
402
|
+
rawSegments.some((segment: string) => {
|
|
403
|
+
return segment.length === 0;
|
|
404
|
+
})
|
|
405
|
+
) {
|
|
406
|
+
return {
|
|
407
|
+
rootType: ReferenceRootType.Unknown,
|
|
408
|
+
reason: "it has an empty part between two dots",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const segments: Array<string> = rawSegments;
|
|
413
|
+
|
|
414
|
+
const root: string = stripArrayAccessor(segments[0] as string);
|
|
415
|
+
|
|
416
|
+
if (root === "global") {
|
|
417
|
+
if (stripArrayAccessor(segments[1] || "") !== "variables") {
|
|
418
|
+
return {
|
|
419
|
+
rootType: ReferenceRootType.Unknown,
|
|
420
|
+
reason: '"global" must be followed by "variables"',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!segments[2]) {
|
|
425
|
+
return {
|
|
426
|
+
rootType: ReferenceRootType.Unknown,
|
|
427
|
+
reason: "no global variable name was given",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
rootType: ReferenceRootType.GlobalVariable,
|
|
433
|
+
variableName: stripArrayAccessor(segments[2]),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (root !== "local") {
|
|
438
|
+
return {
|
|
439
|
+
rootType: ReferenceRootType.Unknown,
|
|
440
|
+
reason: `"${root}" is not a thing a reference can start with — use "local" or "global"`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const second: string = stripArrayAccessor(segments[1] || "");
|
|
445
|
+
|
|
446
|
+
if (second === "variables") {
|
|
447
|
+
if (!segments[2]) {
|
|
448
|
+
return {
|
|
449
|
+
rootType: ReferenceRootType.Unknown,
|
|
450
|
+
reason: "no variable name was given",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
rootType: ReferenceRootType.LocalVariable,
|
|
456
|
+
variableName: stripArrayAccessor(segments[2]),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (second !== "components") {
|
|
461
|
+
return {
|
|
462
|
+
rootType: ReferenceRootType.Unknown,
|
|
463
|
+
reason: `"local.${second}" is not valid — use "local.variables" or "local.components"`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const componentId: string = stripArrayAccessor(segments[2] || "");
|
|
468
|
+
|
|
469
|
+
if (!componentId) {
|
|
470
|
+
return {
|
|
471
|
+
rootType: ReferenceRootType.Unknown,
|
|
472
|
+
reason: "no component id was given",
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const third: string = stripArrayAccessor(segments[3] || "");
|
|
477
|
+
|
|
478
|
+
if (third !== "returnValues") {
|
|
479
|
+
return {
|
|
480
|
+
rootType: ReferenceRootType.Unknown,
|
|
481
|
+
reason: `"local.components.${componentId}" must be followed by "returnValues"`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const returnValueId: string = stripArrayAccessor(segments[4] || "");
|
|
486
|
+
|
|
487
|
+
if (!returnValueId) {
|
|
488
|
+
return {
|
|
489
|
+
rootType: ReferenceRootType.Unknown,
|
|
490
|
+
reason: `no return value was named on "${componentId}"`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
rootType: ReferenceRootType.ComponentReturnValue,
|
|
496
|
+
componentId: componentId,
|
|
497
|
+
returnValueId: returnValueId,
|
|
498
|
+
};
|
|
499
|
+
};
|