Skip to content

Thought: avoiding self-repitition when a schema is defined as a string #216

Closed
@kachkaev

Description

@kachkaev

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions