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';
|
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-05-08 11:55:45 +09:00
|
|
|
const getGraphQLType = (schema, fieldName, isInputType) => {
|
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);
|
|
|
|
},
|
|
|
|
|
2016-11-26 11:17:01 +09:00
|
|
|
// generate a GraphQL schema corresponding to a given collection
|
2016-11-08 15:12:23 +09:00
|
|
|
generateSchema(collection) {
|
2016-11-08 14:56:39 +09:00
|
|
|
|
2017-03-29 15:49:07 +09:00
|
|
|
const collectionName = collection.options.collectionName;
|
|
|
|
|
2016-12-12 11:34:28 +09:00
|
|
|
const mainTypeName = collection.typeName ? collection.typeName : Utils.camelToSpaces(_.initial(collectionName).join('')); // default to posts -> Post
|
2016-11-08 14:56:39 +09:00
|
|
|
|
2016-11-10 16:53:06 +01:00
|
|
|
// backward-compatibility code: we do not want user.telescope fields in the graphql schema
|
2016-12-12 11:34:28 +09:00
|
|
|
const schema = Utils.stripTelescopeNamespace(collection.simpleSchema()._schema);
|
2017-01-11 18:02:12 +01:00
|
|
|
|
2018-01-02 13:04:33 +09:00
|
|
|
let mainSchema = [], inputSchema = [], unsetSchema = [], graphQLSchema = '';
|
2016-11-08 14:56:39 +09:00
|
|
|
|
2017-07-07 20:39:57 +09:00
|
|
|
_.forEach(schema, (field, fieldName) => {
|
|
|
|
// console.log(field, fieldName)
|
2017-01-11 18:02:12 +01:00
|
|
|
|
2017-07-07 20:39:57 +09:00
|
|
|
const fieldType = getGraphQLType(schema, fieldName);
|
2018-05-08 11:55:45 +09:00
|
|
|
// note: intl field have a String "normal" type but a JSON input type
|
|
|
|
const fieldInputType = getGraphQLType(schema, fieldName, true);
|
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
|
|
|
|
if ((field.viewableBy || field.insertableBy || field.editableBy) && fieldName.indexOf('$') === -1) {
|
|
|
|
|
2018-01-25 18:12:26 +09:00
|
|
|
const fieldDescription = field.description ? `# ${field.description}
|
|
|
|
` : '';
|
2016-11-08 15:16:58 +09:00
|
|
|
|
2018-05-08 12:23:42 +09:00
|
|
|
const fieldDirective = isIntlField(field) ? ` @intl` : '';
|
2018-05-21 09:42:08 +09:00
|
|
|
const fieldArguments = isIntlField(field) ? `(locale: 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
|
|
|
|
|
|
|
if (typeof field.resolveAs === 'string') {
|
|
|
|
// if resolveAs is a string, push it and done
|
|
|
|
mainSchema.push(field.resolveAs);
|
|
|
|
} else {
|
|
|
|
|
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
|
|
|
|
mainSchema.push(`${resolverName}${field.resolveAs.arguments ? `(${field.resolveAs.arguments})` : ''}: ${fieldGraphQLType}`);
|
2017-07-03 10:54:10 +09:00
|
|
|
|
|
|
|
// then build actual resolver object and pass it to addGraphQLResolvers
|
|
|
|
const resolver = {
|
|
|
|
[mainTypeName]: {
|
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-01-02 13:04:33 +09:00
|
|
|
mainSchema.push(
|
2018-05-21 09:42:08 +09:00
|
|
|
`${fieldDescription}${fieldName}${fieldArguments}: ${fieldType}${fieldDirective}`);
|
2017-07-08 11:43:43 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// try to guess GraphQL type
|
|
|
|
if (fieldType) {
|
2018-01-02 13:04:33 +09:00
|
|
|
mainSchema.push(
|
2018-05-21 09:42:08 +09:00
|
|
|
`${fieldDescription}${fieldName}${fieldArguments}: ${fieldType}${fieldDirective}`);
|
2017-07-08 11:43:43 +09:00
|
|
|
}
|
2016-11-08 15:16:58 +09:00
|
|
|
}
|
|
|
|
|
2016-12-06 10:55:47 +01:00
|
|
|
if (field.insertableBy || field.editableBy) {
|
2016-11-08 14:56:39 +09:00
|
|
|
|
2017-03-16 01:25:08 +08:00
|
|
|
// note: marking a field as required makes it required for updates, too,
|
2017-01-21 10:02:03 +09:00
|
|
|
// which makes partial updates impossible
|
|
|
|
// const isRequired = field.optional ? '' : '!';
|
2017-03-16 01:25:08 +08:00
|
|
|
|
2017-01-21 10:02:03 +09:00
|
|
|
const isRequired = '';
|
2016-11-08 14:56:39 +09:00
|
|
|
|
|
|
|
// 2. input schema
|
2018-05-08 11:55:45 +09:00
|
|
|
inputSchema.push(`${fieldName}: ${fieldInputType}${isRequired}`);
|
2017-03-16 01:25:08 +08:00
|
|
|
|
2016-11-08 14:56:39 +09:00
|
|
|
// 3. unset schema
|
2017-07-07 20:39:57 +09:00
|
|
|
unsetSchema.push(`${fieldName}: Boolean`);
|
2017-03-16 01:25:08 +08:00
|
|
|
|
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-05-21 09:42:08 +09:00
|
|
|
mainSchema.push(`${fieldName}_intl: [IntlValue]`);
|
|
|
|
inputSchema.push(`${fieldName}_intl: [IntlValueInput]`);
|
|
|
|
unsetSchema.push(`${fieldName}_intl: Boolean`);
|
2018-05-08 11:05:24 +09:00
|
|
|
}
|
2016-11-08 14:56:39 +09:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-07-22 11:19:51 +02:00
|
|
|
const { interfaces = [] } = collection.options;
|
|
|
|
const graphQLInterfaces = interfaces.length ? `implements ${interfaces.join(`, `)} ` : '';
|
|
|
|
|
2018-01-02 13:04:33 +09:00
|
|
|
const description = collection.options.description ? collection.options.description : `Type for ${collectionName}`
|
|
|
|
|
|
|
|
if (mainSchema.length) {
|
|
|
|
|
|
|
|
graphQLSchema +=
|
|
|
|
`# ${description}
|
|
|
|
type ${mainTypeName} ${graphQLInterfaces}{
|
|
|
|
${mainSchema.join('\n ')}
|
|
|
|
}
|
2018-01-25 18:12:26 +09:00
|
|
|
|
2018-01-02 13:04:33 +09:00
|
|
|
`
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputSchema.length) {
|
|
|
|
graphQLSchema +=
|
|
|
|
`# ${description} (input type)
|
|
|
|
input ${collectionName}Input {
|
|
|
|
${inputSchema.join('\n ')}
|
|
|
|
}
|
2018-01-25 18:12:26 +09:00
|
|
|
|
2018-01-02 13:04:33 +09:00
|
|
|
`
|
|
|
|
}
|
|
|
|
|
|
|
|
if (unsetSchema.length) {
|
|
|
|
graphQLSchema +=
|
|
|
|
`# ${description} (unset input type)
|
|
|
|
input ${collectionName}Unset {
|
|
|
|
${unsetSchema.join('\n ')}
|
|
|
|
}
|
2018-01-25 18:12:26 +09:00
|
|
|
|
2018-01-02 13:04:33 +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);
|