2018-06-17 13:24:09 +09:00
// TODO: this should not be loaded on the client?
2016-11-26 11:17:01 +09:00
/ *
Utilities to generate the app ' s GraphQL schema
* /
2016-11-03 21:39:09 +09:00
import deepmerge from 'deepmerge' ;
2016-12-06 15:51:59 +09:00
import GraphQLJSON from 'graphql-type-json' ;
2017-01-10 11:17:16 +01:00
import GraphQLDate from 'graphql-date' ;
2017-04-06 11:10:23 +09:00
import Vulcan from './config.js' ; // used for global export
2016-12-12 11:34:28 +09:00
import { Utils } from './utils.js' ;
2017-06-21 15:03:38 +09:00
import { disableFragmentWarnings } from 'graphql-tag' ;
2018-05-08 12:23:42 +09:00
import { isIntlField } from './intl.js' ;
2018-06-17 13:24:09 +09:00
import { selectorInputTemplate , mainTypeTemplate , createInputTemplate , createDataInputTemplate , updateInputTemplate , updateDataInputTemplate , orderByInputTemplate , selectorUniqueInputTemplate , deleteInputTemplate , upsertInputTemplate , singleInputTemplate , multiInputTemplate , multiOutputTemplate , singleOutputTemplate , mutationOutputTemplate , singleQueryTemplate , multiQueryTemplate , createMutationTemplate , updateMutationTemplate , upsertMutationTemplate , deleteMutationTemplate } from './graphql_templates.js' ;
2017-06-21 15:03:38 +09:00
disableFragmentWarnings ( ) ;
2016-11-03 21:39:09 +09:00
2017-07-07 20:39:57 +09:00
// get GraphQL type for a given schema and field name
2018-06-05 10:04:20 +09:00
const getGraphQLType = ( schema , fieldName ) => {
2017-07-07 20:39:57 +09:00
const field = schema [ fieldName ] ;
const type = field . type . singleType ;
2018-03-22 19:22:54 +09:00
const typeName = typeof type === 'object' ? 'Object' : typeof type === 'function' ? type . name : type ;
2017-07-07 20:39:57 +09:00
2017-01-11 18:02:12 +01:00
switch ( typeName ) {
2017-01-14 18:04:53 +09:00
2017-07-07 20:39:57 +09:00
case 'String' :
return 'String' ;
2017-01-14 18:04:53 +09:00
2017-07-07 20:39:57 +09:00
case 'Boolean' :
return 'Boolean' ;
case 'Number' :
return 'Float' ;
2017-01-11 18:02:12 +01:00
2017-03-16 01:25:08 +08:00
case 'SimpleSchema.Integer' :
return 'Int' ;
2017-07-07 20:39:57 +09:00
// for arrays, look for type of associated schema field or default to [String]
case 'Array' :
const arrayItemFieldName = ` ${ fieldName } . $ ` ;
// note: make sure field has an associated array
if ( schema [ arrayItemFieldName ] ) {
// try to get array type from associated array
const arrayItemType = getGraphQLType ( schema , arrayItemFieldName ) ;
return arrayItemType ? ` [ ${ arrayItemType } ] ` : null ;
}
return null ;
2017-01-11 18:02:12 +01:00
2017-07-07 20:39:57 +09:00
case 'Object' :
return 'JSON' ;
2017-01-11 18:02:12 +01:00
2017-07-07 20:39:57 +09:00
case 'Date' :
return 'Date' ;
2017-01-14 18:04:53 +09:00
2017-01-11 18:02:12 +01:00
default :
2017-07-07 20:39:57 +09:00
return null ;
2016-11-08 14:56:39 +09:00
}
}
2016-12-12 10:41:50 +09:00
export const GraphQLSchema = {
2016-11-08 14:56:39 +09:00
// collections used to auto-generate schemas
collections : [ ] ,
2016-11-08 15:12:23 +09:00
addCollection ( collection ) {
this . collections . push ( collection ) ;
2016-11-08 14:56:39 +09:00
} ,
2016-11-26 11:17:01 +09:00
// generate GraphQL schemas for all registered collections
2016-11-08 12:58:53 +01:00
getCollectionsSchemas ( ) {
const collectionsSchemas = this . collections . map ( collection => {
2016-11-08 15:12:23 +09:00
return this . generateSchema ( collection ) ;
2018-01-25 18:12:26 +09:00
} ) . join ( '' ) ;
2016-11-08 12:58:53 +01:00
return collectionsSchemas ;
2016-11-08 14:56:39 +09:00
} ,
// additional schemas
2016-10-29 16:37:33 +09:00
schemas : [ ] ,
addSchema ( schema ) {
this . schemas . push ( schema ) ;
} ,
2016-11-26 11:17:01 +09:00
// get extra schemas defined manually
2016-11-08 14:56:39 +09:00
getAdditionalSchemas ( ) {
2018-05-07 17:40:21 +09:00
const additionalSchemas = this . schemas . join ( '\n' ) ;
2016-11-08 14:56:39 +09:00
return additionalSchemas ;
} ,
// queries
2016-10-29 16:37:33 +09:00
queries : [ ] ,
2018-01-02 13:04:33 +09:00
addQuery ( query , description ) {
this . queries . push ( { query , description } ) ;
2016-10-31 16:19:37 +01:00
} ,
2016-11-08 14:56:39 +09:00
// mutations
2016-10-31 16:19:37 +01:00
mutations : [ ] ,
2018-01-02 13:04:33 +09:00
addMutation ( mutation , description ) {
this . mutations . push ( { mutation , description } ) ;
2016-10-31 16:19:37 +01:00
} ,
2016-11-08 14:56:39 +09:00
// add resolvers
2016-12-06 15:51:59 +09:00
resolvers : {
JSON : GraphQLJSON ,
2017-01-10 11:17:16 +01:00
Date : GraphQLDate ,
2016-12-06 15:51:59 +09:00
} ,
2016-11-03 21:39:09 +09:00
addResolvers ( resolvers ) {
this . resolvers = deepmerge ( this . resolvers , resolvers ) ;
} ,
2017-03-29 16:43:52 +09:00
removeResolver ( typeName , resolverName ) {
delete this . resolvers [ typeName ] [ resolverName ] ;
} ,
2016-11-08 14:56:39 +09:00
// add objects to context
2016-11-03 21:39:09 +09:00
context : { } ,
addToContext ( object ) {
this . context = deepmerge ( this . context , object ) ;
2016-11-07 17:45:17 +09:00
} ,
2017-03-16 01:25:08 +08:00
2018-05-07 17:40:21 +09:00
directives : { } ,
addDirective ( directive ) {
this . directives = deepmerge ( this . directives , directive ) ;
} ,
2018-06-05 10:04:20 +09:00
// for a given schema, return main type fields, selector fields,
// unique selector fields, orderBy fields, creatable fields, and updatable fields
getFields ( schema , typeName ) {
const fields = {
mainType : [ ] ,
create : [ ] ,
update : [ ] ,
selector : [ ] ,
selectorUnique : [ ] ,
orderBy : [ ] ,
} ;
Object . keys ( schema ) . forEach ( fieldName => {
const field = schema [ fieldName ] ;
2017-07-07 20:39:57 +09:00
const fieldType = getGraphQLType ( schema , fieldName ) ;
2017-03-16 01:25:08 +08:00
2018-01-02 13:05:03 +09:00
// only include fields that are viewable/insertable/editable and don't contain "$" in their name
// note: insertable/editable fields must be included in main schema in case they're returned by a mutation
2018-06-22 20:55:22 +09:00
// OpenCRUD backwards compatibility
if ( ( field . canRead || field . canCreate || field . canUpdate || field . viewableBy || field . insertableBy || field . editableBy ) && fieldName . indexOf ( '$' ) === - 1 ) {
2018-01-02 13:05:03 +09:00
2018-06-05 10:04:20 +09:00
const fieldDescription = field . description ;
const fieldDirective = isIntlField ( field ) ? ` @intl ` : '' ;
const fieldArguments = isIntlField ( field ) ? [ { name : 'locale' , type : 'String' } ] : [ ] ;
2018-05-07 17:41:22 +09:00
2017-07-07 20:39:57 +09:00
// if field has a resolveAs, push it to schema
2016-11-08 15:16:58 +09:00
if ( field . resolveAs ) {
2017-07-03 10:54:10 +09:00
2017-08-24 13:16:50 +09:00
// get resolver name from resolveAs object, or else default to field name
const resolverName = field . resolveAs . fieldName || fieldName ;
2018-03-19 14:57:44 +09:00
// use specified GraphQL type or else convert schema type
const fieldGraphQLType = field . resolveAs . type || fieldType ;
2017-07-03 10:54:10 +09:00
// if resolveAs is an object, first push its type definition
2017-07-14 10:07:48 +09:00
// include arguments if there are any
2018-05-21 09:42:08 +09:00
// note: resolved fields are not internationalized
2018-06-05 10:04:20 +09:00
fields . mainType . push ( {
description : field . resolveAs . description ,
name : resolverName ,
2018-06-06 09:07:13 +09:00
args : field . resolveAs . arguments ,
2018-06-05 10:04:20 +09:00
type : fieldGraphQLType ,
} ) ;
2017-07-03 10:54:10 +09:00
// then build actual resolver object and pass it to addGraphQLResolvers
const resolver = {
2018-06-05 10:04:20 +09:00
[ typeName ] : {
2017-08-24 13:16:50 +09:00
[ resolverName ] : field . resolveAs . resolver
2017-07-03 10:54:10 +09:00
}
} ;
addGraphQLResolvers ( resolver ) ;
2017-07-14 10:37:19 +09:00
// if addOriginalField option is enabled, also add original field to schema
if ( field . resolveAs . addOriginalField && fieldType ) {
2018-06-05 10:04:20 +09:00
fields . mainType . push ( {
description : fieldDescription ,
name : fieldName ,
2018-06-06 09:07:13 +09:00
args : fieldArguments ,
2018-06-05 10:04:20 +09:00
type : fieldType ,
directive : fieldDirective ,
} ) ;
2017-07-08 11:43:43 +09:00
}
} else {
// try to guess GraphQL type
if ( fieldType ) {
2018-06-05 10:04:20 +09:00
fields . mainType . push ( {
description : fieldDescription ,
name : fieldName ,
2018-06-06 09:07:13 +09:00
args : fieldArguments ,
2018-06-05 10:04:20 +09:00
type : fieldType ,
directive : fieldDirective ,
} ) ;
2017-07-08 11:43:43 +09:00
}
2016-11-08 15:16:58 +09:00
}
2018-06-22 20:57:31 +09:00
// OpenCRUD backwards compatibility
if ( field . canCreate || field . insertableBy ) {
2018-06-05 10:04:20 +09:00
fields . create . push ( {
name : fieldName ,
type : fieldType ,
required : ! field . optional ,
} ) ;
}
2018-06-22 20:57:31 +09:00
// OpenCRUD backwards compatibility
if ( field . canUpdate || field . editableBy ) {
2018-06-05 10:04:20 +09:00
fields . update . push ( {
name : fieldName ,
type : fieldType ,
} ) ;
2016-11-08 14:56:39 +09:00
}
2018-05-08 11:05:24 +09:00
2018-05-21 09:42:08 +09:00
// if field is i18nized, add foo_intl field containing all languages
2018-05-08 12:23:42 +09:00
if ( isIntlField ( field ) ) {
2018-06-05 10:04:20 +09:00
fields . mainType . push ( { name : ` ${ fieldName } _intl ` , type : ` [IntlValue] ` } ) ;
fields . create . push ( { name : ` ${ fieldName } _intl ` , type : ` [IntlValueInput] ` } ) ;
fields . update . push ( { name : ` ${ fieldName } _intl ` , type : ` [IntlValueInput] ` } ) ;
}
if ( field . selectable ) {
// TODO
}
if ( field . orderable ) {
fields . orderBy . push ( fieldName ) ;
2018-05-08 11:05:24 +09:00
}
2016-11-08 14:56:39 +09:00
}
} ) ;
2018-06-05 10:04:20 +09:00
return fields ;
} ,
// generate a GraphQL schema corresponding to a given collection
generateSchema ( collection ) {
2018-06-17 13:24:09 +09:00
let graphQLSchema = '' ;
2018-06-05 10:04:20 +09:00
const schemaFragments = [ ] ;
const collectionName = collection . options . collectionName ;
const typeName = collection . typeName ? collection . typeName : Utils . camelToSpaces ( _ . initial ( collectionName ) . join ( '' ) ) ; // default to posts -> Post
const schema = collection . simpleSchema ( ) . _schema ;
const fields = this . getFields ( schema , typeName ) ;
2018-06-17 13:24:09 +09:00
const { interfaces = [ ] , resolvers , mutations } = collection . options ;
2017-07-22 11:19:51 +02:00
2018-01-02 13:04:33 +09:00
const description = collection . options . description ? collection . options . description : ` Type for ${ collectionName } `
2018-06-05 10:04:20 +09:00
const { mainType , create , update , selector , selectorUnique , orderBy } = fields ;
2018-01-02 13:04:33 +09:00
2018-06-17 13:24:09 +09:00
if ( mainType . length ) {
schemaFragments . push ( mainTypeTemplate ( { typeName , description , interfaces , fields : mainType } ) ) ;
schemaFragments . push ( deleteInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( singleInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( multiInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( singleOutputTemplate ( { typeName } ) ) ;
schemaFragments . push ( multiOutputTemplate ( { typeName } ) ) ;
schemaFragments . push ( mutationOutputTemplate ( { typeName } ) ) ;
if ( create . length ) {
schemaFragments . push ( createInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( createDataInputTemplate ( { typeName , fields : create } ) ) ;
}
2018-01-02 13:04:33 +09:00
2018-06-17 13:24:09 +09:00
if ( update . length ) {
schemaFragments . push ( updateInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( upsertInputTemplate ( { typeName } ) ) ;
schemaFragments . push ( updateDataInputTemplate ( { typeName , fields : update } ) ) ;
}
2018-01-02 13:04:33 +09:00
2018-06-17 13:24:09 +09:00
schemaFragments . push ( selectorInputTemplate ( { typeName , fields : selector } ) ) ;
2018-01-25 18:12:26 +09:00
2018-06-17 13:24:09 +09:00
schemaFragments . push ( selectorUniqueInputTemplate ( { typeName , fields : selectorUnique } ) ) ;
2018-06-05 10:04:20 +09:00
2018-06-17 13:24:09 +09:00
schemaFragments . push ( orderByInputTemplate ( { typeName , fields : orderBy } ) ) ;
2018-06-05 10:04:20 +09:00
2018-06-17 13:24:09 +09:00
if ( ! _ . isEmpty ( resolvers ) ) {
const queryResolvers = { } ;
// single
if ( resolvers . single ) {
addGraphQLQuery ( singleQueryTemplate ( { typeName } ) , resolvers . single . description ) ;
queryResolvers [ Utils . camelCaseify ( typeName ) ] = resolvers . single . resolver . bind ( resolvers . single ) ;
}
// multi
if ( resolvers . multi ) {
addGraphQLQuery ( multiQueryTemplate ( { typeName } ) , resolvers . multi . description ) ;
queryResolvers [ ` ${ Utils . camelCaseify ( typeName ) } s ` ] = resolvers . multi . resolver . bind ( resolvers . multi ) ;
}
addGraphQLResolvers ( { Query : { ... queryResolvers } } ) ;
}
if ( ! _ . isEmpty ( mutations ) ) {
const mutationResolvers = { } ;
// create
if ( mutations . create ) { // e.g. "createMovie(input: CreateMovieInput) : Movie"
addGraphQLMutation ( createMutationTemplate ( { typeName } ) , mutations . create . description ) ;
mutationResolvers [ ` create ${ typeName } ` ] = mutations . create . mutation . bind ( mutations . create ) ;
}
// update
if ( mutations . update ) { // e.g. "updateMovie(input: UpdateMovieInput) : Movie"
addGraphQLMutation ( updateMutationTemplate ( { typeName } ) , mutations . update . description ) ;
mutationResolvers [ ` update ${ typeName } ` ] = mutations . update . mutation . bind ( mutations . update ) ;
}
// upsert
if ( mutations . upsert ) { // e.g. "upsertMovie(input: UpsertMovieInput) : Movie"
addGraphQLMutation ( upsertMutationTemplate ( { typeName } ) , mutations . upsert . description ) ;
mutationResolvers [ ` upsert ${ typeName } ` ] = mutations . upsert . mutation . bind ( mutations . upsert ) ;
}
// delete
if ( mutations . delete ) { // e.g. "deleteMovie(input: DeleteMovieInput) : Movie"
addGraphQLMutation ( deleteMutationTemplate ( { typeName } ) , mutations . delete . description ) ;
mutationResolvers [ ` delete ${ typeName } ` ] = mutations . delete . mutation . bind ( mutations . delete ) ;
}
addGraphQLResolvers ( { Mutation : { ... mutationResolvers } } ) ;
}
graphQLSchema = schemaFragments . join ( '\n\n' ) + ` \n \n \n ` ;
2018-06-05 10:04:20 +09:00
2018-06-20 10:24:57 +09:00
} else {
// eslint-disable-next-line no-console
console . log ( ` // Warning: collection ${ collectionName } doesn't have any GraphQL-enabled fields, so no corresponding type can be generated. Pass generateGraphQLSchema = false to createCollection() to disable this warning ` )
2018-06-17 13:24:09 +09:00
}
2016-11-08 14:56:39 +09:00
return graphQLSchema ;
}
2017-01-10 10:09:24 +01:00
} ;
2017-03-18 15:59:31 +09:00
2017-04-06 11:10:23 +09:00
Vulcan . getGraphQLSchema = ( ) => {
const schema = GraphQLSchema . finalSchema [ 0 ] ;
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
2017-04-06 11:10:23 +09:00
console . log ( schema ) ;
return schema ;
}
2017-07-03 10:54:10 +09:00
export const addGraphQLCollection = GraphQLSchema . addCollection . bind ( GraphQLSchema ) ;
2017-03-18 15:59:31 +09:00
export const addGraphQLSchema = GraphQLSchema . addSchema . bind ( GraphQLSchema ) ;
export const addGraphQLQuery = GraphQLSchema . addQuery . bind ( GraphQLSchema ) ;
export const addGraphQLMutation = GraphQLSchema . addMutation . bind ( GraphQLSchema ) ;
export const addGraphQLResolvers = GraphQLSchema . addResolvers . bind ( GraphQLSchema ) ;
2017-03-29 16:43:52 +09:00
export const removeGraphQLResolver = GraphQLSchema . removeResolver . bind ( GraphQLSchema ) ;
2018-05-07 17:40:21 +09:00
export const addToGraphQLContext = GraphQLSchema . addToContext . bind ( GraphQLSchema ) ;
export const addGraphQLDirective = GraphQLSchema . addDirective . bind ( GraphQLSchema ) ;