• Stars
    star
    207
  • Rank 189,769 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 8 years ago
  • Updated about 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Use the GraphQL schema language and a small set of directives to define an API with a PostgreSQL backend declaratively, really quickly, and with a tiny amount of code.

Gestalt

Gestalt lets you use the GraphQL schema language and a small set of directives to define an API with a PostgreSQL backend declaratively, really quickly, and with a tiny amount of code.

Build Status

GraphQL schema language

The GraphQL schema language (also called IDL for interface definition language) is a proposed addition to the GraphQL spec adding a shorthand to describe types in a GraphQL schema. While it isn't yet officially part of the spec, the reference implementation of GraphQL already includes a parser for the IDL, and if you have spent much time with the GraphQL docs you have probably already seen it. It looks like this:

type Human {
  id: String!
  name: String
  age: Int
}

The Schema Language can be used to define the types in a schema: Objects, Enums, Interfaces, etc., but it doesn't cover resolution. To actually create a usable GraphQL API that can load your data you end up writing a lot of code like this:

import {
  GraphQLInt,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLString
} from 'graphql';

export default new GraphQLObjectType({
  name: 'Human',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLString),
      resolve(obj) {
        return obj.id;
      }
    },
    name: {
      type: GraphQLString,
      resolve(obj) {
        return obj.name;
      }
    },
    age: {
      type: GraphQLInt,
      resolve(obj) {
        return obj.age;
      }
    }
  }
});

It would be nice to just write the first thing! If you use Gestalt and are willing to accept some reasonable defaults, you can - gestalt understands how your objects are related and is able to define the resolution for you.

Gestalt is designed to make it really easy for small teams of 1-10 developers to build GraphQL APIs quickly. It's also designed not to lock you in - you can build an API with Gestalt, make changes quickly, and drop down to javascript whenever you need to do something your own way.

Getting started with Gestalt

If you want to jump straight to the code, here is an example project. If you want a hands on introduction, this step by step tutorial will walk you through creating a new project.

Writing a schema

Gestalt apps are based on a schema.graphql file you write using the IDL. Gestalt defines the base mutation and query types, the Relay Node interface, and a few directives and additional scalar types for you, so in schema.graphql, you only define types specific to your app.

Any Objects you define implementing the Node interface result in database tables. Other objects and arrays they reference are stored in PostgreSQL as JSON, and relationships between nodes are specified with directives.

Object relationships

Gestalt needs information about the relationships between objects to generate a database schema and efficient queries. You provide this using the @relationship directive and a syntax inspired by Neo4j's Cypher query language.

type User implements Node {
  name: String
  posts: Post @relationship(path: "=AUTHORED=>")
}
type Post implements Node {
  text: String
  author: User @relationship(path: "<-AUTHORED-")
}

This arrow syntax has three parts - the label AUTHORED, the direction of the arrow in or out, and a cardinality ('singular' or 'plural') based on the - or = characters.

Arrows with identical labels and types at their head and tail are matched, and the combination of their cardinalities determines how the relationship between their types will be stored in the database.

You can think of the path as having the type being defined at its left, and the type of the field at its right. In the example above, the relationship User AUTHORED Post is represented with an arrow pointing out from User and in to Post. Because a user can author many posts, but each post has only one author, the arrow on the posts field of the User type is plural (=) and the arrow on the author field of the Post type is singular (-).

A plural arrow also indicates that a field should be a Relay connection - based on the directives in the example above, Gestalt would create PostsConnection and PostEdge types, and update the type of the posts field to PostsConnection. In addition to the relay connection arguments, Gestalt will add an order argument to the connection field (accepting a PostsOrder enum type with options to sort chronologically or on any indexed field).

Gestalt will calculate how to store and query relationships efficiently - with the relationships above, Gestalt will add a foreign key authored_by_user_id to the posts table.

In addition to simple relationships, paths can be extended to represent more complex relationships between types:

type User implements Node {
  name: String
  posts: Post @relationship(path: "=AUTHORED=>")
  followedUsers: User @relationship(path: "=FOLLOWED=>")
  followers: User @relationship(path: "<=FOLLOWED=")
  feed: Post @relationship(path: "=FOLLOWED=>User=AUTHORED=>")
}
type Post implements Node {
  text: String
  author: User @relationship(path: "<-AUTHORED-")
}

In this example we have added followedUsers and followers fields to the User type with a many to many relationship. Gestalt will create a join table user_followed_users with columns user_id and followed_user_id to represent this relationship.

We also added a feed field to User with multiple segments. This doesn't require any new storage beyond what we already have to represent the FOLLOWED and AUTHORED relationships between users and posts, but it does require a more complex query. Gestalt will generate an efficient query to resolve the field by joining the user_followed_users and posts tables.

Its a good practice to use past tense verbs like AUTHORED when choosing labels, and to make sure that the relationship makes sense when read in the direction of the arrow. For example Post <-AUTHORED- User reads as 'user authored post' and works, while Post -AUTHORED-> User reads as 'post authored user' and does not. Following these two rules will lead to a semantic database schema, and readable code in schema.graphql.

Other directives

There are a few more directives used by Gestalt to provide extra information about how to create the database and GraphQL schemas.

  • @hidden is used to define fields that should become part of the database schema but not be exposed as part of the GraphQL schema. It can be used for private information like email addresses and password hashes.

  • @virtual marks fields that should be part of the GraphQL schema, but should not be stored in the database. These require custom resolution to be defined - they could be computed from existing fields or stored in a different datastore.

  • @index marks fields that should be indexed in the database. They can be used to sort connection fields, or just to make custom queries efficiently from javascript.

  • @unique marks fields that should have a guarantee of uniqueness by constraint in the database.

Session type

Gestalt defines two fields on the query root, node and session - you are expected to define the Session type in schema.graphql as the entry point to your schema.

Session is a Node, but it is a special case that is not stored in the database. The value of the id field on Session will be defined automatically, but you will need to define custom resolution for any other fields you add.

A session object is made accessible in the query context. This object is both readable and writable - if it is modified, any changes are persisted between requests.

type Session implements Node {
  id: ID!
  currentUser: User
}

Defining custom resolution

Sometimes fields in your API need to do more than just read values from the database. It's easy to do this in gestalt by defining custom resolvers. Given the following User type:

type User extends Node {
  email: String @hidden
  firstName: String
  lastName: String
  fullName: String @virtual
  profileImage(size: Int): String @virtual
}

We could define custom resolution for the fullName and profileImage fields by joining firstName and lastName, and by generating a Gravatar image url based on email.

export default {
  name: 'User',
  fields: {
    // calculate user's first name from first and last names
    fullName: obj => `${obj.firstName} ${obj.lastName}`,

    // get a Gravatar image url for a user based on their email address, scaled
    // by an optional size argument
    profileImage: (obj, args) => {
      const email = obj.email.toLowerCase();
      const hash = crypto.createHash('md5').update(email).digest('hex');
      const size = args.size || 200;
      return `//www.gravatar.com/avatar/${hash}?s=${size}`;
    },
  },
}

Custom resolution is defined using the name of the Type, and then providing resolution functions (object, arguments, context) => value. It isn't required for every object, and when it is present for an object, it doesn't need to be defined for every field.

Defining mutations

Mutation definitions depend on the types you define with the schema language, so you create them as functions of an object mapping type names to GraphQL Types. Mutations are added to the schema in a second pass after object types have been fully defined.

export default types => ({
  name: 'UpdateStatus',
  inputFields: {
    status: types.String,
  },
  outputFields: {
    currentUser: types.User,
  },
  mutateAndGetPayload: async (input, context) => {
    const currentUser = await User.load(session.currentUserID);
    await currentUser.update({status: input.status});
    return {currentUser};
  },
});

The configuration object returned by mutation definition functions is nearly the same as what you would pass to graphql-relay-js's mutationWithClientMutationId. The only difference is that types can optionally be passed directly as values in the inputFields and outputFields objects.

Creating an API server

Gestalt provides Connect middleware based on express-graphql to respond to GraphQL API requests.

import gestaltServer from 'gestalt-server';
import gestaltPostgres from 'gestalt-postgres';
import importAll from 'import-all';

const app = express();

app.use('/graphql', gestaltServer({
  schemaPath: `${__dirname}/schema.graphql`,
  database: gestaltPostgres({
    databaseURL: 'postgres://localhost'
  }),
  objects: importAll(`${__dirname}/objects`),
  mutations: importAll(`${__dirname}/mutations`),
  secret: '!',
}));

app.listen(3000);

Gestalt server accepts the following options:

  • schemaPath - the path to your schema definition in GraphQL
  • database - a database adapter
  • objects - an array of object definitions
  • mutations - an array of mutation definition functions
  • secret - used to sign the session cookie
  • development - a boolean, if true gestalt will log queries and serve the GraphiQL IDE.

Should I use Gestalt?

Yes! Gestalt is cool πŸ˜€

If you are trying to add an API to a big existing app, or if you have non-standard storage requirements, Gestalt might not be the best choice for you.

If you are starting a new Relay app from scratch, Gestalt should save you a lot of time and make your schema easier to work with.

Gestalt is usable now - but it's still very early. There are likely to be some major changes before it gets to version 1.0. That said - changes will not be gratuitous and will aim to be easy to work around.

Other database adapters

I have written a backend using PostgreSQL, but Gestalt is designed for pluggable database adapters. If Gestalt sounds cool to you, but you would like to use a different backend, please consider writing one! You can find information on the interface between database adapters and the other parts of Gestalt here

The Gestalt modules

Although they are all part of this git repo, Gestalt is made up a few different npm modules so that you can use only parts you need.

  • gestalt-cli - a command line tool to scaffold new projects using gestalt-server and gestalt-postgres, run database migrations, and update your schema.json file.

  • gestalt-server - connect middleware that loads your schema.graphql file and serves your GraphQL API.

  • gestalt-graphql - if you want to generate a GraphQL schema, but don't need the middleware, you can use gestalt-graphql directly.

  • gestalt-postgres - the only database adapter (so far) for Gestalt. gestalt-postgres generates a SQL schema and queries based on your schema.graphql. It is used with either gestalt-server or gestalt-graphql. Gestalt postgres adds sql query helpers to the graphql query context, you can find more information on these here.

Contributing

For instructions on how to build, run, and test the project for local development, see CONTRIBUTING.md.