Skip to content

Commit a10a653

Browse files
committed
feat: manage schema input
Signed-off-by: Simon Emms <simon@simonemms.com>
1 parent f1f119d commit a10a653

11 files changed

Lines changed: 551 additions & 12 deletions

File tree

src/lib/export/yaml.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ function exportWorkflows(file: WorkflowFile, ctx: ExportContext): unknown[] {
133133
const wf = file.workflows[id];
134134
if (!wf) return {};
135135
const tasks = exportFlowGraphTasks(wf.root, ctx);
136-
return { [wf.name]: { do: tasks } };
136+
const wfDef: Record<string, unknown> = { do: tasks };
137+
if (wf.inputSchema !== undefined) {
138+
wfDef.schema = { format: 'json', document: wf.inputSchema };
139+
}
140+
return { [wf.name]: wfDef };
137141
});
138142
}
139143

src/lib/i18n/messages/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@
403403
"bodyCount": "body ({{count}} tasks)"
404404
}
405405
},
406+
"inputSelector": {
407+
"placeholder": "Select a field…",
408+
"switchToExpression": "Use expression",
409+
"switchToSelect": "Select field"
410+
},
406411
"validation": {
407412
"loop": {
408413
"collectionRequired": "Collection is required",

src/lib/tasks/model.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,27 @@ export type WorkflowFile = {
4747
order: string[]; // stable ordering of named workflows
4848
};
4949

50+
// ---------------------------------------------------------------------------
51+
// JSON Schema (subset) — used for workflow input schema definitions.
52+
// Only `type: object` with `properties` is supported; other JSON Schema
53+
// keywords are preserved for round-trip fidelity but not interpreted.
54+
// ---------------------------------------------------------------------------
55+
56+
export type JsonSchema = {
57+
type?: string;
58+
properties?: Record<string, JsonSchema>;
59+
required?: string[];
60+
title?: string;
61+
description?: string;
62+
};
63+
5064
export type NamedWorkflow = {
5165
id: string;
5266
name: string;
5367
root: FlowGraph;
68+
// Optional input schema — maps to `schema: { format: json, document: ... }`
69+
// in the Zigflow DSL YAML. Used by the Studio to offer field-picker UX.
70+
inputSchema?: JsonSchema;
5471
};
5572

5673
// ---------------------------------------------------------------------------

src/lib/tasks/parse.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import type {
4848
FlowGraph,
4949
ForkBranch,
5050
ForkNode,
51+
JsonSchema,
5152
LifetimePolicy,
5253
ListenConfig,
5354
ListenEvent,
@@ -138,7 +139,20 @@ export function parseWorkflowFile(
138139
}
139140

140141
// Old format: flat steps with hoisted sub-graph entries.
141-
return parseOldFormat(doEntries, document, ctx);
142+
const oldResult = parseOldFormat(doEntries, document, ctx);
143+
// Parse top-level schema for the single workflow in old format.
144+
const topLevelSchema = parseWorkflowSchema(raw);
145+
if (topLevelSchema !== undefined) {
146+
const wfId = oldResult.workflowFile.order[0]!;
147+
const wf = oldResult.workflowFile.workflows[wfId];
148+
if (wf) {
149+
oldResult.workflowFile.workflows[wfId] = {
150+
...wf,
151+
inputSchema: topLevelSchema,
152+
};
153+
}
154+
}
155+
return oldResult;
142156
}
143157

144158
// ---------------------------------------------------------------------------
@@ -167,6 +181,9 @@ function parseNewFormat(
167181
const root = parseGraph(steps, new Map(), new Set(), ctx);
168182
const id = crypto.randomUUID();
169183
const workflow: NamedWorkflow = { id, name, root };
184+
// Parse optional input schema: schema: { format: json, document: { ... } }
185+
const inputSchema = parseWorkflowSchema(def);
186+
if (inputSchema !== undefined) workflow.inputSchema = inputSchema;
170187
workflows[id] = workflow;
171188
order.push(id);
172189
}
@@ -798,6 +815,22 @@ function parseTryNode(
798815
return node;
799816
}
800817

818+
// ---------------------------------------------------------------------------
819+
// Input schema parsing
820+
//
821+
// Reads `schema: { format: json, document: { ... } }` from a workflow entry
822+
// (new format) or the top-level document (old format). Returns undefined if
823+
// the key is absent or malformed, preserving the existing workflow unchanged.
824+
// ---------------------------------------------------------------------------
825+
826+
function parseWorkflowSchema(def: RawEntry): JsonSchema | undefined {
827+
const schemaRaw = def['schema'];
828+
if (!schemaRaw || typeof schemaRaw !== 'object') return undefined;
829+
const doc = (schemaRaw as Record<string, unknown>)['document'];
830+
if (!doc || typeof doc !== 'object') return undefined;
831+
return doc as JsonSchema;
832+
}
833+
801834
// ---------------------------------------------------------------------------
802835
// LoopNode
803836
// ---------------------------------------------------------------------------

src/lib/tasks/schema-paths.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2025 - 2026 Zigflow authors <https://github.com/zigflow/studio/graphs/contributors>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// Zigflow Visual Editor — Input Schema Path Utilities
17+
//
18+
// Pure functions for deriving selectable dot-notation field paths from a
19+
// JsonSchema. Only `properties` traversal is supported — other JSON Schema
20+
// keywords are intentionally ignored to keep the Studio simple.
21+
import type { JsonSchema } from './model';
22+
23+
// ---------------------------------------------------------------------------
24+
// parseSchemaToPaths
25+
//
26+
// Traverses the `properties` tree of a JsonSchema and returns a flat list of
27+
// dot-notation paths for every leaf and intermediate node.
28+
//
29+
// Example:
30+
// schema = {
31+
// type: 'object',
32+
// properties: {
33+
// order: {
34+
// type: 'object',
35+
// properties: { pet: { type: 'object', properties: { id: {} } } }
36+
// }
37+
// }
38+
// }
39+
//
40+
// Returns: ['order', 'order.pet', 'order.pet.id']
41+
// ---------------------------------------------------------------------------
42+
43+
export function parseSchemaToPaths(schema: JsonSchema): string[] {
44+
const paths: string[] = [];
45+
46+
function traverse(s: JsonSchema, prefix: string): void {
47+
if (!s.properties) return;
48+
for (const [key, child] of Object.entries(s.properties)) {
49+
const path = prefix ? `${prefix}.${key}` : key;
50+
paths.push(path);
51+
traverse(child, path);
52+
}
53+
}
54+
55+
traverse(schema, '');
56+
return paths;
57+
}
58+
59+
// ---------------------------------------------------------------------------
60+
// toInputExpression / parseInputExpression
61+
//
62+
// Convert between a dot-notation field path and a Zigflow runtime expression.
63+
//
64+
// Example:
65+
// 'order.pet.id' → '${ $input.order.pet.id }'
66+
// '${ $input.order.pet.id }' → 'order.pet.id'
67+
// ---------------------------------------------------------------------------
68+
69+
const INPUT_EXPR_RE =
70+
/^\$\{\s*\$input\.([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\s*\}$/;
71+
72+
export function toInputExpression(path: string): string {
73+
return '${ $input.' + path + ' }';
74+
}
75+
76+
export function parseInputExpression(value: string): string | null {
77+
const m = INPUT_EXPR_RE.exec(value);
78+
return m ? (m[1] ?? null) : null;
79+
}

src/lib/ui/Inspector.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
} from '$lib/tasks/actions';
2727
import type {
2828
ForkNode,
29+
JsonSchema,
2930
NamedWorkflow,
3031
Node,
3132
SwitchNode,
@@ -56,6 +57,9 @@
5657
// Workflow list for switch branch target selection
5758
workflows?: NamedWorkflow[];
5859
currentWorkflowName?: string;
60+
// Input schema for the active workflow — passed to node editors that
61+
// support field-picker UX (e.g. the loop collection field).
62+
inputSchema?: JsonSchema | null;
5963
}
6064
6165
let {
@@ -72,6 +76,7 @@
7276
onremovebranch,
7377
workflows = [],
7478
currentWorkflowName = '',
79+
inputSchema = null,
7580
}: Props = $props();
7681
7782
// ---------------------------------------------------------------------------
@@ -474,7 +479,7 @@
474479

475480
<!-- Task-specific or structural property editor -->
476481
{#if NodeEditor}
477-
<NodeEditor {node} onupdate={(n) => onupdate?.(n)} />
482+
<NodeEditor {node} onupdate={(n) => onupdate?.(n)} {inputSchema} />
478483
{/if}
479484

480485
<!-- Common fields: if + metadata — present on all node types -->

0 commit comments

Comments
 (0)