Expose - Automatic Query Field Generation
View SourceGreenFairy's expose macro automatically generates query fields for fetching types by their fields. This eliminates boilerplate and provides a consistent pattern for object fetching.
Quick Start
Add expose :id to your type to auto-generate a query field:
defmodule MyApp.GraphQL.Types.User do
use GreenFairy.Type
type "User", struct: MyApp.Accounts.User do
# Auto-generates: query { user(id: ID!): User }
expose :id
field :id, non_null(:id)
field :email, non_null(:string)
field :name, :string
end
endThat's it! The schema will automatically include a user(id: ID!) query field that:
- Decodes the GlobalId
- Fetches the record from your configured repo
- Returns the user or an error
How It Works
When you add expose :id to a type:
- Field Type Detection: GreenFairy looks up the field type from your struct's adapter (Ecto schema, etc.)
- Query Field Generation: A query field is auto-generated with the appropriate argument type
- GlobalId Handling: For
:idfields, the GlobalId is automatically decoded - Database Fetching: Records are fetched using your schema's configured repo
Configuration
Schema Setup
Configure your repo in the schema:
defmodule MyApp.GraphQL.Schema do
use GreenFairy.Schema,
query: MyApp.GraphQL.RootQuery,
repo: MyApp.Repo
endExposing Multiple Fields
You can expose multiple fields on a type:
type "User", struct: MyApp.Accounts.User do
expose :id # Generates: user(id: ID!): User
expose :email # Generates: userByEmail(email: String!): User
expose :username # Generates: userByUsername(username: String!): User
field :id, non_null(:id)
field :email, non_null(:string)
field :username, non_null(:string)
endCustom Field Names
Override the generated query field name with :as:
type "User", struct: MyApp.Accounts.User do
expose :id, as: :get_user # Generates: getUser(id: ID!): User
expose :email, as: :find_by_email # Generates: findByEmail(email: String!): User
field :id, non_null(:id)
field :email, non_null(:string)
endField Type Inference
The argument type is automatically inferred from your Ecto schema:
| Ecto Type | GraphQL Arg Type |
|---|---|
:id | :id |
:integer | :integer |
:string | :string |
:boolean | :boolean |
Ecto.UUID | :id |
| Other | :string |
Generated Query Behavior
For :id Fields
When exposing :id, the generated resolver:
- Decodes the GlobalId to extract the local ID
- Calls
Repo.get(StructModule, local_id) - Returns
{:ok, record}or{:error, "TypeName not found"}
query {
user(id: "VXNlcjoxMjM=") { # GlobalId for User:123
id
email
name
}
}For Other Fields
When exposing non-id fields, the generated resolver:
- Uses the raw argument value
- Calls
Repo.get_by(StructModule, field: value) - Returns
{:ok, record}or{:error, "TypeName not found"}
query {
userByEmail(email: "jane@example.com") {
id
email
name
}
}Combining with Query Modules
Exposed fields work alongside custom query fields:
# Types expose themselves
type "User", struct: MyApp.Accounts.User do
expose :id # Auto-generates user(id:)
# ...
end
# Query module handles complex queries
defmodule MyApp.GraphQL.RootQuery do
use GreenFairy.Query
alias MyApp.GraphQL.Types
alias MyApp.GraphQL.Inputs
queries do
# Relay node field
node_field()
# List queries with filters (not auto-generated)
field :users, list_of(Types.User) do
arg :filter, Inputs.UserFilter
resolve &MyApp.Resolvers.User.list/3
end
# Complex search queries
field :search_users, list_of(Types.User) do
arg :query, non_null(:string)
resolve &MyApp.Resolvers.User.search/3
end
end
endMemory Adapter Support
For types backed by plain structs (not Ecto schemas), the Memory adapter provides basic field type inference:
defmodule MyApp.Config do
defstruct [:id, :name, :value]
end
type "Config", struct: MyApp.Config do
expose :id # Works with Memory adapter
expose :name
field :id, non_null(:id)
field :name, non_null(:string)
field :value, :string
endNote: Memory adapter types require custom resolvers since there's no database to fetch from.
Best Practices
- Use
expose :idfor primary lookups - Every type that can be fetched should expose its ID - Expose unique fields only - Only expose fields that uniquely identify a record
- Use Query modules for lists - List queries with filters should be in your Query module
- Combine with Node interface - Types with
expose :idshould also implement the Node interface
Example: Complete Type
defmodule MyApp.GraphQL.Types.Post do
use GreenFairy.Type
alias MyApp.GraphQL.Interfaces
alias MyApp.GraphQL.Types
type "Post", struct: MyApp.Content.Post do
implements Interfaces.Node
# Expose for fetching
expose :id
expose :slug, as: :post_by_slug
# Fields - all auto-resolved from struct
field :id, non_null(:id)
field :title, non_null(:string)
field :slug, non_null(:string)
field :body, non_null(:string)
field :published, non_null(:boolean)
field :inserted_at, non_null(:naive_datetime)
# Associations - auto batch-loaded
field :author, Types.User
field :comments, list_of(Types.Comment)
end
endThis generates:
post(id: ID!): Post- Fetch by GlobalIdpostBySlug(slug: String!): Post- Fetch by slug
Complete Query Pattern
GreenFairy provides three complementary macros for query fields:
| Macro | Purpose | Where Defined |
|---|---|---|
expose :id | Single record by ID | In the type definition |
list :users | Flat list with CQL filters | In the Query module |
connection :users | Paginated list with CQL | In the Query module |
Example: Complete Setup
# Type defines expose for single-record lookup
defmodule MyApp.GraphQL.Types.User do
use GreenFairy.Type
type "User", struct: MyApp.Accounts.User do
implements MyApp.GraphQL.Interfaces.Node
# Auto-generates: user(id: ID!): User
expose :id
field :id, non_null(:id)
field :email, non_null(:string)
field :name, :string
end
end
# Query module defines list/connection for collection queries
defmodule MyApp.GraphQL.RootQuery do
use GreenFairy.Query
alias MyApp.GraphQL.Types
queries do
node_field()
# NOTE: user(id:) is auto-generated from the type's expose :id
# No need to define it here!
# Flat list with CQL filtering
list :users, Types.User
# Or paginated connection
connection :users_paginated, Types.User
end
endThis generates:
type Query {
# From expose :id
user(id: ID!): User
# From node_field()
node(id: ID!): Node
# From list :users
users(where: CqlFilterUserInput, orderBy: [CqlOrderUserInput]): [User]
# From connection :users_paginated
usersPaginated(
first: Int
after: String
last: Int
before: String
where: CqlFilterUserInput
orderBy: [CqlOrderUserInput]
): UserConnection
}See Also
- Operations Guide -
list,connection, andnode_field()macros - Relay Guide - Full Relay specification support
- Global ID Guide - Custom GlobalId implementations