Active Record Source
The gem comes with full support for Active Record, which means that one line can create all the necessary components for you to perform any CRUD operation on them.
N+1 Protected By default, associations will be properly loaded once during the
prepare
phase of the request.
Setup
You can define Active Record sources on a file or using the shortcut on the schema.
# app/graphql/sources/user_source.rb
module GraphQL
class UserSource < GraphQL::Source::ActiveRecordSource
# OR
class UserSource < GraphQL::ARSource
build_all
end
end
# OR
# app/graphql/app_schema.rb
source User
This will scaffold the following components:
type User {
id: ID!
email: String!
# ... any other attributes and associations
}
input UserInput {
id: ID
email: String!
# ... any other attributes and associations with accepts nested attributes
_delete: Boolean = false # For the inverted reason of the above
}
type _Mutation {
createUser(user: UserInput!): User!
deleteUser(id: ID!): Boolean!
updateUser(id: ID!, user: UserInput!): User!
}
type _Query {
user(id: ID!): User!
users: [User!]!
}
Read more about inline sources.
Settings
Here are all the settings that you can add to your sources:
with_associations
true
- Marks if fields related to associations should be added.errors_to_extensions
false
- Marks if errors should be exported to theextensions
of the response.
Possible values:false
,:details
,:messages
act_as_interface
false
- Marks if the source should build an interface instead of an object.
Behaviors
Here are all the behaviors of an Action Record source and how you can take advantage of them:
Fields
Here is some important information about the fields created automatically and what
events are added to them in what order (examples based on the UserSource
):
query users: [User!]!
Uses the plural
name of the model for the field name and resolves to load_records
.
query user(id: ID!): User!
Uses the singular
name of the model for the field name and resolves to load_record
.
mutation createUser(user: UserInput!): User!
Uses the singular
name of the model with create
, has a singular
argument with the proper
input type, performs create_record
, and resolves to the created record.
mutation updateUser(id: ID!, user: UserInput!): User!
Uses the singular
name of the model with update
, has an id
argument and a singular
argument
with the proper input type, prepares with load_record
, performs update_record
,
and resolves to the updated record.
mutation deleteUser(id: ID!, user: UserInput!): Boolean!
Uses the singular
name of the model with delete
, has an id
argument, prepares with load_record
,
performs destroy_record
, and resolves to either true
or false
.
Assignment
Sources take a huge advantage of type assignment. That said, the source will assume that its name is a reference to a model and make an automatic assignment to that. You can override that and manually set the assignment:
# app/graphql/sources/user_source.rb
class GraphQL::UserSource < GraphQL::ARSource
# Thus automatically assign it to User, the same as
self.assigned_to = 'User'
Interface
By default, the source would create an object to represent its values.
However, if you don’t set act_as_interface
, the source can still identify that it should
behave as an interface when it has an inheritance column (aka type
), and the assigned
class is the base class of such inheritance. This is an automatic support for
Single table inheritances.
Enums
The source will attempt to create enums for each attribute on the model that
is marked as an enum. In case of any conflict, it will threaten the attribute as their plain type.
You can skip this process by simply calling disable :enums
.
Associations
This source takes advantage of the Type Map hooks to lazy add fields into both the object/interface and the input that is related to associations. Here is an example:
# app/models/user.rb
class User < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, allow_destroy: true
end
# app/models/address.rb
class Address < ApplicationRecord
belongs_to :user
end
# app/graphql/sources/user_source.rb
class GraphQL::UserSource < GraphQL::ARSource
build_all
end
# app/graphql/sources/address_source.rb
class GraphQL::AddressSource < GraphQL::ARSource
build_all
end
type Address {
id: ID!
line1: String!
userId: ID!
user: User!
_delete: Boolean = false
}
type User {
id: ID!
email: String!
addresses: [Address!]
_delete: Boolean = false
}
type AddressInput {
id: ID
line1: String!
userId: ID!
}
type UserInput {
id: ID
email: String!
addressesAttributes: [AddressInput!]
}
Warning Polymorphic associations are ignored.
Proxy Field
An interesting thing to notice is that collection associations, those with has_many
, will attempt
to proxy the collection field from the other source. By doing that, you can share common
arguments and features, like scoped arguments, between them.
Here is what may happen:
type _Query {
# This is the original field
addresses(primary: Boolean! = false): [Addresses!]!
}
type User {
# This is the proxy field, with the same arguments as its companion
addresses(primary: Boolean! = false): [Addresses!]!
}
Important This is an experimental feature and may change in the future.
Proxy fields is an advanced feature. Read more about proxy fields adn scoped arguments.
Errors
When you use the errors_to_extensions
setting, whenever the default mutations
are not successful, it will expose the
Errors
to the extensions
portion of the result. It will include
the operation name, if any, and the field name or alias. See an example:
mutation SingUp($user: UserInput!) {
createUser(user: $user) { id }
}
When set to :details
, it uses
details
:
{
"data": {},
"errors": [
{
"message": "Validation failed: Email can't be blank",
"path": ["SingUp", "createUser"],
"extensions": {
"stage": "prepare",
"exception": "ActiveRecord::RecordInvalid"
}
}
],
"extensions": {
"SingUp": {
"createUser": { "email": [{ "error": "blank" }] }
}
}
}
When set to :messages
, it uses
as_json
:
{
// ...
"extensions": {
"SingUp": {
"createUser": { "email": ["can't be blank"] }
}
}
}
Note If you decide to use this with
accepts_nested_attributes_for
for ahas_many
association, it is recommended to enableconfig.active_record.index_nested_attribute_errors = true
to get better results.
Methods
Here is a list of methods that you can use and rely on to facilitate the usage with your models:
load_records(scope = nil)
Responsible for loading several records from the model during the prepare
stage.
The scope
will be either the event’s last_result
or default_scoped
.
load_record(scope = nil, find_by: nil)
Responsible for loading one record from the model during the prepare
stage.
The scope
will be either the event’s last_result
or default_scoped
.
If find_by
is not provided, it assumes { primary_key => argument(primary_key) }
.
create_record
It will call save!
from input_argument
.
Read more about inputs assignment.
update_record
It will call update!
from the current record with params from input_argument
.
destroy_record
It will call destroy!
from the current record.
build_association_scope(association)
This is the first step to loading an association, which will get the proper scope for the association. Read more about events to see how you can build on top of this method.
preload_association(association, scope = nil)
This is the second step to loading an association, which will use Active Record internal
components to properly load the records and make them available for the next step. If the
scope
is not provided, it will use the value from the above method.
parent_owned_records(collection_result = false)
This is the last step to loading an association, which will use the preloaded data and get the relevant records for the current record.
errors_to_extensions(errors, path = nil, format = nil)
This method is responsible for delivering the errors behavior. You can still
use it regardless of the errors_to_extensions
setting by simply passing the format
you want.
You can also override the path
to which the errors will be added to.
input_argument
This method gets the proper input associated with the attributes of the record from the list of arguments of the field. You can use it to manipulate the attributes that will be saved.
Adapters
Here is the list of supported adapters and their respective mapping of types: