-
Notifications
You must be signed in to change notification settings - Fork 45
Schema DSL #15
Comments
Hi @1player, thanks so much for writing this up and starting this discussion! There has definitely been some thinking around this already, but it is probably one of the areas where there is a bit of confusion in the spec and the reference implementation. @peburrows, @freshtonic and I all separately assumed that one would specify the schema using the type system. Much like https://github.com/joshprice/graphql-elixir/blob/master/test/graphql_parser_schema_kitchen_sink_test.exs or https://github.com/peburrows/plot/blob/master/test/fixtures/schema/user-and-book.gql @peburrows solution was along the lines of what I was thinking which was to decouple the schema and type definitions from the method used to resolve the data. I think Phil has nailed this idea with his use of Protocols to separate out these concepts and this needs further exploration. See https://github.com/peburrows/plot#query-resolution That said, I think there is merit to the approach you've gone with not just because it's how the reference implementation does it. It's simple, easy to read, it's just Elixir code, you can keep resolvers along with the schema, squint a bit and it looks like ecto, etc. Although my preference is to go with using the schema DSL, I think it makes sense to enable both ways of defining a schema, and if there ends up being more consensus in the spec and other implementations then pick one way or the other. Thoughts? |
@joshprice What I would like to see is the ability to decouple the resolve blocks from the schema i.e. keep the what (the schema) separate from the how (the resolving). However, for convenience I'd still like the option to define the resolve blocks inline with the schema DSL. So far I like the syntax @1player has gone with - it's very tasteful! |
Agreed it looks like a great starting point |
In my mind, the ability to decouple the schema definition from how things are resolved seems important. |
(I prematurely submitted my previous comment 😢 Let me try that again.) In my mind, the ability to decouple the schema definition from how things are resolved seems important. However, I do appreciate the elegance of DSL @1player outlined above. It seems to me it would be useful to provide both options for schema definition, but maybe that just makes for unnecessary complexity here in the library (for the record, I still don't understand why more implementations don't allow people to specify schema using the type system — it's there for a reason, and schema definitions seems like one of those reasons...). |
I like the idea of using protocols for resolution, although I'm not sure sold on @peburrows solution which puts all the resolution code in a I'd like to defimpl each object separately, with the possibility of having a generic resolve function in the body of the schema which overrides per-object resolution. For example: defmodule Character, do: ...snip...
defmodule Human, do: ...snip...
# per-object resolution
defimpl GraphQL.Resolve, for: Human do
def resolve(%{id: id}) do
getHuman(id)
end
end
defmodule Schema do
[...]
field :hero, type: Character do
# generic resolution, overriding a possible GraphQL.Resolve implementation
# on the Character Object
resolve %{episode: episode} do
getHero(episode)
end
[...]
end
I actually don't know if we need per-object and generic resolution, but two different schemas may want to resolve the same object differently -- although I'm not sure how many projects will have more than a schema, or if that would even come up in practice. Makes any sense? |
Agree with the following:
Rather than speculate, I think the best course of action is to go with what the reference and other implementations have done (ie what @1player proposes) while investigating the alternatives. |
I just recently read the GraphQL spec, so I'm new, but it seems to me like using the existing type documentation would be best. Adding an Elixir layer over top of it adds a lot of complexity and requires more mental work to understand. Personally, I would rather have the schema look like it does in the GraphQL spec, even if it means that I have to hunt down the resolvers. This saves the mental effort and confusion of trying to figure out "How do I convert this type schema from the GraphQL docs into the correct Elixir modules?" and also all the effort of documenting how the type system maps to the Elixir module system. Just my two cents. |
One thing to mention about using the type system: as far as I can tell, it does not support adding descriptions (eg, as is done in graphql-js' |
@bruce That is a very good point. In fact, that could explain why all the other implementations have used a DSL. |
Given this extra information regarding descriptions, I'm leaning towards a DSL approach. However, if deemed valuable, we could also make a tool to translate a native GraphQL schema definition into our own particular DSL. Thoughts? |
Some things I've noticed working on an implementation of this feature:
defmodule Person do
use GraphQL.Object
field :name
field :age do
resolve, do: 28
end
end
### MAPS TO ###
defmodule Person do
defstruct [:name]
def age, do: 28
end
This is an arbitrary limitation of GraphQL IMO because as per spec objects can only implement Interfaces, and I don't see why an Object can't implement/derive another Object. We could be cool and permit that in this library, and collapse the object into an interface an introspection type, but that's for the future, if we'll ever go that route. For example, this could in theory be viable: defmodule Person do
use GraphQL.Object
field :name
end
defmodule Employee do
use GraphQL.Object, deriving: Person
field :salary
end represented as
|
Have you looked at graphql-ruby? I played around with a few hours and I found it was very pleasant to hook up with a Rails app. They have a demo app. It might serve as a source of inspiration. |
I really quite like how readable the DSL approach is. I do agree with @danielberkompas that it could cause problems with figuring out what's going on / debugging because it's so abstracted. Here's an approach i've gone with in JavaScript that just cuts down the noise and verbosity of the GraphQL exports. The main takeaway here is that it's the same as using the normal GraphQL exports but you're passing in the keys for the Anyhow thought i'd throw this in, just in case is sparks any ideas for Elixir use cases. Query = {};
Mutation = {};
Types = {}; import {object, string, date, list} from 'graphql-helpers'
Types.Post = object('Post', {
id: string(),
createdAt: date(),
body: string({desc: "The main part of a blog post"}),
title: string(),
comments: list(Types.Comment),
}) These functions are basically just taking in options, building up the const GraphHelpers = {
string(opts = {}) {
var type;
if (opts.required) {
type = {type: new GraphQLNonNull(GraphQLString)};
} else {
type = {type: GraphQLString };
}
return merge(opts, type)
},
} And here is the rest of the code (semi-related). The rest focuses on building up a Query and Mutation namespace and each 'chunk' is organized in a separate file (as opposed to the examples which have everything in one 'tree'). These are all glued together in the root query by passing // allow users to fetch a single post or all posts they own:
Query.usersPosts = {
type: listType(Types.Post),
description: "All posts that a user wrote",
args: {
userId: string({required: true}),
limit: integer(),
},
resolve(root, args) {
return Posts.find({userId: args.userId}, {limit: args.limit})
}
}; Mutation.createPost = {
type: Types.Post,
args: {
description: string({required: true}),
title: string({required: true}),
},
resolve: (root, args) => {
args.createdAt = new Date();
const id = Posts.insert(args);
return Posts.findOne(id);
}
}; |
I think this project and Absinthe should merge or atleast implement their DSL. Absinthe has the best DSL of all the elixir graphql implementations so far. For example, this is Absinthe: defmodule MyApp.Schema do
use Absinthe.Schema
# Example data
@items %{
"foo" => %{id: "foo", name: "Foo"},
"bar" => %{id: "bar", name: "Bar"}
}
@desc "An item"
object :item do
field :id, :id
field :name, :string
end
query do
field :item, type: :item do
arg :id, non_null(:id)
resolve fn %{id: item_id}, _ ->
{:ok, @items[item_id]}
end
end
end
end |
Closing as we'll be focussing on supporting schemas written in GraphQL IDL. See the following links for more detail:
It's possible that we may end up implementing this before 1.0 but it's not currently planned. |
I will be the bad guy here and say that I prefer the current way of defining schema. This allows me not to depend on creating modules for each types in the schema. One of the apps I am working on has dynamic types, which are created and destroyed at run time. The current method allows me to change the schema any time. If I want DSL, I will go with Absinthe. Or at least allow both ways. |
Good to know Son, dynamic types are an interesting use case that we'll On Fri, Jun 24, 2016 at 2:07 AM, Son Tran-Nguyen [email protected]
|
Sure. My app takes a type definition list in JSON format, without resolvers. Later on, that JSON list can change, in which types can be added or With modules, I would need to rewrite and recompile them. Of course, I would need to use a new plug on top of |
I've been lurking in this repo for a while and I've tried to come up with some kind of DSL to describe a schema at a high level. Has anybody already given some thought about this?
I've tried to replicate the Star Wars example from the reference graphql repository from Facebook (https://github.com/graphql/graphql-js/tree/master/src/__tests__), this is what I've come up with:
The Schema module defines the schema with its field. Each field has some arguments and one or more resolve functions, depending on whether some arguments are nullable.
Fields and arguments accept a "type" option, and nullability is expressed with a "null: false" option.
Interfaces are defined by using
GraphQL.ObjectInterface
, with a similar DSL to define the fields, while objects deriving the interface specify doing so when "using"GraphQL.Object
(see Human and Droid).Modules importing
GraphQL.Object
behave very similarly to structs, and inherit all the fields defined in the parent interface, if any.That's just a proof of concept but I'd like to hear some comments and if there's any idea somebody's written down for a DSL.
Disclaimer: I'm quite new with Elixir, there surely are syntax errors in the POC code above.
The text was updated successfully, but these errors were encountered: