diff --git a/README.md b/README.md index b96d8968..7901c4b3 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Gestalt ======= -Gestalt lets you use the [GraphQL](http://graphql.org/) 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 lets you use an extended version of the [GraphQL](http://graphql.org/) +schema language to define an API with a PostgreSQL backend declaratively, +*really quickly*, and with a *tiny* amount of code. [![Build Status](https://travis-ci.org/charlieschwabacher/gestalt.svg?branch=master)](https://travis-ci.org/charlieschwabacher/gestalt?branch=master) @@ -106,21 +106,20 @@ tables. Other objects and arrays they reference are stored in PostgreSQL as JSON, and relationships between nodes are specified with directives. -Object relationships --------------------- +Language Extensions +------------------- 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](//github.com/neo4j/neo4j)'s Cypher query language. +database schema and efficient queries. You provide this using a syntax inspired +by [Neo4j](//github.com/neo4j/neo4j)'s Cypher query language. ```GraphQL type User implements Node { name: String - posts: Post @relationship(path: "=AUTHORED=>") + posts: =AUTHORED=> Post } type Post implements Node { text: String - author: User @relationship(path: "<-AUTHORED-") + author: <-AUTHORED- User } ``` @@ -157,14 +156,14 @@ complex relationships between types: ```GraphQL 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=>") + posts: =AUTHORED=> Post + followedUsers: =FOLLOWED=> User + followers: <=FOLLOWED= User + feed: =FOLLOWED=> User =AUTHORED=> Post } type Post implements Node { text: String - author: User @relationship(path: "<-AUTHORED-") + author: <-AUTHORED- User } ``` @@ -187,10 +186,10 @@ 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. +Directives +---------- +There are a few 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 diff --git a/packages/blogs-example/schema.graphql b/packages/blogs-example/schema.graphql index 2b71b6e4..c9fbda1e 100644 --- a/packages/blogs-example/schema.graphql +++ b/packages/blogs-example/schema.graphql @@ -13,10 +13,10 @@ type User implements Node { fullName: String @virtual profileImage(size: Int): String! @virtual following: Boolean! @virtual - followedUsers: User @relationship(path: "=FOLLOWED=>") - followers: User @relationship(path: "<=FOLLOWED=") - posts: Post @relationship(path: "=AUTHORED=>") - feed: Post @relationship(path: "=FOLLOWED=>User=AUTHORED=>") + followedUsers: =FOLLOWED=> User + followers: <=FOLLOWED= User + posts: =AUTHORED=> Post + feed: =FOLLOWED=> User =AUTHORED=> Post } type Post implements Node { @@ -24,5 +24,5 @@ type Post implements Node { title: String! @index text: String! createdAt: Date! - author: User @relationship(path: "<-AUTHORED-") + author: <-AUTHORED- User } diff --git a/packages/gestalt-cli/src/migrate.js b/packages/gestalt-cli/src/migrate.js index dc334aa9..a93a00f3 100644 --- a/packages/gestalt-cli/src/migrate.js +++ b/packages/gestalt-cli/src/migrate.js @@ -11,8 +11,8 @@ import {blue} from 'colors/safe'; import snake from 'snake-case'; import {graphql, parse} from 'graphql'; import {introspectionQuery} from 'graphql/utilities'; -import {generateGraphQLSchemaWithoutResolution, databaseInfoFromAST} from - 'gestalt-graphql'; +import {generateGraphQLSchemaWithoutResolution, databaseInfoFromAST, + translateSyntaxExtensions} from 'gestalt-graphql'; import {generateDatabaseInterface, generateDatabaseSchemaMigration, readExistingDatabaseSchema} from 'gestalt-postgres'; import {invariant} from 'gestalt-utils'; @@ -30,6 +30,7 @@ export default async function migrate() { ); const schemaText = fs.readFileSync('schema.graphql', 'utf8'); + const translatedText = translateSyntaxExtensions(schemaText); const localPackage = JSON.parse(fs.readFileSync('package.json', 'utf8')); const localVersion = localPackage.dependencies['gestalt-server']; const cliVersion = require('../package.json').version; @@ -44,8 +45,8 @@ export default async function migrate() { console.log('migrating..'); - await updateJSONSchema(localPackage, schemaText); - await updateDatabaseSchema(localPackage, schemaText); + await updateJSONSchema(localPackage, translatedText); + await updateDatabaseSchema(localPackage, translatedText); } catch (err) { diff --git a/packages/gestalt-graphql/src/generateGraphQLSchema.js b/packages/gestalt-graphql/src/generateGraphQLSchema.js index 5c742b59..3c7bf734 100644 --- a/packages/gestalt-graphql/src/generateGraphQLSchema.js +++ b/packages/gestalt-graphql/src/generateGraphQLSchema.js @@ -7,6 +7,7 @@ import camel from 'camel-case'; import {parse, buildASTSchema, concatAST, printSchema, GraphQLObjectType, getNamedType, GraphQLSchema} from 'graphql'; import {mutationWithClientMutationId} from 'graphql-relay'; +import translateSyntaxExtensions from './translateSyntaxExtensions'; import {insertConnectionTypes, removeHiddenNodes} from './ASTTransforms'; import databaseInfoFromAST from './databaseInfoFromAST'; import scalarTypeDefinitions from './scalarTypeDefinitions'; @@ -25,7 +26,8 @@ export default function generateGraphQLSchema( databaseInterfaceDefinitionFn: DatabaseInterfaceDefinitionFn, config?: GestaltServerConfig, ): {schema: GraphQLSchema, databaseInterface: DatabaseInterface} { - const ast = parse(schemaText); + const translatedText = translateSyntaxExtensions(schemaText); + const ast = parse(translatedText); // we take inventory of object definitions and relationships before the ast // is modified diff --git a/packages/gestalt-graphql/src/index.js b/packages/gestalt-graphql/src/index.js index cfdcdf0a..41f7ba44 100644 --- a/packages/gestalt-graphql/src/index.js +++ b/packages/gestalt-graphql/src/index.js @@ -1,3 +1,5 @@ export {default, generateGraphQLSchemaWithoutResolution} from './generateGraphQLSchema'; export {default as databaseInfoFromAST} from './databaseInfoFromAST'; +export {default as translateSyntaxExtensions} from + './translateSyntaxExtensions'; diff --git a/packages/gestalt-graphql/src/translateSyntaxExtensions.js b/packages/gestalt-graphql/src/translateSyntaxExtensions.js new file mode 100644 index 00000000..b98f9c86 --- /dev/null +++ b/packages/gestalt-graphql/src/translateSyntaxExtensions.js @@ -0,0 +1,31 @@ +// @flow + +// the regex below matches singular or plural arrows in either direction, +// followed by typenames, repeating any number of times. +// ie: +// =FOLLOWED=>User=AUTHORED=>Post +// or: +// <-AUTHORED-User + +import {invariant} from 'gestalt-utils'; + +const ARROW_MATCHER = + /:[ \t]*(?:(?:<([=-])[ \t]*[A-Z_]+[ \t]*\1|([=-])[ \t]*[A-Z_]+[ \t]*\2>)[ \t]*[A-Z][a-zA-Z0-9]*!?[ \t]*)+/g; + +export default function translateSyntaxExtensions(schemaText: string): string { + const translatedSchema = schemaText.replace( + ARROW_MATCHER, + (arrow: string): string => { + const normalArrow = arrow.replace(/[ \t:]/g, ''); + + const match = normalArrow.match(/[A-Z][a-zA-Z0-9]+!?$/); + invariant(match != null, 'error parsing schema'); + const finalType = match[0]; + + const path = normalArrow.slice(0, normalArrow.length - finalType.length); + + return `: ${finalType} @relationship(path: "${path}")`; + } + ); + return translatedSchema; +} diff --git a/packages/gestalt-graphql/test/fixtures/schemaWithSyntaxExtensions.graphql b/packages/gestalt-graphql/test/fixtures/schemaWithSyntaxExtensions.graphql new file mode 100644 index 00000000..ec72386b --- /dev/null +++ b/packages/gestalt-graphql/test/fixtures/schemaWithSyntaxExtensions.graphql @@ -0,0 +1,36 @@ +type Session { + id: ID! + currentUser: User +} + +type User implements Node { + id: ID! + email: String! @unique @hidden @index + passwordHash: String! @hidden + createdAt: Date! + firstName: String + lastName: String + fullName: String @virtual + followedUsers: =FOLLOWED=> User + followers: <=FOLLOWED= User + posts: =AUTHORED=> Post + comments: =AUTHORED=> Comment + feed: =FOLLOWED=> User =AUTHORED=> Post +} + +type Post implements Node { + id: ID! + title: String! @index + text: String! + createdAt: Date! + author: <-AUTHORED- User! + comments: =INSPIRED=> Comment +} + +type Comment implements Node { + id: ID! + text: String! + createdAt: Date! + author: <-AUTHORED- User + subject: <-INSPIRED- Post! +} diff --git a/packages/gestalt-graphql/test/translateSyntaxExtensionsText.js b/packages/gestalt-graphql/test/translateSyntaxExtensionsText.js new file mode 100644 index 00000000..b91aa2eb --- /dev/null +++ b/packages/gestalt-graphql/test/translateSyntaxExtensionsText.js @@ -0,0 +1,15 @@ +import assert from 'assert'; +import fs from 'fs'; +import translateSyntaxExtensions from '../src/translateSyntaxExtensions'; + +const schema = fs.readFileSync(`${__dirname}/fixtures/schema.graphql`, 'utf8'); +const schemaWithSyntaxExtensions = fs.readFileSync( + `${__dirname}/fixtures/schemaWithSyntaxExtensions.graphql`, + 'utf8' +); + +describe('translateSyntaxExtensions', () => { + it('translates infix arrow syntax to @relationship directives', () => { + assert.equal(translateSyntaxExtensions(schemaWithSyntaxExtensions), schema); + }); +}); diff --git a/packages/gestalt-postgres/src/index.js b/packages/gestalt-postgres/src/index.js index eb18f687..15045fad 100644 --- a/packages/gestalt-postgres/src/index.js +++ b/packages/gestalt-postgres/src/index.js @@ -9,6 +9,7 @@ export {default as generateDatabaseSchemaMigration} from './generateDatabaseSchemaMigration'; export {default as readExistingDatabaseSchema} from './readExistingDatabaseSchema'; +export {default as DB} from './DB'; export default function gestaltPostgres(databaseAdapterConfig: { databaseURL: string