Rails jsonb columns in graphql - ruby-on-rails

I am trying to set up types for my 'A' model that uses store_accessor. I am not sure how to define the column that has the JSON column - :foo
class A < ActiveRecord
store_accessor :foo, :bar, :baz
end
Types::AType = GraphQL::ObjectType.define do
name ‘A’
field: id, !types.ID
field :bar, !types.String, hash_key: :bar
field :baz, !types.String, hash_key: :baz
end

Create a custom ScalarType called JSONType
JSONType = GraphQL::ScalarType.define do
name "JSON Type"
coerce_input -> (value) { JSON.parse(value) }
coerce_result -> (value) { value }
end
And consume it in ObjectType definition
field :foo, JSONType do
resolve Resolvers::Foo.new
end

Related

In GraphQL/Rails how to pass arguments in the query_type?

So I'm trying to query on a single user within the database but end up getting:
"Field 'userSingle' doesn't accept argument 'first_name'"
I'm getting that in GraphiQL when I run the following query:
query GetSingleUser {
userSingle(first_name: "Test"){
first_name
last_name
}
}
In my query_type.rb I have the following:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {
argument :first_name, !types.String
argument :id, types.ID
User.find(id: args[:id])}
end
Originally I had:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {User.find(id: args[:id])}
end
Same issue. If I take out the id: same issue. Also the same issue with:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {
argument :first_name, !types.String
argument :id, types.ID
user = User.find_by(
id: args[:id],
first_name: args[:first_name])
}
end
You could create a user_type.rb file with the following:
class Types::UserType < Types::BaseObject
description "A user object"
field :id, Integer, null: false
field :first_name, String, null: false
end
Then have the following in query_type.rb file:
module Types
class QueryType < Types::BaseObject
...
# User query
field :user, UserType, null: true do
description "Find a user by first_name"
argument :first_name, String, required: true
end
def user(first_name:)
User.find_by(first_name: first_name)
end
end
end
With all this in place, the query should then look like this:
{
user(first_name: "name") {
id
firstName: first_name
}
}
Instead of, !types[Types::UserType] in query_type file to
field :userSingle do
type Types::UserType
argument
resolve
..
end

graphql-ruby. Use (not Relay) mutations DRY. With or without GraphQL::Function?

