Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended syntax #3

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
```

Expand Down Expand Up @@ -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
}
```

Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions packages/blogs-example/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ 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 {
id: ID!
title: String! @index
text: String!
createdAt: Date!
author: User @relationship(path: "<-AUTHORED-")
author: <-AUTHORED- User
}
9 changes: 5 additions & 4 deletions packages/gestalt-cli/src/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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) {

Expand Down
4 changes: 3 additions & 1 deletion packages/gestalt-graphql/src/generateGraphQLSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/gestalt-graphql/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export {default, generateGraphQLSchemaWithoutResolution} from
'./generateGraphQLSchema';
export {default as databaseInfoFromAST} from './databaseInfoFromAST';
export {default as translateSyntaxExtensions} from
'./translateSyntaxExtensions';
31 changes: 31 additions & 0 deletions packages/gestalt-graphql/src/translateSyntaxExtensions.js
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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!
}
15 changes: 15 additions & 0 deletions packages/gestalt-graphql/test/translateSyntaxExtensionsText.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
1 change: 1 addition & 0 deletions packages/gestalt-postgres/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down