feat(graphql): method-level alias support with three-state mutation feedback (fixes #140) - #141
Merged
Conversation
…apsing (US1, specs/023) Issue #140: aliased fields in a compose query were silently collapsed to the last one — 6 aliased add_node invocations executed just once with errors=[]. Now execute_compose_query detects any alias at AST level (including nested) before any service call and returns an ALIAS_CONFLICT error naming the alias path. Adds spec 023 (spec/plan/research/data-model/contracts/quickstart/tasks) and its requirements checklist. Co-Authored-By: Claude <noreply@anthropic.com>
…e gate (US2+US4, specs/023) sub_fields keys become RESPONSE keys (alias or name); lookups use FieldSelection.name. Same-name aliased invocations no longer overwrite each other, and duplicate response keys (alias repeats, alias/field collisions, plain duplicate fields — no spec field-merging) now raise ALIAS_CONFLICT instead of silently deduplicating. - compose: method resolution by sel.name, response keys carry aliases, query fans out with per-field failure isolation (QUERY_FAILED nulls only its own key; planning errors still fail fast) - entity-first: response keys + projection lookups honor aliases at group/method level; nested aliases rejected pre-execution (FR-009) - federation: wire renders original field names via _wire_render_name — aliases never leak to members (alias-gated: hand-built trees reuse 'name' for the target TYPE, so only aliased nodes substitute) - validate_no_aliases retired from the handler call path (kept as an optional user-side guard); CLI --select rejects aliases explicitly Full suite 1630 passed (baseline 1605 + 25 new, zero regressions). Co-Authored-By: Claude <noreply@anthropic.com>
…e-group nulling (US3, specs/023) Mutation groups run aliased invocations serially in declaration order (N aliases = N side effects, no execution-level dedup). On failure: prior results survive, the failing key nulls with MUTATION_FAILED (compose) / RESOLVER_ERROR (entity-first), and fail-stop marks every later key SKIPPED_PRIOR_FAILURE — replacing the pre-023 semantics where one failure nulled the whole group and erased already-executed writes (D4, unify both paths per clarify Q1). Full suite 1637 passed (baseline 1605 + 32 new, zero regressions). Co-Authored-By: Claude <noreply@anthropic.com>
…sh, specs/023) Adds docs/guide/graphql-aliases.md (quickstart examples, behavior table, error envelope, migration notes) and records two implementation-era findings in research.md: FieldSelection.name carries the target TYPE name on hand-built resolver trees (wire gate is alias-gated), and response_builder is live on the entity-first serialize path. Co-Authored-By: Claude <noreply@anthropic.com>
Adds 8 cases for gaps found in coverage review: - CLI --select alias rejection (FR-009 half that had zero tests) - top-level (service/entity-group) aliases: parser semantics + compose fan-out (implemented in US2 but untested) - federation remote-relationship field alias rejected pre-execution (FR-009's wire-gate complement, asserts zero method execution) - MCP compose_query end-to-end aliased fan-out Also verified γ (DTO federation) posts plain JSON to /nexusx/dto-batch — aliases never touch that wire, FR-008 holds by construction. Full suite 1645 passed (was 1637 + 8 new). Co-Authored-By: Claude <noreply@anthropic.com>
…cs/023 review) Groundwork for the review polish: - find_nested_alias(): single FR-009 tree walk, returning (dotted response-key path, original field name) so messages can render 'tasks' aliased to 'r' instead of the bare alias key - nested_alias_message(): shared reject wording - ResponseKeyConflictError: ValueError subclass for duplicate response keys (FR-007) — existing except-ValueError callers unaffected; lets handlers emit a machine-readable code Purely additive; consumers land in the following two commits. Also adds the validate_no_aliases smoke tests (public user-side guard, previously zero callers and zero coverage). Co-Authored-By: Claude <noreply@anthropic.com>
…1/P2) P1: gather(return_exceptions=True) delivers CancelledError (and KeyboardInterrupt/SystemExit) as VALUES; the old isinstance(value, BaseException) branch downgraded them to QUERY_FAILED error entries — a cancelled request (client disconnect, asyncio.timeout) then kept executing its serial mutations and returned a normal response. Now: Exception → per-field QUERY_FAILED; other BaseException → re-raise. The mutation path's except Exception already excluded CancelledError. P2: error paths use response keys — the service group alias (path_key), not the class name, matching the data keys the client received. Polish: per-field errors carry extensions.service_method (the fail-fast path always did) so MCP consumers locate the failing method without parsing the message; nested-alias detection delegates to the shared query_parser.find_nested_alias with unified wording. Tests: TestQueryCancellationPropagates (child raise / external task.cancel + mutation-not-run / plain-exception regression guard), aliased-group error path, service_method assertion. Co-Authored-By: Claude <noreply@anthropic.com>
…2/P3 + polish) P2: error paths and data keys both use response keys (alias or name) per the GraphQL spec — group_key computed once in execute_query and _execute_entity_group; validation / resolver / unknown-method / skip paths, the bare-group error, and the introspection data key all follow. Messages and logs keep ORIGINAL names (better for humans looking up the schema); only path carries response keys. P3: replace the always-true `... or True` assertion with the actual shape — a validation-rejected method is skipped and the group serializes as an empty object (group-level nulling was dropped in 023 D4). Polish: nested-alias detection delegates to the shared query_parser.find_nested_alias (drops the local duplicate walk) with unified 'x' aliased to 'y' wording; selection.py --select rejects via the same walk (path-qualified message); GraphQLHandler catches ResponseKeyConflictError to emit the ALIAS_CONFLICT extensions code, aligned with the compose path. Co-Authored-By: Claude <noreply@anthropic.com>
…review follow-up) Fail-stop previously only applied within a single service/entity group: measured, a mutation failing in SvcA let SvcB.write execute anyway (both paths). FR-006 says "calls after the failure stop" with no group qualifier, and GraphQL mutations are serial at OPERATION level. Both executors now propagate an abort flag across group boundaries: later groups' mutations are skipped and marked SKIPPED_PRIOR_FAILURE; queries are unaffected (FR-005). Contract scenario 4 semantics. Tests: cross-group/cross-service skip + query-unaffected on both paths. Co-Authored-By: Claude <noreply@anthropic.com>
…cs/023) - Behavior matrix: fail-stop range is operation-level (cross-group propagation); query failure code QUERY_FAILED now documented - Error structure: response-key path semantics + service_method extension in the example - Migration notes: add the compose-side per-field change (whole response null → per-field) and the operation-scope fail-stop change - Contract scenario 4/5 updated to match - tasks.md: review-fix record (P1-P3 + polish + new public APIs + the known multi-operation same-name-group boundary, documented) Co-Authored-By: Claude <noreply@anthropic.com>
allmonday
added a commit
that referenced
this pull request
Sep 2, 2026
Version bump for PR #141 (issue #140): method-level alias support on queries and mutations, three-state mutation feedback with operation-scope fail-stop, federation wire gate, and the review fixes (per-field query errors, response-key error paths, cancellation propagation). Updates docs/changelog.md, pyproject.toml, uv.lock. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Summary
Fixes #140 and delivers full specs/023: method-level GraphQL aliases on both query paths, with per-call three-state mutation feedback and a federation wire gate that keeps member services alias-free.
Root cause fixed:
QueryParser._parse_selection_setkeyedsub_fieldsby field name, so same-name aliased invocations overwrote each other — N aliasedadd_nodecalls executed once witherrors: []. Keys are now response keys (alias or name); lookups resolve viaFieldSelection.name.What you get
QUERY_FAILED), siblings unaffectedMUTATION_FAILED, later keys are skipped and markedSKIPPED_PRIOR_FAILURE(fail-stop)ALIAS_CONFLICTbefore any method executes_wire_render_namerenders original field names (alias-gated: hand-built resolver trees reusenamefor the target TYPE name, so only aliased nodes substitute). Members need zero changes--selectQueryParser.validate_no_aliases()keeps its semantics as an optional user-side guard; nexusx no longer calls it internallyBehavior changes to note (6.2.0, minor)
ALIAS_CONFLICTRESOLVER_ERROR, sibling results survive; mutation groups fail-stop (per specs/023 clarify Q1, both paths unified)QUERY_FAILED) instead of nulling the whole response; planning errors still fail fastValidation
ruff check src/cleanadd_node→ 6 nodes, 6 keyed results)specs/023-gql-alias-support/(spec/plan/research/contracts/quickstart/tasks, all 🇨🇳 per project convention); user guide atdocs/guide/graphql-aliases.md🤖 Generated with Claude Code