Skip to content

feat: add TypeSystem directives support with introspection#1408

Open
jwaldrip wants to merge 5 commits intoabsinthe-graphql:mainfrom
gigsmart:feat/typesystem-directives
Open

feat: add TypeSystem directives support with introspection#1408
jwaldrip wants to merge 5 commits intoabsinthe-graphql:mainfrom
gigsmart:feat/typesystem-directives

Conversation

@jwaldrip
Copy link
Copy Markdown
Contributor

Summary

This adds full TypeSystem directive support as requested in issue #1003.

Changes

  • Add applied_directives field to all Type structs (Object, Scalar, Field, Interface, Union, Enum, InputObject, Argument, Enum.Value)
  • Preserve applied directives through the build phase from Blueprint to final Type structs
  • Add __AppliedDirective and __DirectiveArgument introspection types
  • Add appliedDirectives field to __Type, __Field, __InputValue, and __EnumValue introspection types

Supported Locations

TypeSystem directives can be applied to all spec-defined locations:

  • SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION
  • INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION

The directive expand callback continues to work for transforming type definitions at compile time, and applied directives are now visible through introspection queries.

Example Introspection

{
  __type(name: "User") {
    appliedDirectives {
      name
      args {
        name
        value
      }
    }
  }
}

Closes #1003

Test plan

  • Added comprehensive TypeSystem directive tests (25 new tests)
  • All 1467 tests pass
  • Tested directive application on all type system elements
  • Verified introspection returns applied directives correctly

🤖 Generated with Claude Code

This adds full TypeSystem directive support as requested in issue absinthe-graphql#1003:

- Add `applied_directives` field to all Type structs (Object, Scalar,
  Field, Interface, Union, Enum, InputObject, Argument, Enum.Value)
- Preserve applied directives through the build phase from Blueprint
  to final Type structs
- Add `__AppliedDirective` and `__DirectiveArgument` introspection types
- Add `appliedDirectives` field to `__Type`, `__Field`, `__InputValue`,
  and `__EnumValue` introspection types
- TypeSystem directives can be applied to all spec-defined locations:
  SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION,
  INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION

The directive `expand` callback continues to work for transforming
type definitions at compile time, and applied directives are now
visible through introspection queries.

Closes absinthe-graphql#1003

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jwaldrip jwaldrip force-pushed the feat/typesystem-directives branch from 5d8f1f5 to ccfd7c7 Compare January 13, 2026 15:17
@jwaldrip
Copy link
Copy Markdown
Contributor Author

jwaldrip commented Feb 6, 2026

Any thoughts on the TypeSystem directives support? This adds introspection for directives applied to schema types. Would love any feedback or concerns.

@jwaldrip jwaldrip marked this pull request as ready for review February 6, 2026 18:44
absinthe-graphql#1377)

* update

* fix introspection

* add claude.md

* Fix mix tasks to respect schema adapter for proper naming conventions

- Fix mix absinthe.schema.json to use schema's adapter for introspection
- Fix mix absinthe.schema.sdl to use schema's adapter for directive names
- Update SDL renderer to accept adapter parameter and use it for directive definitions
- Ensure directive names follow naming conventions (camelCase, etc.) in generated SDL

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Add field description inheritance from referenced types

When a field has no description, it now inherits the description from its
referenced type during introspection. This provides better documentation
for GraphQL APIs by automatically propagating type descriptions to fields.

- Modified __field introspection resolver to fall back to type descriptions
- Handles wrapped types (non_null, list_of) correctly by unwrapping first
- Added comprehensive test coverage for various inheritance scenarios
- Updated field documentation to explain the new behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* gitignore local settings

* fix sdl render

* feat: Add @defer and @stream directive support for incremental delivery

- Add @defer directive for deferred fragment execution
- Add @stream directive for incremental list delivery
- Implement streaming resolution phase
- Add incremental response builder
- Add transport abstraction layer
- Implement Dataloader integration for streaming
- Add error handling and resource management
- Add complexity analysis for streaming operations
- Add auto-optimization middleware
- Add comprehensive test suite
- Add performance benchmarks
- Add pipeline integration hooks
- Add configuration system

* docs: Add comprehensive incremental delivery documentation

- Complete usage guide with examples
- API reference for @defer and @stream directives
- Performance optimization guidelines
- Transport configuration details
- Troubleshooting and monitoring guidance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Correct Elixir syntax errors in incremental delivery implementation

