Skip to content

feat(composition): validation for transforms with better errors and backward-compat API#9398

Merged
ardatan merged 9 commits intomasterfrom
validation-transform
Apr 9, 2026
Merged

feat(composition): validation for transforms with better errors and backward-compat API#9398
ardatan merged 9 commits intomasterfrom
validation-transform

Conversation

@ardatan
Copy link
Copy Markdown
Owner

@ardatan ardatan commented Apr 7, 2026

Adds validation for Federation transform resolveReference config and @merge/@resolveTo directives with actionable error messages (including field/argument suggestions). Fixes several bugs introduced in the initial implementation and restores API backward compatibility.

Fixes

  • getComposedSchemaFromConfig: Restored return type to Promise<string> (was changed to { supergraphSdl, subgraphs }, breaking programmatic consumers). Added internal getComposedResultFromConfig used by run.ts instead.
  • run.ts: Skip validateSupergraphSdl when --subgraph flag is set (single-subgraph SDL is not a supergraph).
  • federation.ts: Changed throw new Error(...)throw new TransformValidationError(...) in all resolveReference validation to be consistent with the rest of the transform.
  • compose.ts: Preserve original error as cause when wrapping transform failures; stringify non-Error throws.
    // Before
    throw new Error(`Failed to apply transform "...": ${e.message}`);
    // After
    const cause = e instanceof Error ? e : new Error(String(e));
    throw new Error(`Failed to apply transform "...": ${errorMessage}`, { cause });
  • validate.ts: Fixed three bugs in @resolveTo field/arg validation:
    • typeDef was used without an undefined check (throws when sourceTypeName doesn't exist)
    • fields was FieldDefinitionNode[] but suggestionList was called with .map(f => f.name.value) on already-string values
    • fieldDef was a string (from fields.find) but accessed as .arguments (an AST node property) — arg validation never actually ran
    • Removed unreachable if (!subgraph) block and unused imports

Tests Added

  • federation.test.ts: Validation for resolveReference — missing fieldName, non-existent field (with suggestion), invalid operation type all throw TransformValidationError.
  • validate.test.ts (new): validateSupergraphSdl coverage for @merge (unknown subgraph) and @resolveTo (unknown subgraph, type, field with suggestion, argument with suggestion).

Copilot AI review requested due to automatic review settings April 7, 2026 10:25
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added validation for transform configurations with informative error messages and suggestions for missing fields and arguments
    • Implemented validation for @merge and @resolveTo directives to verify referenced fields and arguments exist in subgraph schemas
  • Bug Fixes

    • Improved error reporting when transforms are applied to subgraphs with clearer messaging
  • Tests

    • Added comprehensive test coverage for directive validation and transform configuration validation

Walkthrough

Composes annotated subgraphs and returns both supergraph SDL and annotated subgraphs; adds validator for federation directives (@merge, @resolveTo, @key/resolveReference) that checks subgraph/type/field/arg existence and produces suggestions; integrates validation into the CLI to fail early; improves per-transform errors with subgraph/transform context.

Changes

Cohort / File(s) Summary
Changeset Entry
.changeset/some-pianos-smoke.md
New changeset documenting patch releases and the added validation/error-message behavior for transforms and federation directives (with suggestions).
CLI composition & validation
packages/compose-cli/src/getComposedSchemaFromConfig.ts, packages/compose-cli/src/run.ts, packages/compose-cli/src/validate.ts
Composition now returns { supergraphSdl, subgraphs } via new getComposedResultFromConfig; replaced composeSubgraphs with composeAnnotatedSubgraphs; added validateSupergraphSdl(...) and FatalCompositionError; CLI invokes validator and exits on errors.
Fusion composition core
packages/fusion/composition/src/compose.ts
Wraps each SubgraphTransform application in try/catch and rethrows with transform name + subgraph context and cause.
Federation transform validation
packages/fusion/composition/src/transforms/federation.ts
Validates resolveReference config: enforces valid operation type, requires fieldName, verifies root type and field existence, provides suggestionList suggestions for near-matches, and uses validated target field name in merge map; makes returned transform function named.
Tests — validator & federation
packages/compose-cli/tests/validate.test.ts, packages/fusion/composition/tests/transforms/federation.test.ts
Adds tests for validateSupergraphSdl covering @merge and @resolveTo validation and suggestions; adds tests asserting resolveReference validation errors and suggestion messages.
Minor test/runtime tweaks
packages/legacy/runtime/test/reproduction-8688/sourceService.ts, packages/plugins/hive/tests/hive.test.ts
Removed console.log in connection-tracking middleware; updated Hive plugin test to use a logger stub with mocked methods.

Sequence Diagram

sequenceDiagram
    actor CLI as CLI Process
    participant Config as Config Loader
    participant Compose as Composition Engine
    participant Transforms as SubgraphTransforms
    participant Validate as Validator
    participant Output as Writer

    CLI->>Config: Load config
    Config->>Compose: getComposedResultFromConfig(config)
    Compose->>Transforms: getAnnotatedSubgraphs()\n(apply transforms with try/catch)
    Transforms-->>Compose: annotatedSubgraphs
    Compose->>Compose: composeAnnotatedSubgraphs(annotatedSubgraphs)
    Compose-->>Validate: supergraphSdl, subgraphs
    Validate->>Validate: Parse SDL, inspect `@merge/`@resolveTo/@key\nverify subgraph/type/field/args, compute suggestions

    alt Validation errors found
        Validate-->>CLI: GraphQLError[]
        CLI->>CLI: Log failures and messages
        CLI->>CLI: process.exit(1)
    else No errors
        Validate-->>Output: Validated supergraphSdl
        Output->>CLI: Write output
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • dotansimha

Poem

🐰 I hop through SDLs with careful paws,

I sniff missing fields and offer cause,
I nudge transforms with a knowing twitch,
“Did you mean…?” — I whisper, soft and rich,
A rabbit mending schemas without pause.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding validation for composition transforms with improved error messages and restoring backward compatibility.
Description check ✅ Passed The description clearly explains the purpose of the PR, detailing the validation additions, bugs fixed, and tests added, all of which align with the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch validation-transform

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@theguild-bot
Copy link
Copy Markdown
Collaborator

theguild-bot commented Apr 7, 2026

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-mesh/compose-cli 1.5.27-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/fusion-composition 0.8.43-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/include 0.3.36-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/incontext-sdk-codegen 0.0.14-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/cli 0.100.48-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/config 0.108.42-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/grpc 0.108.39-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/odata 0.106.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/openapi 0.109.48-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/soap 0.107.42-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/http 0.106.40-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/migrate-config-cli 1.7.35-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@omnigraph/grpc 0.2.20-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@omnigraph/odata 0.2.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@omnigraph/openapi 0.109.47-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@omnigraph/soap 0.107.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/plugin-hive 0.105.11-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/plugin-newrelic 0.104.40-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/plugin-response-cache 0.104.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/plugin-statsd 0.104.40-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/transport-grpc 0.3.37-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/transport-odata 0.2.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎
@graphql-mesh/transport-soap 0.10.41-alpha-20260409103243-cbac018ee4286fe3fcdc933c84d5769289fe3ea6 npm ↗︎ unpkg ↗︎

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

💻 Website Preview

The latest changes are available as preview in: https://9998d7f6.graphql-mesh.pages.dev

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds additional validation and clearer error reporting during composition—especially around transforms (e.g. Federation transform) and directive-driven wiring (@merge, @resolveTo)—and surfaces these validation errors in the compose CLI.

Changes:

  • Add validation for Federation transform resolveReference config, including root field existence + suggestions.
  • Improve transform error reporting by wrapping failures with transform/subgraph context.
  • Add compose-cli post-compose validation for @merge / @resolveTo and integrate it into the CLI flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/fusion/composition/src/transforms/federation.ts Adds validation for @key/resolveReference config (operation/root-field existence + suggestions).
packages/fusion/composition/src/compose.ts Wraps transform application errors to include transform + subgraph context (also introduces an unused import).
packages/compose-cli/src/validate.ts New supergraph validation pass for @merge/@resolveTo references (currently has several runtime-breaking issues).
packages/compose-cli/src/run.ts Runs validation after composition and exits on errors (currently breaks --subgraph).
packages/compose-cli/src/getComposedSchemaFromConfig.ts Changes return shape to include { supergraphSdl, subgraphs } for downstream validation.
.changeset/some-pianos-smoke.md Declares patch releases for compose-cli and fusion-composition for the validation/error-message improvements.
Comments suppressed due to low confidence (1)

packages/fusion/composition/src/compose.ts:26

  • createGraphQLError is imported but never used in this file. This will trigger unused-import linting in many setups; either remove the import or use it when wrapping transform failures (e.g. to preserve locations/cause).
import { snakeCase } from 'snake-case';
import { stitchingDirectives } from '@graphql-tools/stitching-directives';
import {
  createGraphQLError,
  getDirectiveExtensions,
  getDocumentNodeFromSchema,
  MapperKind,
  mapSchema,
  type FieldMapper,
} from '@graphql-tools/utils';

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +131 to +137
const errors = validateSupergraphSdl(supergraphSdl, subgraphs);
if (errors.length) {
log.error(`Composition validation failed with the following errors:`);
for (const error of errors) {
log.error(error.message);
}
process.exit(1);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateSupergraphSdl is executed unconditionally, but when --subgraph is used getComposedSchemaFromConfig returns that subgraph’s SDL (not a composed supergraph). This will throw (e.g. missing join__Graph) and break the --subgraph workflow. Consider skipping validateSupergraphSdl when config.subgraph is set, or returning a real supergraph SDL for that mode.

Suggested change
const errors = validateSupergraphSdl(supergraphSdl, subgraphs);
if (errors.length) {
log.error(`Composition validation failed with the following errors:`);
for (const error of errors) {
log.error(error.message);
}
process.exit(1);
if (!config.subgraph) {
const errors = validateSupergraphSdl(supergraphSdl, subgraphs);
if (errors.length) {
log.error(`Composition validation failed with the following errors:`);
for (const error of errors) {
log.error(error.message);
}
process.exit(1);
}

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +118
}
// Args validation
if (resolveToDirective.sourceArgs) {
const argOfFields =
'arguments' in fieldDef && fieldDef.arguments
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typeDef can be undefined when sourceTypeName doesn’t exist in the referenced subgraph, but the code uses it immediately with the in operator ('fields' in typeDef), which will throw at runtime. Add an explicit if (!typeDef) branch that reports a GraphQL error with suggestions for the type name.

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +123
const argOfFields =
'arguments' in fieldDef && fieldDef.arguments
? fieldDef.arguments.map(arg => arg.name.value)
: [];
for (const argName in resolveToDirective.sourceArgs) {
const argNameInField = argOfFields.find(arg => arg === argName);
if (!argNameInField) {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fields is built as a string[] (field names), but later it’s treated as if it contained AST field nodes (fields.map(f => f.name.value)). This will crash when trying to build suggestions. Use the string[] directly for suggestionList (or keep both the field nodes and the names separately).

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +142
const argOfFields =
'arguments' in fieldDef && fieldDef.arguments
? fieldDef.arguments.map(arg => arg.name.value)
: [];
for (const argName in resolveToDirective.sourceArgs) {
const argNameInField = argOfFields.find(arg => arg === argName);
if (!argNameInField) {
const suggestions = suggestionList(argName, argOfFields);
const suggestionStr = suggestions.length
? ` Did you mean "${suggestions.join(' or ')}"?`
: '';
errors.push(
createGraphQLError(
`@resolveTo directive on type ${typeName} references unknown argument ${argName} in field ${fieldName} of subgraph ${subgraphName} ${suggestionStr}`,
{
nodes: type.astNode,
},
),
);
}
}
}
}
}
}
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fieldDef is derived from fields.find(...) where fields is a string[], so fieldDef is a string. The subsequent args validation treats fieldDef like a field AST node (fieldDef.arguments), which means argument validation will never work and may misreport every sourceArgs entry as unknown. Keep the actual field definition node (from typeDef.fields) so you can read its arguments correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +117 to 133
const annotatedSubgraphs = getAnnotatedSubgraphs(subgraphConfigsForComposition, {
ignoreSemanticConventions: config.ignoreSemanticConventions,
alwaysAddTransportDirective: config.subgraph != null,
});
if (config.subgraph) {
const annotatedSubgraphs = getAnnotatedSubgraphs(subgraphConfigsForComposition, {
ignoreSemanticConventions: config.ignoreSemanticConventions,
alwaysAddTransportDirective: true,
});
const subgraph = annotatedSubgraphs.find(sg => sg.name === config.subgraph);
if (!subgraph) {
logger.error(`Subgraph ${config.subgraph} not found`);
process.exit(1);
}
return print(subgraph.typeDefs);
return {
supergraphSdl: print(subgraph.typeDefs),
subgraphs: annotatedSubgraphs,
};
}
const result = composeSubgraphs(subgraphConfigsForComposition, {
ignoreSemanticConventions: config.ignoreSemanticConventions,
alwaysAddTransportDirective: false,
});
const result = composeAnnotatedSubgraphs(annotatedSubgraphs);
if (result.errors?.length) {
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function return type changed from a SDL string to an object { supergraphSdl, subgraphs }, but getComposedSchemaFromConfig is re-exported from the package entrypoint. This is a breaking change for programmatic consumers and doesn’t match the patch-level changeset. Consider keeping the old return type (and adding a new API for additional data) or bumping the version appropriately.

Copilot uses AI. Check for mistakes.
Comment on lines +329 to +330
throw new Error(
`Failed to apply transform "${transform.name}" on subgraph "${subgraphName}": ${e.message}`,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping transform errors with new Error(...) drops the original error type and stack trace, and e.message can be undefined if something non-Error is thrown. Prefer preserving the original error as cause (and stringify non-Error throws) so the resulting failure is still debuggable.

Suggested change
throw new Error(
`Failed to apply transform "${transform.name}" on subgraph "${subgraphName}": ${e.message}`,
const cause = e instanceof Error ? e : new Error(String(e));
const errorMessage = e instanceof Error ? e.message : String(e);
throw new Error(
`Failed to apply transform "${transform.name}" on subgraph "${subgraphName}": ${errorMessage}`,
{ cause },

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +325
// Validate merge directive
// First validate operation config
const operationType =
keyConfig.resolveReference?.operation || ('query' as OperationTypeNode);
if (
operationType !== 'query' &&
operationType !== 'mutation' &&
operationType !== 'subscription'
) {
throw new Error(
`Invalid operation type "${operationType}" in resolveReference config for @key directive on ${type.name} type. Expected "query", "mutation" or "subscription".`,
);
}
const targetFieldName = keyConfig.resolveReference.fieldName;
if (!targetFieldName) {
throw new Error(
`Missing fieldName in resolveReference config for @key directive on ${type.name} type.`,
);
}
const rootType = getDefinedRootType(subgraphSchema, operationType);
if (!rootType) {
throw new Error(
`Root type for operation "${operationType}" not found in schema for @key directive on ${type.name} type.`,
);
}
const rootTypeFields = rootType.getFields();
if (!rootTypeFields[targetFieldName]) {
const suggestions = suggestionList(
targetFieldName,
Object.keys(rootTypeFields) as string[],
);
const suggestionStr = suggestions.length
? ` Did you mean "${suggestions.join(' or ')}"?`
: '';
throw new Error(
`Field "${targetFieldName}" not found in root type "${rootType.name}" for @key directive on ${type.name} type. ${suggestionStr}`,
);
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new resolveReference validation throws plain Errors, while the rest of this transform uses TransformValidationError for configuration problems. Using the same error class here keeps error handling consistent (and avoids losing the semantic error type when callers catch by name).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/compose-cli/src/validate.ts (1)

29-43: Use @join__graph(name:) values for subgraph identity, not enum value names.

subgraphNames is currently populated from enum value identifiers (value.name), which can differ from the actual subgraph name carried in @join__graph(name: ...). This can produce false “unknown subgraph” errors.

Proposed fix
-  const subgraphNames = new Set();
+  const subgraphNames = new Set<string>();
   for (const value of joinGraphEnum.getValues()) {
-    subgraphNames.add(value.name);
     const directives = getDirectiveExtensions(value, schema);
     if (!directives?.join__graph) {
       throw new FatalCompositionError(
         `Expected join__Graph enum value ${value.name} to have `@join__graph` directive`,
       );
     }
     for (const directive of directives.join__graph) {
       if (!directive.name) {
         throw new FatalCompositionError(
           `Expected `@join__graph` directive on join__Graph enum value ${value.name} to have a name argument`,
         );
       }
+      subgraphNames.add(directive.name);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/compose-cli/src/validate.ts` around lines 29 - 43, The code
currently adds enum value identifiers (value.name) to subgraphNames, but the
canonical subgraph identity is the `@join__graph`(name: ...) argument; update the
logic in the loop over joinGraphEnum.getValues() (around joinGraphEnum,
subgraphNames, getDirectiveExtensions, directives.join__graph) to extract and
add the name argument from each join__graph directive (e.g., directive.name) to
subgraphNames instead of value.name, and still throw a FatalCompositionError via
the same checks if a join__graph directive or its name argument is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/compose-cli/src/run.ts`:
- Around line 130-138: The code unconditionally calls validateSupergraphSdl on
the value returned by getComposedSchemaFromConfig, which breaks subgraph-only
mode because supergraphSdl in that path is not a composed supergraph; update the
logic to skip validation when running in subgraph-only mode (check
config.subgraph) or when supergraphSdl is not a composed supergraph, e.g., only
call validateSupergraphSdl(supergraphSdl, subgraphs) when config.subgraph is
falsy (or when a composed marker like join__Graph exists), and ensure you do not
process.exit(1) for subgraph-only output so --subgraph continues to work.

In `@packages/compose-cli/src/validate.ts`:
- Around line 114-145: The AST lookup for
resolveToDirective.sourceTypeName/sourceFieldName mistakenly maps typeDef.fields
to strings so fieldDef becomes a string; revert fields to the AST
FieldDefinition nodes (do not map to f.name.value), then find fieldDef by
comparing f.name.value === fieldName; when building suggestions call
suggestionList(fieldName, fields.map(f => f.name.value)) (i.e., map only for
suggestions), and for arg validation use the actual fieldDef.arguments (e.g.,
const argOfFields = fieldDef.arguments ? fieldDef.arguments.map(a =>
a.name.value) : []), ensuring subsequent checks like 'arguments' in fieldDef or
accessing fieldDef.arguments operate on the AST node rather than a string.
- Around line 46-76: The loop currently validates only type-level directives via
getDirectiveExtensions(type, schema) and misses field-level `@merge` and
`@resolveTo`; update the validation to also iterate each type's fields (use
type.getFields() inside the for (const typeName in typeMap) block), call
getDirectiveExtensions(field, schema) for each field, and apply the same checks:
for `@merge` verify mergeDirective.subgraph exists and is in subgraphNames, and
for `@resolveTo` verify resolveTo.sourceName/subgraph (or the expected arguments)
are present and valid. When pushing errors use createGraphQLError with nodes:
field.astNode (or the appropriate field AST node) and keep existing error
messages/formats; reuse same symbols mergeDirective.subgraph,
resolveTo.sourceName, subgraphNames, createGraphQLError to locate where to add
these checks.

---

Nitpick comments:
In `@packages/compose-cli/src/validate.ts`:
- Around line 29-43: The code currently adds enum value identifiers (value.name)
to subgraphNames, but the canonical subgraph identity is the `@join__graph`(name:
...) argument; update the logic in the loop over joinGraphEnum.getValues()
(around joinGraphEnum, subgraphNames, getDirectiveExtensions,
directives.join__graph) to extract and add the name argument from each
join__graph directive (e.g., directive.name) to subgraphNames instead of
value.name, and still throw a FatalCompositionError via the same checks if a
join__graph directive or its name argument is missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2820b1f6-d674-4758-b2cd-ba027c5c818a

📥 Commits

Reviewing files that changed from the base of the PR and between 8a33be2 and a30c891.

📒 Files selected for processing (6)
  • .changeset/some-pianos-smoke.md
  • packages/compose-cli/src/getComposedSchemaFromConfig.ts
  • packages/compose-cli/src/run.ts
  • packages/compose-cli/src/validate.ts
  • packages/fusion/composition/src/compose.ts
  • packages/fusion/composition/src/transforms/federation.ts

Comment on lines +46 to +76
for (const typeName in typeMap) {
const type = typeMap[typeName];
if (type) {
const directives = getDirectiveExtensions(type, schema);
if (directives?.merge) {
for (const mergeDirective of directives.merge) {
const subgraphName = mergeDirective.subgraph;
if (!subgraphName) {
errors.push(
createGraphQLError(
`Expected @merge directive on type ${typeName} to have a subgraph argument`,
{
nodes: type.astNode,
},
),
);
continue;
} else if (!subgraphNames.has(subgraphName)) {
errors.push(
createGraphQLError(
`@merge directive on type ${typeName} references unknown subgraph ${subgraphName}`,
{
nodes: type.astNode,
},
),
);
continue;
}
}
}
if (directives?.resolveTo) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the validate.ts file and examine its content
fd -t f "validate.ts" packages/compose-cli/src/

Repository: ardatan/graphql-mesh

Length of output: 100


🏁 Script executed:

# Read the validate.ts file to understand full context
cat -n packages/compose-cli/src/validate.ts

Repository: ardatan/graphql-mesh

Length of output: 6560


🏁 Script executed:

# Search for `@merge` directive usage in test files and examples
rg "@merge" --type graphql --type typescript --type json -A 2 -B 2 | head -100

Repository: ardatan/graphql-mesh

Length of output: 95


🏁 Script executed:

# Search for `@merge` directive usage across the codebase
rg "@merge" -A 2 -B 2 | head -150

Repository: ardatan/graphql-mesh

Length of output: 13079


🏁 Script executed:

# Search for `@resolveTo` directive usage
rg "@resolveTo" -A 2 -B 2 | head -100

Repository: ardatan/graphql-mesh

Length of output: 7572


🏁 Script executed:

# Search for field-level directive validation
rg "getDirectiveExtensions" -A 3 -B 1

Repository: ardatan/graphql-mesh

Length of output: 31371


🏁 Script executed:

# Check if there are any tests for the validate function
rg "validateSupergraphSdl" -A 10 -B 2 | head -100

Repository: ardatan/graphql-mesh

Length of output: 2560


🏁 Script executed:

# Search for test files related to validate
fd -t f "*test*" packages/compose-cli/src/ | head -20

Repository: ardatan/graphql-mesh

Length of output: 349


🏁 Script executed:

# Search for test files related to validate
fd --glob "*test*" packages/compose-cli/

Repository: ardatan/graphql-mesh

Length of output: 150


🏁 Script executed:

# Check test directory structure and content
find packages/compose-cli/tests -type f -name "*.ts" | head -20

Repository: ardatan/graphql-mesh

Length of output: 122


🏁 Script executed:

# Search for field iteration in validation or processing of directives
rg "MapperKind.FIELD|\.fields\[|for.*field" packages/compose-cli/src/ -A 2 -B 2

Repository: ardatan/graphql-mesh

Length of output: 46


🏁 Script executed:

# Search for any validation of field-level `@merge` or `@resolveTo` directives
rg "fieldConfig|field.*merge|field.*resolveTo" packages/compose-cli/ -A 2 -B 2

Repository: ardatan/graphql-mesh

Length of output: 471


🏁 Script executed:

# Check GraphQL type structure to verify if fields can be iterated
rg "getFields|\.fields" packages/compose-cli/src/ -A 2

Repository: ardatan/graphql-mesh

Length of output: 1849


Field-level @merge and @resolveTo directives are not validated.

The validation loop only checks type-level directives via getDirectiveExtensions(type, schema). Documentation shows both directives are primarily used on fields (e.g., author(id: ID): Author @merge(...) and author: Author @resolveTo(...)), but field-level instances with missing or invalid subgraph/sourceName arguments will silently pass validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/compose-cli/src/validate.ts` around lines 46 - 76, The loop
currently validates only type-level directives via getDirectiveExtensions(type,
schema) and misses field-level `@merge` and `@resolveTo`; update the validation to
also iterate each type's fields (use type.getFields() inside the for (const
typeName in typeMap) block), call getDirectiveExtensions(field, schema) for each
field, and apply the same checks: for `@merge` verify mergeDirective.subgraph
exists and is in subgraphNames, and for `@resolveTo` verify
resolveTo.sourceName/subgraph (or the expected arguments) are present and valid.
When pushing errors use createGraphQLError with nodes: field.astNode (or the
appropriate field AST node) and keep existing error messages/formats; reuse same
symbols mergeDirective.subgraph, resolveTo.sourceName, subgraphNames,
createGraphQLError to locate where to add these checks.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Apollo Federation Subgraph Compatibility Results

Federation 1 Support Federation 2 Support
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢

Learn more:

@ardatan
Copy link
Copy Markdown
Owner Author

ardatan commented Apr 9, 2026

@copilot apply changes based on the comments in this thread and also add tests for those changes

Copilot AI and others added 2 commits April 9, 2026 10:22
…nError, backward-compat API, and add tests

Agent-Logs-Url: https://github.com/ardatan/graphql-mesh/sessions/c0546f66-b604-46e4-bbb2-9c0ef51f6a4d

Co-authored-by: ardatan <20847995+ardatan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

@copilot apply changes based on the comments in this thread and also add tests for those changes

Applied all review changes in commits 61a4d8d and 65be1a0:

Review fixes applied:

  • compose.ts: Preserve original error as cause when wrapping transform failures; stringify non-Error throws properly
  • federation.ts: Changed throw new Error(...) to throw new TransformValidationError(...) in all resolveReference validation to be consistent with the rest of the transform
  • getComposedSchemaFromConfig.ts: Kept backward-compatible getComposedSchemaFromConfig returning Promise<string>; added new getComposedResultFromConfig (returning { supergraphSdl, subgraphs }) used internally by run.ts
  • validate.ts: Fixed typeDef undefined handling, fixed fields being treated as AST nodes (now correctly FieldDefinitionNode[]), fixed fieldDef being used as AST node (was string), removed unreachable if (!subgraph) block, removed unused imports, removed unsafe as string type assertions
  • run.ts: Already had the if (!config.subgraph) guard; updated to use getComposedResultFromConfig instead

Tests added:

  • packages/fusion/composition/tests/transforms/federation.test.ts: Tests for resolveReference validation (missing fieldName, non-existent field with suggestion, invalid operation type)
  • packages/compose-cli/tests/validate.test.ts: Tests for validateSupergraphSdl covering @merge and @resolveTo directive validation (unknown subgraph, unknown type, unknown field with suggestion, unknown argument with suggestion)

Copilot AI changed the title feat(composition): validation for transforms feat(composition): validation for transforms with better errors and backward-compat API Apr 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/compose-cli/src/validate.ts (1)

7-13: Error.captureStackTrace is V8-specific and may not exist in all environments.

While this works in Node.js, it could throw in non-V8 environments (e.g., some edge runtimes). Consider adding a guard.

♻️ Suggested defensive guard
 export class FatalCompositionError extends Error {
   constructor(message: string) {
     super(`Fatal composition error: ${message}`);
     this.name = 'FatalCompositionError';
-    Error.captureStackTrace(this, this.constructor);
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, this.constructor);
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/compose-cli/src/validate.ts` around lines 7 - 13, The class
FatalCompositionError currently calls Error.captureStackTrace in its constructor
which is V8-specific and may throw in non-V8 runtimes; update the
FatalCompositionError constructor to check for the existence of
Error.captureStackTrace (e.g., typeof Error.captureStackTrace === 'function')
before calling it and fall back silently when unavailable so the code is safe in
edge/non-V8 environments while preserving the message and name assignments.
packages/fusion/composition/tests/transforms/federation.test.ts (1)

122-138: Minor inefficiency: transform is called twice per test case.

Each test invokes transform(schema, subgraphConfig) twice—once to check the error type and once for the message pattern. Consider capturing the error once using expect().toThrow() with a function that validates both, or use a try/catch pattern to assert on both properties from a single invocation.

♻️ Suggested optimization (optional)
     it('throws TransformValidationError when fieldName is missing', () => {
       const transform = createFederationTransform({
         Foo: {
           key: {
             fields: 'id',
             resolveReference: {
               fieldName: '',
               keyArg: 'id',
             },
           },
         },
       });
-      expect(() => transform(schema, subgraphConfig)).toThrow(TransformValidationError);
-      expect(() => transform(schema, subgraphConfig)).toThrow(
-        /Missing fieldName in resolveReference config for `@key` directive on Foo type/,
-      );
+      expect(() => transform(schema, subgraphConfig)).toThrow(
+        expect.objectContaining({
+          name: 'TransformValidationError',
+          message: expect.stringMatching(/Missing fieldName in resolveReference config for `@key` directive on Foo type/),
+        }),
+      );
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fusion/composition/tests/transforms/federation.test.ts` around lines
122 - 138, The test currently calls transform(schema, subgraphConfig) twice;
change it to invoke transform once and capture the thrown error (e.g., via
try/catch) and then assert both that the error is an instance of
TransformValidationError and that its message matches /Missing fieldName.../;
this involves updating the test that uses createFederationTransform (variable
transform) to call transform(schema, subgraphConfig) a single time and then run
expect(err).toBeInstanceOf(TransformValidationError) and
expect(err.message).toMatch(/Missing fieldName in resolveReference config for
`@key` directive on Foo type/).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/compose-cli/src/validate.ts`:
- Around line 7-13: The class FatalCompositionError currently calls
Error.captureStackTrace in its constructor which is V8-specific and may throw in
non-V8 runtimes; update the FatalCompositionError constructor to check for the
existence of Error.captureStackTrace (e.g., typeof Error.captureStackTrace ===
'function') before calling it and fall back silently when unavailable so the
code is safe in edge/non-V8 environments while preserving the message and name
assignments.

In `@packages/fusion/composition/tests/transforms/federation.test.ts`:
- Around line 122-138: The test currently calls transform(schema,
subgraphConfig) twice; change it to invoke transform once and capture the thrown
error (e.g., via try/catch) and then assert both that the error is an instance
of TransformValidationError and that its message matches /Missing fieldName.../;
this involves updating the test that uses createFederationTransform (variable
transform) to call transform(schema, subgraphConfig) a single time and then run
expect(err).toBeInstanceOf(TransformValidationError) and
expect(err.message).toMatch(/Missing fieldName in resolveReference config for
`@key` directive on Foo type/).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 358f95fa-0411-482f-931b-95c9459832c0

📥 Commits

Reviewing files that changed from the base of the PR and between b2b46ed and cbac018.

📒 Files selected for processing (9)
  • packages/compose-cli/src/getComposedSchemaFromConfig.ts
  • packages/compose-cli/src/run.ts
  • packages/compose-cli/src/validate.ts
  • packages/compose-cli/tests/validate.test.ts
  • packages/fusion/composition/src/compose.ts
  • packages/fusion/composition/src/transforms/federation.ts
  • packages/fusion/composition/tests/transforms/federation.test.ts
  • packages/legacy/runtime/test/reproduction-8688/sourceService.ts
  • packages/plugins/hive/tests/hive.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/legacy/runtime/test/reproduction-8688/sourceService.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/fusion/composition/src/transforms/federation.ts
  • packages/fusion/composition/src/compose.ts

@ardatan ardatan merged commit bc4b5d0 into master Apr 9, 2026
19 checks passed
@ardatan ardatan deleted the validation-transform branch April 9, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants