Skip to content

Commit f8c1b69

Browse files
os-zhuangclaude
andauthored
feat(automation): publish a configSchema for the map node (designer parity, #3304) (#3321)
Second increment of #3304 (descriptor-side counterpart to objectui #2670 Phase 3). The `map` node shipped no `configSchema`, so the flow designer had no server-driven form for it. Its descriptor now carries one that mirrors objectui's hardcoded `map` field group field-for-field: • collection → xExpression: 'template' (an interpolate() `{items}` template, same marker + rationale as loop.collection) • flowName → xRef { kind: 'flow' } (typed reference picker) • itemObject → xRef { kind: 'object' } • iteratorVariable / outputVariable → plain text `map` is the one previously-schemaless flow node whose fields are all scalars and typed references — no `keyValue` map, no virtual columns — so it maps cleanly through objectui's jsonSchemaToFlowFields with zero regression, making the online and offline forms agree. The other schemaless nodes (assignment, the CRUD quartet, script, subflow, screen, wait, decision) rely on editor kinds the schema→fields adapter does not yet reproduce — `keyValue` maps, the decision virtual `target` column, `wait`'s top-level `waitEventConfig` block. Shipping their configSchemas before the adapter learns those would make objectui prefer the server schema and DROP those editors (a live regression), so they are deferred to #3304 pending an objectui adapter extension. Verified: new descriptor test (map configSchema carries the collection template marker + flowName/itemObject references); tsc clean; full @objectstack/service- automation suite green (331 tests) — the added `required` breaks nothing. Additive; no runtime behavior change. Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3e6622b commit f8c1b69

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
feat(automation): publish a configSchema for the `map` node (flow designer parity, #3304)
6+
7+
The `map` (sequential multi-instance) node shipped no `configSchema`, so the flow
8+
designer fell back to its hardcoded field group online and to raw Advanced-JSON
9+
where that wasn't present. Its descriptor now carries a structured `configSchema`
10+
that mirrors the objectui hardcoded `map` field group field-for-field —
11+
`collection` (marked `xExpression: 'template'`, an `interpolate()` `{items}`
12+
template, same as `loop.collection`), `flowName` + `itemObject` as typed
13+
references (`xRef`), and `iteratorVariable` / `outputVariable` as plain text — so
14+
the online (schema-driven) and offline forms match.
15+
16+
`map` is the one previously-schemaless flow node whose fields are all scalars and
17+
typed references, so it maps cleanly through objectui's `jsonSchemaToFlowFields`
18+
with zero regression. The remaining schemaless nodes lean on editor kinds the
19+
schema→fields adapter does not yet reproduce (`keyValue` maps, the decision
20+
virtual `target` column, `wait`'s top-level block), and are deferred to #3304
21+
until that adapter is extended. Additive and backward-compatible: no runtime
22+
behavior change; an older designer that ignores the schema is unaffected.

packages/services/service-automation/src/builtin/map-node.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,27 @@ describe('map node executor (sequential multi-instance)', () => {
164164
expect(await store.list()).toHaveLength(0);
165165
});
166166
});
167+
168+
describe('map descriptor configSchema (objectui #2670 Phase 3 / #3304)', () => {
169+
it('publishes a structured config form matching the objectui `map` field group', () => {
170+
// `map` is the one previously-schemaless node whose fields all map cleanly
171+
// through jsonSchemaToFlowFields (scalars + typed references, no keyValue /
172+
// virtual columns), so shipping this schema makes the online designer form
173+
// match the offline hardcoded one with zero regression.
174+
const engine = new AutomationEngine(silentLogger());
175+
registerMapNode(engine, ctx());
176+
const schema = engine.getActionDescriptor('map')?.configSchema as
177+
| { properties?: Record<string, { xExpression?: unknown; xRef?: { kind?: string } }>; required?: string[] }
178+
| undefined;
179+
expect(schema).toBeDefined();
180+
// `collection` is an interpolate() `{items}` template (mono editor + `{var}`
181+
// picker, no CEL brace-trap) — the same marker as loop.collection.
182+
expect(schema?.properties?.collection?.xExpression).toBe('template');
183+
// `flowName` / `itemObject` are typed references → pickers, not free text.
184+
expect(schema?.properties?.flowName?.xRef?.kind).toBe('flow');
185+
expect(schema?.properties?.itemObject?.xRef?.kind).toBe('object');
186+
// Plain scalar text fields carry no authoring marker.
187+
expect(schema?.properties?.iteratorVariable?.xExpression).toBeUndefined();
188+
expect(schema?.properties?.outputVariable?.xExpression).toBeUndefined();
189+
});
190+
});

packages/services/service-automation/src/builtin/map-node.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ export function registerMapNode(engine: AutomationEngine, ctx: PluginContext): v
4949
// Each item's subflow may pause, so the map suspends and resumes per item.
5050
supportsPause: true,
5151
isAsync: true,
52+
// Structured config form for the flow designer (ADR-0018). Mirrors the
53+
// objectui hardcoded `map` field group field-for-field, so the online
54+
// (schema-driven) form matches the offline one (objectui #2670 Phase 3 /
55+
// #3304). `map` is the one previously-schemaless node whose fields are all
56+
// scalars / typed references — no `keyValue` map, no virtual columns — so
57+
// it maps cleanly through `jsonSchemaToFlowFields` with zero regression.
58+
configSchema: {
59+
type: 'object',
60+
properties: {
61+
// interpolate() single-brace `{items}` template, not bare CEL — same
62+
// marker + rationale as loop.collection.
63+
collection: { type: 'string', title: 'Collection', description: 'Expression resolving to the array to process, one item at a time.', xExpression: 'template' },
64+
flowName: { type: 'string', title: 'Per-item flow', description: 'Subflow run for each item — it may pause (e.g. an approval).', xRef: { kind: 'flow' } },
65+
iteratorVariable: { type: 'string', title: 'Item variable' },
66+
itemObject: { type: 'string', title: 'Item object', description: 'When items are records, the object they belong to (exposes each item as the child’s record).', xRef: { kind: 'object' } },
67+
outputVariable: { type: 'string', title: 'Output variable', description: 'Each item’s subflow output, collected in order.' },
68+
},
69+
required: ['collection', 'flowName'],
70+
},
5271
}),
5372
async execute(node, variables, context) {
5473
const cfg = (node.config ?? {}) as Record<string, unknown>;

0 commit comments

Comments
 (0)