Skip to content

Prototype: FieldDirective - auto-wrap resolvers with directive functions#229

Open
captbaritone wants to merge 5 commits intomainfrom
field-directive-impl
Open

Prototype: FieldDirective - auto-wrap resolvers with directive functions#229
captbaritone wants to merge 5 commits intomainfrom
field-directive-impl

Conversation

@captbaritone
Copy link
Copy Markdown
Owner

Summary

  • Adds a new FieldDirective type exported from grats that directive functions can return to opt into automatic resolver wrapping
  • When a @gqlDirective function returns FieldDirective, Grats composes the directive wrapper around the field resolver in the generated schema
  • Eliminates the need for manual mapSchema + getDirective wiring for the common case of field-level directive behavior

Example

import { Int, FieldDirective } from "grats";

/** @gqlDirective on FIELD_DEFINITION */
export function rateLimit(args: { max: Int }): FieldDirective {
  return (next) => (source, args, context, info) => {
    checkRateLimit(context, args.max);
    return next(source, args, context, info);
  };
}

Generated output

resolve: rateLimit({ max: 10 })(function resolve(source) {
    return queryLikesResolver(source);
})

Design decisions

  • Opt-in via return type: Only directives that return FieldDirective get this behavior. Existing directives with void return type continue to work as metadata-only.
  • Syntactic detection: FieldDirective is detected by name in the return type annotation (the extractor is purely syntactic, no type checker).
  • Composable: Multiple FieldDirective directives on a single field nest naturally (outermost directive listed first).
  • Follows existing patterns: Similar to how @semanticNonNull already wraps resolvers at codegen time.

Related

Test plan

  • New test fixture fieldDirectiveWrapper.ts validates the generated output
  • Full test suite passes
  • Manual testing with the production-app example

🤖 Generated with Claude Code

When a `@gqlDirective` function's return type is `FieldDirective` (exported
from grats), Grats automatically composes the directive wrapper around the
field resolver in the generated schema. This eliminates the need for manual
`mapSchema` + `getDirective` wiring for the common case of field-level
directive behavior (auth, rate limiting, logging, etc).

The directive function is called with its annotation arguments and returns
a higher-order function that wraps the resolver:

  resolve: rateLimit({ max: 10 })(function resolve(source) { ... })

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 3, 2026

👷 Deploy Preview for grats processing.

Name Link
🔨 Latest commit 7fe7744
🔍 Latest deploy log https://app.netlify.com/projects/grats/deploys/69d029478870990008996cca

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 3, 2026

Deploy Preview for grats failed. Why did it fail? →

Name Link
🔨 Latest commit ddcc5f3
🔍 Latest deploy log https://app.netlify.com/projects/grats/deploys/69d044279e787a00086d28df

captbaritone and others added 4 commits April 3, 2026 14:18
- Move FieldDirective detection from syntactic name check in Extractor to
  a type-aware transform (resolveFieldDirectives) that uses the TS checker
  to verify the return type resolves to grats' FieldDirective type
- Update production-app example to use FieldDirective (removes mapSchema)
- Update directive definitions docs with FieldDirective section
- Update directive annotations docs to recommend FieldDirective approach
- Update permissions guide to use FieldDirective pattern
- Add integration test that verifies directives execute at runtime
  (both logging and result transformation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused eslint-disable in Types.ts
- Format resolveFieldDirectives.ts with prettier
- Fix integration test: use args with typed result instead of `never`
  args (which caused empty call) and `unknown` concatenation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace `type Query = unknown` + `@gqlField` with `@gqlQueryField`
  in both unit and integration test fixtures
- Add explicit `next: GraphQLFieldResolver<...>` type annotations in
  all FieldDirective examples: docs snippets, permissions guide,
  production-app, and test fixtures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use ctx.resolveNodeDeclaration() (new public method on TypeContext)
instead of directly calling checker.getSymbolAtLocation and
checker.getAliasedSymbol. This keeps the checker encapsulated within
TypeContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@captbaritone captbaritone marked this pull request as ready for review April 3, 2026 22:55
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.

1 participant