- Fix Ruby-style return statements in auto_defer_stream middleware
- Correct Elixir typespec syntax in response module
- Mark unused variables with underscore prefix
- Remove invalid optional() syntax from typespecs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Update test infrastructure for incremental delivery

- Fix supervisor startup handling in tests
- Simplify test helpers to use standard Absinthe.run
- Enable basic test execution for incremental delivery features
- Address compilation issues and warnings

Tests now run successfully and provide baseline for further development.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Complete @defer and @stream directive implementation

This commit finalizes the implementation of GraphQL @defer and @stream directives
for incremental delivery in Absinthe:

- Fix streaming resolution phase to properly handle defer/stream flags
- Update projector to gracefully handle defer/stream flags without crashing
- Improve telemetry phases to handle missing blueprint context gracefully
- Add comprehensive test infrastructure for incremental delivery
- Create debug script for testing directive processing
- Add BuiltIns module for proper directive loading

The @defer and @stream directives now work correctly according to the GraphQL
specification, allowing for incremental query result delivery.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: Add comprehensive incremental delivery guide

Add detailed guide for @defer and @stream directives following
the same structure as other Absinthe feature guides.

Includes:
- Basic usage examples
- Configuration options
- Transport integration (WebSocket, SSE)
- Advanced patterns (conditional, nested)
- Error handling
- Performance considerations
- Relay integration
- Testing approaches
- Migration guidance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add incremental delivery guide to documentation extras

Include guides/incremental-delivery.md in the mix.exs extras list so it
appears in the generated documentation alongside other guides.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove automatic field description inheritance

Based on community feedback from PR absinthe-graphql#1373, automatic field description
inheritance was not well received. The community preferred explicit
field descriptions that are specific to each field's context rather
than automatically inheriting from the referenced type.

This commit:
- Reverts the automatic inheritance behavior in introspection
- Removes the associated test file
- Returns to the standard field description handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix code formatting

Run mix format to fix formatting issues detected by CI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix dialyzer

* remove elixir 1.19

* fix: resolve @defer/@stream incremental delivery issues

- Fix Absinthe.Type.list?/1 undefined function by using pattern matching
- Fix directive expand callbacks to return node directly (not {:ok, node})
- Add missing analyze_node clauses for Operation and Fragment.Named nodes
- Fix defer depth tracking for nested defers
- Fix projector to only skip __skip_initial__ flagged nodes, not all defer/stream
- Update introspection tests for new @defer/@stream directives
- Remove duplicate documentation files per PR review
- Add comprehensive complexity analysis tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: clarify supervisor startup and dataloader integration

Address review comments:
- Add detailed documentation on how to start the Incremental Supervisor
- Include configuration options and examples in supervisor docs
- Add usage documentation for Dataloader integration
- Explain how streaming-aware resolvers work with batching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove debug test file

* feat: add on_event callback for monitoring integrations

Add an `on_event` callback option to the incremental delivery system
that allows sending defer/stream events to external monitoring services
like Sentry, DataDog, or custom telemetry systems.

The callback is invoked at each stage of incremental delivery:
- `:initial` - When the initial response is sent
- `:incremental` - When each deferred/streamed payload is delivered
- `:complete` - When the stream completes successfully
- `:error` - When an error occurs during streaming

Each event includes payload data and metadata such as:
- `operation_id` - Unique identifier for tracking
- `path` - GraphQL path to the deferred field
- `label` - Label from @defer/@stream directive
- `duration_ms` - Time taken for the operation
- `task_type` - `:defer` or `:stream`

Example usage:

    Absinthe.run(query, schema,
      on_event: fn
        :error, payload, metadata ->
          Sentry.capture_message("GraphQL streaming error",
            extra: %{payload: payload, metadata: metadata}
          )
        _, _, _ -> :ok
      end
    )

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add telemetry events for incremental delivery instrumentation

Add telemetry events for the incremental delivery transport layer to
enable integration with instrumentation libraries like opentelemetry_absinthe.

New telemetry events:

- `[:absinthe, :incremental, :delivery, :initial]`
  Emitted when initial response is sent with has_next, pending_count

- `[:absinthe, :incremental, :delivery, :payload]`
  Emitted for each @defer/@stream payload with path, label, task_type,
  duration, and success status

- `[:absinthe, :incremental, :delivery, :complete]`
  Emitted when streaming completes successfully with total duration

