feat(app-shell): the connector Input section derives typed fields from the descriptor's inputSchema (#4305) - #4572
Merged
Conversation
…m the descriptor's inputSchema (#4305) A committed connector action published its input contract and the designer ignored it: `GET /api/v1/automation/connectors` serves each action's `inputSchema` (the connector's own JSON Schema, projected verbatim by the engine) and nothing in app-shell read it, so the Input section stayed a generic untyped key/value repeater. The mapping reuses `json-schema-to-fields` — the resolver the inspector already uses for a node type's engine-published `configSchema`, which speaks exactly this language. A small adapter only RE-ROOTS its output: the resolver hard-roots every field at `config.<key>`, while a connector's inputs live in the spec-structured `connectorConfig.input` block the executor reads. No second JSON Schema interpreter is introduced, and a property the resolver declines stays repeater-editable so no descriptor can make a key unreachable. Round-trip is the binding constraint: typed fields read and write the SAME stored map, key by key, so editing one input leaves every other key at its stored value and position. `additionalProperties` is followed as measured — absent in every shipped connector, and JSON Schema's default for absent is open, which the executor confirms by passing the map to the handler unvalidated. Open keeps the repeater beside the typed fields for the extras only, merging its commit back instead of replacing the map; closed drops it, unless stored undeclared keys would otherwise be hidden. Descriptors with no inputSchema, an unreachable registry, no action committed yet, and an array-shaped input all keep the repeater unchanged. Labels come from the descriptor's own title/description, so no UI copy was added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash) — landing verified per the merge-queue discipline. Generated by Claude Code Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4305
A committed connector action published its input contract and the designer ignored it.
GET /api/v1/automation/connectorsserves each action'sinputSchema, and nothing inapp-shellread it, so the Input section stayed a generic untyped key/value repeater. The connector and action pickers were correct throughout — this was the last untyped step of that flow.Both ends measured first (ruling step 1)
(a) The descriptor's schema language.
ConnectorActionDescriptor.inputSchemais typedRecord< string, unknown >and documented as JSON Schema (@objectstack/specintegration/connector-descriptor.ts:53); the engine projects it verbatim from the connector's authoredConnectorActionSchema.inputSchema(service-automation/src/engine.ts:2122,inputSchema: a.inputSchema). Every shipped connector emits a plain object schema:{type:'object', required:['channel'], properties:{…}}— string / array{type:'object', properties:{ method, path, headers:{type:'object'}, query, body:{} }}{type:'object', properties:{ path/query/header:{type:'object',properties}, body }, required}inputSchema, passed straight throughrequiredis the JSON-Schema array at object level.flow.nodes[].inputSchema(specautomation/flow.zod.ts:335) isRecord< string, {type, required, description} >, a per-keyrequired: truemap. Only the descriptor one is read here.(b) Today's Input section.
flow-node-config.ts:815—at('connectorConfig', 'input', 'Input', 'keyValue', …), i.e. onekeyValuefield at path['connectorConfig','input'], rendered byFlowKeyValueField. Storage shape is the spec'sinput: z.record(z.string(), z.unknown()).mergeServerFlowFieldsalways preserves that sibling-block field, so it survives every engine-publishedconfigSchema(framework#4210).One resolver, no new interpreter (ruling step 2)
json-schema-to-fields— the resolver the inspector already uses for a node type's publishedconfigSchema— speaks exactly this language, so the descriptor's schema goes through it unchanged. The new adapter only re-roots what comes back: that resolver hard-roots every field under theconfigroot (one path segment per schema property), while a connector's inputs live underconnectorConfig.input, which is what the executor reads. It rewrites the path, prefixes the id, and followsshowWhencontroller references onto their re-rooted ids.A property the resolver declines is not claimed either — an
arraywith noitems(Slack'sblocks), a bare{type:'object'}(REST'sheaders), a union — so it stays repeater-editable and no descriptor can make a stored key unreachable.Round-trip and additionalProperties (ruling step 3)
Typed fields read and write the same
connectorConfig.inputmap an existing flow already committed, key by key — editing one input leaves every other key, declared or not, at its stored value and position.additionalPropertieswas measured, not assumed: no shipped connector emits the key at all, JSON Schema's default for an absent one is open, and the executor confirms it —connector-nodes.ts:107callshandler((cfg.input ?? {}) as Record< string, unknown >, handlerCtx), passing the whole map through unvalidated. So undeclared keys really are accepted, and the rule followed is the JSON-Schema rule: closed iffadditionalProperties === false, open otherwise.omitKeysto exactly the keys the typed fields do not own. Its commit merges back over the stored map rather than replacing it (the widget's own commit replaces whatever it is handed, so without this one extras edit would wipe every typed input).Red-first (ruling step 4)
Predicted split written before running; 7 red / 7 green on unfixed code, matching. Two assertions initially passed and were strengthened rather than kept: a bare
getByDisplayValue('C123')probe was satisfied by the untyped repeater's own cell, so it could never go red — it now goes through the field label instead.Reverse verification (fix removed via
git checkout+ module moved aside, nevergit stash; all five files restored and sha256-verified): 8 red / 6 green. The one-test delta from the prediction is honest and explained — the extras-edit test became fix-dependent after I added its settling wait.Pins green on both sides: no-schema descriptor keeps the byte-identical repeater, an unreachable registry keeps it, a node with no action committed keeps it, an array-shaped input is left wholly alone, and the picker behaviours (registry diff, undispatchable refusal, provenance annotation) are untouched with their existing suites green.
i18n and versioning (ruling step 5)
Labels and help come from the descriptor's own
title/description— the connector's own channel,meta()in the shared resolver — so no UI copy keys were added. The extras repeater keeps its existing localized "Input" label, which is why the form is applied after localization: typed fields must not be overlaid from the client's zh table..d.tsdiff measured both ways from cleandist/+tsconfig.tsbuildinfo: one new internal module, one added optional property (omitKeys?: string[]), nothing removed or narrowed. Neither reaches the package entry (dist/index.d.tsexports neitherFlowConfigFieldnor the new module), so this is internal surface → patch (#4496 precedent).Verification
vitestflow-builder + inspectors + previews + flow-envelope ratchet: 87 files, 1009 passed, 1 skippedpnpm --filter @object-ui/app-shell type-check(both tsc passes:tsc --noEmitandtsc -p tsconfig.test.json) greensetStatein an effect, and a React Compiler "existing memoization could not be preserved" caused bynode?.typeinline inferring a dependency on all ofnodecheck:phantom-deps,check:control-bytesgreen;grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over every created/edited file cleanNot built, reported instead
JSON Schema
requiredis not represented.FlowConfigFieldhas no requiredness concept, so the typed fields cannot mark it; the engine reads it at dispatch. Building that would have meant extending the shared field vocabulary, which is beyond this card.Scope respected: only the flow-builder connector-node files, one new adapter module, new tests, and the changeset. The inspector/host files from #4536 / #4547 / #4558 were checked and are untouched by this change.
Generated by Claude Code