From 4e44b1a20f776a58af7b2453b46fb748fd5e07e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 18:26:37 +0000 Subject: [PATCH] feat(automation): publish a configSchema for the map node (designer parity, #3304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi --- .changeset/map-node-config-schema.md | 22 +++++++++++++++++ .../src/builtin/map-node.test.ts | 24 +++++++++++++++++++ .../src/builtin/map-node.ts | 19 +++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 .changeset/map-node-config-schema.md diff --git a/.changeset/map-node-config-schema.md b/.changeset/map-node-config-schema.md new file mode 100644 index 0000000000..0cea361c3b --- /dev/null +++ b/.changeset/map-node-config-schema.md @@ -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. diff --git a/packages/services/service-automation/src/builtin/map-node.test.ts b/packages/services/service-automation/src/builtin/map-node.test.ts index 8a245faa4e..c236acb082 100644 --- a/packages/services/service-automation/src/builtin/map-node.test.ts +++ b/packages/services/service-automation/src/builtin/map-node.test.ts @@ -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; 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(); + }); +}); diff --git a/packages/services/service-automation/src/builtin/map-node.ts b/packages/services/service-automation/src/builtin/map-node.ts index 845dcb6c53..9f92e52555 100644 --- a/packages/services/service-automation/src/builtin/map-node.ts +++ b/packages/services/service-automation/src/builtin/map-node.ts @@ -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;