- `[:absinthe, :incremental, :delivery, :error]`
  Emitted on errors with reason and message

All events include operation_id for correlation across spans.
Events follow the same pattern as existing Absinthe telemetry events
with measurements (system_time, duration) and metadata.

This enables opentelemetry_absinthe and other instrumentation libraries
to create proper spans for @defer/@stream operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add incremental delivery telemetry documentation

Update the telemetry guide to document the new @defer/@stream events:

- [:absinthe, :incremental, :delivery, :initial]
- [:absinthe, :incremental, :delivery, :payload]
- [:absinthe, :incremental, :delivery, :complete]
- [:absinthe, :incremental, :delivery, :error]

Includes detailed documentation of measurements and metadata for each
event, plus examples for attaching handlers and using the on_event
callback for custom monitoring integrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add incremental delivery to CHANGELOG

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: clarify @defer/@stream are draft/RFC, not finalized spec

The incremental delivery directives are still in the RFC stage and not
yet part of the finalized GraphQL specification. Updated documentation
to make this clear and link to the actual RFC.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: make @defer/@stream directives opt-in

Move @defer and @stream directives from core built-ins to a new
opt-in module Absinthe.Type.BuiltIns.IncrementalDirectives.

Since @defer/@stream are draft-spec features (not yet finalized),
users must now explicitly opt-in by adding:

    import_types Absinthe.Type.BuiltIns.IncrementalDirectives

to their schema definition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: fix formatting across incremental delivery files

Run mix format to fix whitespace and formatting issues that were
causing CI to fail.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci: restore Elixir 1.19 support

Restore Elixir 1.19 to the CI matrix to match upstream main.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: unify streaming architecture for subscriptions and incremental delivery

- Add Absinthe.Streaming module with shared abstractions
- Add Absinthe.Streaming.Executor behaviour for pluggable task execution
- Add Absinthe.Streaming.TaskExecutor as default executor (Task.async_stream)
- Add Absinthe.Streaming.Delivery for pubsub incremental delivery
- Enable @defer/@stream in subscriptions (automatic multi-payload delivery)
- Refactor Transport to use shared TaskExecutor
- Update Subscription.Local to detect and handle incremental directives
- Add comprehensive backwards compatibility tests
- Update guides and documentation

Subscriptions with @defer/@stream now automatically deliver multiple payloads
using the standard GraphQL incremental format. Existing PubSub implementations
work unchanged - publish_subscription/2 is called multiple times.

Custom executors (Oban, RabbitMQ, etc.) can be configured via:
- Schema attribute: @streaming_executor MyApp.ObanExecutor
- Context: context: %{streaming_executor: MyApp.ObanExecutor}
- Application config: config :absinthe, :streaming_executor, MyApp.ObanExecutor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract middleware and telemetry modules for better discoverability

- Move Absinthe.Middleware.IncrementalComplexity to its own file in lib/absinthe/middleware/
- Move Absinthe.Incremental.TelemetryReporter to its own file in lib/absinthe/incremental/
- Improves code organization and makes these modules easier to find

Addresses PR review feedback from @bryanjos

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@cschiewek
Copy link
Copy Markdown
Member

I'm in favour of adding the applied_directives field to all the type structs, but definitely against the new introspection types as they are not part of the spec. IMO, if folks want to extend the introspection schema, it should be a seperate package / plugin.

jwaldrip and others added 3 commits March 4, 2026 12:23
- Merge with origin/main (clean merge)
- Change schema_types_test to use membership assertions instead of
  exact list comparison, making it resilient to future type additions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove __AppliedDirective and __DirectiveArgument introspection types
and the appliedDirectives fields from __type, __field, __inputvalue,
and __enumvalue as these are not part of the GraphQL spec.

The applied_directives field on type structs (runtime data) is kept
as requested — this allows programmatic access to applied directives
without extending the introspection schema.

Addresses feedback from @cschiewek.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jwaldrip
Copy link
Copy Markdown
Contributor Author

jwaldrip commented Mar 4, 2026

@cschiewek Good call — removed the __AppliedDirective and __DirectiveArgument introspection types and the appliedDirectives fields from __type, __field, __inputvalue, and __enumvalue. Agreed these shouldn't extend the introspection schema beyond the spec.

The applied_directives field on the type structs (runtime) is kept so directives applied to types/fields/args are still accessible programmatically via Absinthe.Schema.lookup_type/2 and friends.

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.

Support TypeSystem Directives

2 participants