Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/map-node-config-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@objectstack/service-automation": patch
---

feat(automation): publish a configSchema for the `map` node (flow designer parity, #3304)

The `map` (sequential multi-instance) node shipped no `configSchema`, so the flow
designer fell back to its hardcoded field group online and to raw Advanced-JSON
where that wasn't present. Its descriptor now carries a structured `configSchema`
that mirrors the objectui hardcoded `map` field group field-for-field —
`collection` (marked `xExpression: 'template'`, an `interpolate()` `{items}`
template, same as `loop.collection`), `flowName` + `itemObject` as typed
references (`xRef`), and `iteratorVariable` / `outputVariable` as plain text — so
the online (schema-driven) and offline forms match.

`map` is the one previously-schemaless flow node whose fields are all scalars and
typed references, so it maps cleanly through objectui's `jsonSchemaToFlowFields`
with zero regression. The remaining schemaless nodes lean on editor kinds the
schema→fields adapter does not yet reproduce (`keyValue` maps, the decision
virtual `target` column, `wait`'s top-level block), and are deferred to #3304
until that adapter is extended. Additive and backward-compatible: no runtime
behavior change; an older designer that ignores the schema is unaffected.
24 changes: 24 additions & 0 deletions packages/services/service-automation/src/builtin/map-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,27 @@ describe('map node executor (sequential multi-instance)', () => {
expect(await store.list()).toHaveLength(0);
});
});

describe('map descriptor configSchema (objectui #2670 Phase 3 / #3304)', () => {
it('publishes a structured config form matching the objectui `map` field group', () => {
// `map` is the one previously-schemaless node whose fields all map cleanly
// through jsonSchemaToFlowFields (scalars + typed references, no keyValue /
// virtual columns), so shipping this schema makes the online designer form
// match the offline hardcoded one with zero regression.
const engine = new AutomationEngine(silentLogger());
registerMapNode(engine, ctx());
const schema = engine.getActionDescriptor('map')?.configSchema as
| { properties?: Record<string, { xExpression?: unknown; xRef?: { kind?: string } }>; required?: string[] }
| undefined;
expect(schema).toBeDefined();
// `collection` is an interpolate() `{items}` template (mono editor + `{var}`
// picker, no CEL brace-trap) — the same marker as loop.collection.
expect(schema?.properties?.collection?.xExpression).toBe('template');
// `flowName` / `itemObject` are typed references → pickers, not free text.
expect(schema?.properties?.flowName?.xRef?.kind).toBe('flow');
expect(schema?.properties?.itemObject?.xRef?.kind).toBe('object');
// Plain scalar text fields carry no authoring marker.
expect(schema?.properties?.iteratorVariable?.xExpression).toBeUndefined();
expect(schema?.properties?.outputVariable?.xExpression).toBeUndefined();
});
});
19 changes: 19 additions & 0 deletions packages/services/service-automation/src/builtin/map-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ export function registerMapNode(engine: AutomationEngine, ctx: PluginContext): v
// Each item's subflow may pause, so the map suspends and resumes per item.
supportsPause: true,
isAsync: true,
// Structured config form for the flow designer (ADR-0018). Mirrors the
// objectui hardcoded `map` field group field-for-field, so the online
// (schema-driven) form matches the offline one (objectui #2670 Phase 3 /
// #3304). `map` is the one previously-schemaless node whose fields are all
// scalars / typed references — no `keyValue` map, no virtual columns — so
// it maps cleanly through `jsonSchemaToFlowFields` with zero regression.
configSchema: {
type: 'object',
properties: {
// interpolate() single-brace `{items}` template, not bare CEL — same
// marker + rationale as loop.collection.
collection: { type: 'string', title: 'Collection', description: 'Expression resolving to the array to process, one item at a time.', xExpression: 'template' },
flowName: { type: 'string', title: 'Per-item flow', description: 'Subflow run for each item — it may pause (e.g. an approval).', xRef: { kind: 'flow' } },
iteratorVariable: { type: 'string', title: 'Item variable' },
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' } },
outputVariable: { type: 'string', title: 'Output variable', description: 'Each item’s subflow output, collected in order.' },
},
required: ['collection', 'flowName'],
},
}),
async execute(node, variables, context) {
const cfg = (node.config ?? {}) as Record<string, unknown>;
Expand Down
Loading