DEV Community

Yamashou
Yamashou

Posted on

rgql: AST-Aware GraphQL Refactoring That AI Agents Can Trust

TL;DRrgql is a CLI tool that renames GraphQL types, fields, and fragments across schema files and embedded TypeScript queries using full AST analysis. Because it's type-safe and deterministic, it's ideal for AI coding agents like Claude Code and Codex — they can invoke a single command instead of performing fragile find-and-replace across dozens of files.

The Problem: Renaming in GraphQL Is Surprisingly Hard

Imagine you have a User type with a firstName field, and a Product type that also has a firstName field. You want to rename User.firstName to User.fullName.

A naive find-and-replace would touch both. Even a regex-aware approach struggles when the same field name appears in nested selections:

query GetDashboard {
  user {
    firstName    # ← should be renamed
    orders {
      product {
        firstName  # ← should NOT be renamed
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now multiply this across .graphql schema files and graphql tagged templates embedded in .ts/.tsx files. Manual refactoring is tedious and error-prone. IDE support for GraphQL refactoring is limited — most editors treat GraphQL strings as opaque text.

The Solution: AST-Aware, Type-Safe Refactoring

rgql solves this with two key GraphQL-js primitives: TypeInfo and visitWithTypeInfo.

When renaming User.firstName → User.fullName, rgql:

  1. Parses your schema to build a full type graph
  2. Walks every query/mutation/fragment using visitWithTypeInfo, which tracks the parent type at each field node
  3. Only renames a firstName field when typeInfo.getParentType() resolves to User

This means Product.firstName is never touched — not because of text heuristics, but because the tool understands the schema semantics.

# Dry run — preview changes without modifying files
rgql rename field User.firstName User.fullName

# Apply
rgql rename field User.firstName User.fullName --write
Enter fullscreen mode Exit fullscreen mode

Output:

Changes:
  schema/user.graphql:12  firstName → fullName  [schema]
  src/components/UserCard.tsx:34  firstName → fullName  (graphql`...`)  [document]

2 files, 2 occurrences
Dry run. Use --write to apply.
Enter fullscreen mode Exit fullscreen mode

Why This Matters for AI Agents

Here's the angle I'm most excited about: rgql is designed to be used by AI coding agents.

When you ask Claude Code or Codex to rename a GraphQL field, the agent typically has to:

  1. Find the schema definition
  2. Find all query files referencing that field
  3. Determine which firstName belongs to which type
  4. Apply changes to each file individually
  5. Hope it didn't miss anything or break something

This consumes a lot of context window — the agent needs to read and reason about every file. And even smart agents can miss edge cases like inline fragments or interface implementations.

With rgql, the agent just runs:

rgql rename field User.firstName User.fullName --write
Enter fullscreen mode Exit fullscreen mode

One command. Zero context consumed. Guaranteed correctness.

The agent doesn't need to read any GraphQL files. It doesn't need to understand query nesting. The AST does the work. This is the same philosophy behind tools like gopls rename or ts-morph — give agents safe, atomic refactoring primitives instead of asking them to do text surgery.

Interface Safety — Even Agents Can't Break This

When renaming a field that's defined on an interface, rgql detects the impact:

⚠️  Breaking change detected:
    'firstName' is defined on interface 'Node'
    The following types implement this interface:

    - User.firstName
    - Admin.firstName

Rename all of the above together? [y/n/q]
Enter fullscreen mode Exit fullscreen mode

Without --force, it refuses to apply a partial rename that would break your schema. This guardrail protects both humans and AI agents from introducing silent inconsistencies.

Architecture: Pure Core / Impure Shell

rgql follows a Functional Domain Modeling architecture:

src/
├── core/           # Pure functions — no IO, no side effects
│   ├── pipeline.ts       # computeRenamePlan() → RenameOutcome
│   ├── validate.ts       # Type/field existence, collision checks
│   ├── rename-schema.ts  # AST rename in .graphql files
│   └── rename-document.ts # AST rename in queries (standalone + embedded)
├── shell/          # IO boundary — the only place with side effects
│   ├── cli.ts            # handleOutcome() — console output, file writes
│   ├── config.ts         # graphql-config loading
│   └── file-system.ts    # File I/O
└── types/
    ├── domain.ts         # Branded types, discriminated unions
    └── result.ts         # Result<T, E> instead of exceptions
Enter fullscreen mode Exit fullscreen mode

The entire rename pipeline is a pure function:

RenameCommand + Schema + Documents → RenameOutcome
Enter fullscreen mode Exit fullscreen mode

RenameOutcome is a discriminated union with 7 variants — dry-run, written, no-changes, validation-error, interface-skipped, interface-aborted, interactive-complete. The shell layer pattern-matches on the variant and performs the appropriate IO.

This makes the core logic trivially testable and predictable — which is exactly what you want when an AI agent is deciding whether to call your tool.

Branded Types Prevent Mixing

TypeScript lets you accidentally pass a type name where a field name is expected — they're both string. rgql prevents this at compile time:

type TypeName = string & { readonly __brand: "TypeName" };
type FieldName = string & { readonly __brand: "FieldName" };

const toTypeName = (value: string): TypeName => value as TypeName;
Enter fullscreen mode Exit fullscreen mode

No runtime cost. Full compile-time safety.

Result Over Exceptions

Instead of throw, every function returns a Result:

type Result<T, E> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };
Enter fullscreen mode Exit fullscreen mode

This makes error paths explicit and composable with mapResult / flatMapResult. No hidden control flow.

Embedded Query Detection

rgql doesn't just handle standalone .graphql files. It uses ts-morph (TypeScript AST) to find graphql and gql tagged template literals inside .ts and .tsx files:

// rgql finds and correctly renames inside this:
const GET_USER = graphql`
  query GetUser {
    user {
      firstName  # ← renamed to fullName
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

The offset arithmetic is handled internally — GraphQL AST positions are relative to the query string, but rgql translates them to absolute file positions for precise text replacement.

Getting Started

# Install (requires Bun)
bun install
bun run build    # produces ./rgql single binary

# Configuration — uses standard graphql-config
# graphql.config.yml
schema: ./schema/**/*.graphql
documents: ./src/**/*.{ts,tsx}
Enter fullscreen mode Exit fullscreen mode

Three rename commands:

rgql rename type User Account          # Rename a type
rgql rename field User.firstName User.fullName  # Rename a field
rgql rename fragment UserBasic UserSummary      # Rename a fragment
Enter fullscreen mode Exit fullscreen mode

Flags:

Flag Description
--write Apply changes (default: dry run)
-i Interactive — confirm each change
--force Allow interface cascade renames
--config Explicit config file path
--project Project name for monorepo configs

What's Next

  • Support /* GraphQL */ and # graphql comment-tagged queries
  • rename enum-value command
  • Watch mode for continuous refactoring
  • npm / Homebrew distribution

Try It Out

GitHub: https://github.com/Yamashou/rgql

If you're using GraphQL with TypeScript and tired of manual renames — or if you're building AI agent workflows that need safe refactoring primitives — give rgql a try.


Built with Bun, graphql-js, ts-morph, and Commander.js.

Top comments (0)