I'M NOT USING RELAY.
I have read some tutorials. Many use this way for mutations:
app/graphql/graphql_tutorial_schema.rb
GraphqlTutorialSchema = GraphQL::Schema.define do
query(Types::QueryType)
mutation(Types::MutationType)
end
app/graphql/resolvers/create_link.rb
class Resolvers::CreateLink < GraphQL::Function
argument :description, !types.String
argument :url, !types.String
type Types::LinkType
def call(_obj, args, _ctx)
Link.create!(
description: args[:description],
url: args[:url],
)
end
end
and finally they have:
app/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do
name 'Mutation'
field :createLink, function: Resolvers::CreateLink.new
end
So they are using a GraphQL::Function.
Is this the way to go? If I'm not using Relay this is just the only way to go?
And what if I want a unique file for all link operations (CRUD)?
Other tutorials (http://tech.eshaiju.in/blog/2017/05/15/graphql-mutation-query-implementation-ruby-on-rails/) use this:
app/graphql/mutations/comment_mutations.rb
module CommentMutations
Create = GraphQL::Relay::Mutation.define do
name "AddComment"
# Define input parameters
input_field :articleId, !types.ID
input_field :userId, !types.ID
input_field :comment, !types.String
# Define return parameters
return_field :article, ArticleType
return_field :errors, types.String
resolve ->(object, inputs, ctx) {
article = Article.find_by_id(inputs[:articleId])
return { errors: 'Article not found' } if article.nil?
comments = article.comments
new_comment = comments.build(user_id: inputs[:userId], comment: inputs[:comment])
if new_comment.save
{ article: article }
else
{ errors: new_comment.errors.to_a }
end
}
end
end
and app/graphql/mutations/mutation_type.rb
MutationType = GraphQL::ObjectType.define do
name "Mutation"
# Add the mutation's derived field to the mutation type
field :addComment, field: CommentMutations::Create.field
end
so I can add also:
MutationType = GraphQL::ObjectType.define do
name "Mutation"
field :addComment, field: CommentMutations::Create.field
field :updateComment, field: CommentMutations::Update.field
field :deleteComment, field: CommentMutations::Delete.field
end
But this works good just with Create = GraphQL::Relay::Mutation.define: I'm not using Relay!
In your docs I find nothing related to this problem.
I have to always use GraphQL::Functions?
Or maybe I can use it this way:
MutationType = GraphQL::ObjectType.define do
name "Mutation"
field :addComment, field: CommentMutations::Create
field :updateComment, field: CommentMutations::Update
field :deleteComment, field: CommentMutations::Delete
end
and have this (code is an example):
module Mutations::commentMutations
Createcomment = GraphQL::ObjectType.define do
name "Createcomment"
input_field :author_id, !types.ID
input_field :post_id, !types.ID
return_field :comment, Types::commentType
return_field :errors, types.String
resolve ->(obj, inputs, ctx) {
comment = comment.new(
author_id: inputs[:author_id],
post_id: inputs[:post_id]
)
if comment.save
{ comment: comment }
else
{ errors: comment.errors.to_a }
end
}
end
Updatecomment = GraphQL::ObjectType.define do
name "Updatecomment"
input_field :author_id, !types.ID
input_field :post_id, !types.ID
return_field :comment, Types::commentType
return_field :errors, types.String
resolve ->(obj, inputs, ctx) {
comment = comment.new(
author_id: inputs[:author_id],
post_id: inputs[:post_id]
)
if comment.update
{ comment: comment }
else
{ errors: comment.errors.to_a }
end
}
end
end
Is this another way?
You should try https://github.com/samesystem/graphql_rails gem. It has MVC structure on graphql side, so your GraphQL will be almost the same as your RoR code.
And what if I want a unique file for all link operations (CRUD)?
GraphqlRails has controllers instead of resolvers. You could have something like this:
class CommentsController < GraphqlRails::Controller
action(:create).permit(:article_id, :body).returns(!Types::CommentType)
action(:update).permit(:id, :body).returns(!Types::CommentType)
def create
Comment.create!(params)
end
def update
Comment.find(params[:id]).update!(params)
end
end
There is another method I've been utilizing recently. We also do not use React, and it seemed odd to be using GraphQL::Relay::Mutation.define for describing mutations.
Instead we describe the fields. (for example: app/graphql/mutations/create_owner.rb)
Mutations::CreateOwner = GraphQL::Field.define do
name 'CreateOwner'
type Types::OwnerType
description 'Update owner attributes'
argument :name, !types.String
argument :description, types.String
resolve ->(_obj, args, _ctx) do
Owner.create!(args.to_h)
end
end
Then in your app/graphql/types/mutation_type.rb you add:
field :createOwner, Mutations::CreateOwner
This can be refactored further by extracting resolvers into their own resolver classes.
Without some defined best practices that I've been able to locate, this has been a pretty clean way of dealing with this issue.
Heres what mine currently look like:
blah_schema.rb
BlahSchema = GraphQL::Schema.define do
...
query(Types::QueryType)
mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do
name "Mutation"
field :comment, !Types::CommentType do
argument :resource_type, !types.String
argument :resource_id, !types.ID
argument :comment, !types.String
resolve ResolverErrorHandler.new ->(obj, args, ctx) do
ctx[:current_user].comments.
create!(resource_id: args[:resource_id],
resource_type: args[:resource_type],
comment: args[:comment])
end
end
field :destroy_comment, !Types::CommentType do
argument :id, !types.ID
resolve ResolverErrorHandler.new ->(obj, args, ctx) do
comment = ctx[:current_user].comments.where(id: args[:id]).first
if !comment
raise ActiveRecord::RecordNotFound.new(
"couldn't find comment for id #{args[:id]} belonging to #{current_user.id}")
end
comment.destroy!
comment
end
end
end
resolver_error_handler.rb
class ResolverErrorHandler
def initialize(resolver)
#r = resolver
end
def call(obj, args, ctx)
#r.call(obj, args, ctx)
rescue ActiveRecord::RecordNotFound => e
GraphQL::ExecutionError.new("Missing Record: #{e.message}")
rescue AuthorizationError => e
GraphQL::ExecutionError.new("sign in required")
rescue ActiveRecord::RecordInvalid => e
# return a GraphQL error with validation details
messages = e.record.errors.full_messages.join("\n")
GraphQL::ExecutionError.new("Validation failed: #{messages}")
rescue StandardError => e
# handle all other errors
Rails.logger.error "graphql exception caught: #{e} \n#{e.backtrace.join("\n")}"
Raven.capture_exception(e)
GraphQL::ExecutionError.new("Unexpected error!")
end
end
So yes it is different - I'm not sure it's better, it's just what i came up with. My mutation_type.rb is a lot fatter which i don't like.
You didn't clearly spell out any goals or problems so that might help you get a more specific answer.

Default value for Mongoid Hash field accessor

Given a Mongoid model:
class Counts
include Mongoid::Document
# lists of tag counts
field :tags, type: Hash, default: {}
end
c = Counts.new( tags = {new: 12, old: 7})
I would like to override c#tags[] so that if a key isn't set on the tags field, it should return a default of 0 instead of nil like this:
c.tags['ancient']
# => 0
Try setting default hash values as below:
class Counts
...
field :tags, type: Hash, default: Hash.new{ |h, k| h[k] = 0 }
end

rails elastic search relationship attributes not indexed

Basically I got 3 models(Book,Chapter,Author), and I want to include some of the books and author attributes when indexing chapter.
here is my Chapter.rb
class Chapter < ActiveRecord::Base
belongs_to :book, :counter_cache => true
include Elasticsearch::Model
index_name [Rails.env, model_name.collection.gsub(/\//, '-')].join('_')
mappings do
indexes :id, type: :integer
indexes :title, type: :string
indexes :description, type: :string
indexes :content, type: :string
indexes :updated_at, type: :date # Date example
indexes :book_title
indexes :book_type
indexes :author_name
indexes :book_id
end
def book_title
book.title
end
def book_type
book.book_type
end
def author_name
" #{book.author.firstname} #{book.author.lastname} "
end
def to_indexed_json
to_json methods: [:book_title, :book_type, :author_name]
end
end
http://localhost:9200/development_chapters/_mapping?pretty shows correct mapping
{
"development_chapters" : {
"mappings" : {
"chapter" : {
"properties" : {
"author_name" : {
"type" : "string"
},
"book_title" : {
"type" : "string"
},....
}
}
}
}
}
Then why do I not get author_name, book_title etc... in the search results
<Elasticsearch::Model::Response::Result:0x00000105e393a0 #result=#<Hashie::Mash _id="415" _index="development_chapters" _score=1.0 _source=#<Hashie::Mash book_id=153 content="[\"Explicabo accusantium odit .\"]" created_at="2015-04-22T18:43:58.586Z" description="You can't generate the application without quantifying the cross-platform SDD bandwidth!" id=415 title="Future Communications Orchestrator" updated_at="2015-04-22T18:43:58.586Z"> _type="chapter">>
You are defining wrong serialization method. Elasticsearch::Model searches for method as_indexed_json and you are defining to_indexed_json. In elasticesearch-model gem you can find examples https://github.com/elastic/elasticsearch-rails/blob/master/elasticsearch-model/examples/activerecord_associations.rb#L82
It should look something like this:
def as_indexed_json(options = {})
as_json methods: [:book_title, :book_type, :author_name]
end

Have to_json return a mongoid as a string

In my Rails API, I'd like a Mongo object to return as a JSON string with the Mongo UID as an "id" property rather than as an "_id" object.
I want my API to return the following JSON:
{
"id": "536268a06d2d7019ba000000",
"created_at": null,
}
Instead of:
{
"_id": {
"$oid": "536268a06d2d7019ba000000"
},
"created_at": null,
}
My model code is:
class Profile
include Mongoid::Document
field :name, type: String
def to_json(options={})
#what to do here?
# options[:except] ||= :_id #%w(_id)
super(options)
end
end
You can monkey patch Moped::BSON::ObjectId:
module Moped
module BSON
class ObjectId
def to_json(*)
to_s.to_json
end
def as_json(*)
to_s.as_json
end
end
end
end
to take care of the $oid stuff and then Mongoid::Document to convert _id to id:
module Mongoid
module Document
def serializable_hash(options = nil)
h = super(options)
h['id'] = h.delete('_id') if(h.has_key?('_id'))
h
end
end
end
That will make all of your Mongoid objects behave sensibly.
For guys using Mongoid 4+ use this,
module BSON
class ObjectId
alias :to_json :to_s
alias :as_json :to_s
end
end
Reference
You can change the data in as_json method, while data is a hash:
class Profile
include Mongoid::Document
field :name, type: String
def as_json(*args)
res = super
res["id"] = res.delete("_id").to_s
res
end
end
p = Profile.new
p.to_json
result:
{
"id": "536268a06d2d7019ba000000",
...
}
Use for example:
user = collection.find_one(...)
user['_id'] = user['_id'].to_s
user.to_json
this return
{
"_id": "54ed1e9896188813b0000001"
}
class Profile
include Mongoid::Document
field :name, type: String
def to_json
as_json(except: :_id).merge(id: id.to_s).to_json
end
end
If you don't want to change default behavior of MongoId, just convert result of as_json.
profile.as_json.map{|k,v| [k, v.is_a?(BSON::ObjectId) ? v.to_s : v]}.to_h
Also, this convert other BSON::ObjectId like user_id.
# config/initializers/mongoid.rb
# convert object key "_id" to "id" and remove "_id" from displayed attributes on mongoid documents when represented as JSON
module Mongoid
module Document
def as_json(options={})
attrs = super(options)
id = {id: attrs["_id"].to_s}
attrs.delete("_id")
id.merge(attrs)
end
end
end
# converts object ids from BSON type object id to plain old string
module BSON
class ObjectId
alias :to_json :to_s
alias :as_json :to_s
end
end

Resources