Description
I quite like the idea of defining schemas simply as strings – it's great that it can be a part of the git repo in such a form. This makes the schema readable by any member of the team without running a server and also enables the new features to be tracked even by those who is not great in server-side JS (git diff
for a spec looks pretty straightforward).
A problem being introduced by this approach is that some parts of the schema become a bit more verbose than needed, e.g. when some set of fields is shared between an interface and derived types:
export default /* GraphQL */`
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
`;
When I wrote my first schema in a text form, I intuitively expected this to work:
export default /* GraphQL */`
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
primaryFunction: String
}
`;
As probably guessed, this was not very successful :–) BTW /* GraphQL */
just helped Atom's language-babel
plugin nicely highlight the types.
On the yesterday's GraphQL meetup in London I shortly discussed this problem of self-repetition with @martijnwalraven and he explained me why it cannot be solved with the concept fragments. Turns out fragments are designed to be used only on the client and their main purpose is not to reduce the number of the lines of code, but to perform some cool magic.
However, I still think that there's something that may be done in the server-side app to avoid too much repetition. It does not necessary have to be a part of the GraphQL spec – just some basic ‘syntax sugar’ will do the job (our purpose is just to make the schema definition readable by everyone including product managers).
The first thing that comes to mind is some form of a ES6 spread operator (which implements a simple string.replace()
under the hood). A quick sketch:
import withSchemaMixin from 'graphql-tools/with-schema-mixin';
const withCharacterFields = withSchemaMixin('characterFields', /* GraphQLMixin */`
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
`);
export default withCharacterFields/* GraphQLWithMixins */`
interface Character {
...characterFields
}
type Human implements Character {
...characterFields
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
...characterFields
primaryFunction: String
}
`;
This may work not just for the sets of fields, but also for certain collections of commonly used attributes. Mixins (or whatever this thing may be called) can be easily made composable.
import composeSchemaMixins from 'graphql-tools/compose-schema-mixins';
import withSchemaMixin from 'graphql-tools/with-schema-mixin';
const withLocaleArgument = withSchemaMixin('localeArgument', /* GraphQLMixin */`
locale: Locale = EN
`);
const withTimeIntervalArguments = withSchemaMixin('timeIntervalAruments', /* GraphQLMixin */`
from: Timestamp,
to: Timestamp
`);
const withProductModelFilterArguments = withSchemaMixin('productModelFilterArguments', /* GraphQLMixin */`
manufacturerIds: [ID!],
departmentIds: [ID!],
tagIds: [ID!]
`);
const withStandardEntityFields = withSchemaMixin('standardEntityFields', /* GraphQLMixin */`
createdAt: Timestamp!
modifiedAt: Timestamp
isDeleted: Boolean
name(...locale): String
`);
export default composeSchemaMixins(
withStandardEntityFields,
withLocaleArgument,
withTimeIntervalArguments,
withProductModelFilterArguments,
)/* GraphQLWithMixins */`
scalar Timestamp
enum Locale {
DE
EN
RU
}
type ProductModel {
...standardEntityFields
}
type RetailPoint {
...standardEntityFields
description(...locale): String
productModelsInStock(...productModelFilter): [ProductModel!]!
productModelsInStockCount(...productModelFilter): Int
}
type Customer {
...standardEntityFields
name: String
productModelsInCart(...productModelFilter): [ProductModel!]!
productModelsInCartCount(...productModelFilter): Int!
productModelsOrdered(...timeIntervalFilter, ...productModelFilter): [ProductModel!]!
productModelsOrderedCount(...timeIntervalFilter, ...productModelFilter): Int!
#...
}
`;
Thinking further, these mixins may be made smarter than just bare-bone string.replace()
and even work as true spread operators. In type Customer
name
would turn from a locale-aware filed to a normal one. But that's probably a bit too hard conceptually for a start.
Although I see certain benefits in this extra layer of abstraction, I understand that it comes at a cost, which is first of all the existence of the layer of abstraction as such. Besides, with the introduction of these mixins in the schema definition file, the resolvers might need to get something similar as well.
However, at the moment I see more pros than cons in the overall idea, especially in the long run. As the GraphQL tooling improves further, server developers will probably get more focused on the schema than on the underlying layer with resolvers, so keeping things shorter and thus more human-readable can help many teams.
I'm curious to know what the community thinks of this extra ‘syntax-sugar’ idea for Apollo's graphql-server
. Feel free to criticize it as much as you want – it's pretty raw after all! :–)
This issue was originally opened in apollographql/apollo-server#230 and was moved to this repo as a more relevant place, according to @stubailo.