Vulcan/packages/vulcan-lib/lib/modules/graphql.js

298 lines
9.6 KiB
JavaScript
Raw Normal View History

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';
import GraphQLJSON from 'graphql-type-json';
import GraphQLDate from 'graphql-date';
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';
import { isIntlField } from './intl.js';
import { selectorInputTemplate, mainTypeTemplate, createInputTemplate, createDataInputTemplate, updateInputTemplate, updateDataInputTemplate, orderByInputTemplate, selectorUniqueInputTemplate, deleteInputTemplate, upsertInputTemplate, singleInputTemplate, multiInputTemplate, multiOutputTemplate, singleOutputTemplate, mutationOutputTemplate } from './graphql_templates.js';
2017-06-21 15:03:38 +09:00
disableFragmentWarnings();
2016-11-03 21:39:09 +09:00
// get GraphQL type for a given schema and field name
const getGraphQLType = (schema, fieldName) => {
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;
switch (typeName) {
case 'String':
return 'String';
case 'Boolean':
return 'Boolean';
case 'Number':
return 'Float';
case 'SimpleSchema.Integer':
return 'Int';
// 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;
case 'Object':
return 'JSON';
case 'Date':
return 'Date';
default:
return null;
}
}
export const GraphQLSchema = {
// collections used to auto-generate schemas
collections: [],
2016-11-08 15:12:23 +09:00
addCollection(collection) {
this.collections.push(collection);
},
2016-11-26 11:17:01 +09:00
// generate GraphQL schemas for all registered collections
getCollectionsSchemas() {
const collectionsSchemas = this.collections.map(collection => {
2016-11-08 15:12:23 +09:00
return this.generateSchema(collection);
}).join('');
return collectionsSchemas;
},
// additional schemas
schemas: [],
addSchema(schema) {
this.schemas.push(schema);
},
2016-11-26 11:17:01 +09:00
// get extra schemas defined manually
getAdditionalSchemas() {
2018-05-07 17:40:21 +09:00
const additionalSchemas = this.schemas.join('\n');
return additionalSchemas;
},
// queries
queries: [],
addQuery(query, description) {
this.queries.push({ query, description });
},
// mutations
mutations: [],
addMutation(mutation, description) {
this.mutations.push({ mutation, description });
},
// add resolvers
resolvers: {
JSON: GraphQLJSON,
Date: GraphQLDate,
},
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];
},
// 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
},
2018-05-07 17:40:21 +09:00
directives: {},
addDirective(directive) {
this.directives = deepmerge(this.directives, directive);
},
// 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];
const fieldType = getGraphQLType(schema, fieldName);
// 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) {
const fieldDescription = field.description;
const fieldDirective = isIntlField(field) ? `@intl` : '';
const fieldArguments = isIntlField(field) ? [{ name: 'locale', type: 'String' }] : [];
// if field has a resolveAs, push it to schema
2016-11-08 15:16:58 +09:00
if (field.resolveAs) {
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;
// if resolveAs is an object, first push its type definition
// include arguments if there are any
// note: resolved fields are not internationalized
fields.mainType.push({
description: field.resolveAs.description,
name: resolverName,
2018-06-06 09:07:13 +09:00
args: field.resolveAs.arguments,
type: fieldGraphQLType,
});
// then build actual resolver object and pass it to addGraphQLResolvers
const resolver = {
[typeName]: {
2017-08-24 13:16:50 +09:00
[resolverName]: field.resolveAs.resolver
}
};
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) {
fields.mainType.push({
description: fieldDescription,
name: fieldName,
2018-06-06 09:07:13 +09:00
args: fieldArguments,
type: fieldType,
directive: fieldDirective,
});
}
} else {
// try to guess GraphQL type
if (fieldType) {
fields.mainType.push({
description: fieldDescription,
name: fieldName,
2018-06-06 09:07:13 +09:00
args: fieldArguments,
type: fieldType,
directive: fieldDirective,
});
}
2016-11-08 15:16:58 +09:00
}
if (field.insertableBy) {
fields.create.push({
name: fieldName,
type: fieldType,
required: !field.optional,
});
}
if (field.editableBy) {
fields.update.push({
name: fieldName,
type: fieldType,
});
}
// if field is i18nized, add foo_intl field containing all languages
if (isIntlField(field)) {
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);
}
}
});
return fields;
},
// generate a GraphQL schema corresponding to a given collection
generateSchema(collection) {
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);
const { interfaces = [] } = collection.options;
const description = collection.options.description ? collection.options.description : `Type for ${collectionName}`
const { mainType, create, update, selector, selectorUnique, orderBy } = fields;
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 }));
}
if (update.length) {
schemaFragments.push(updateInputTemplate({ typeName }));
schemaFragments.push(upsertInputTemplate({ typeName }));
schemaFragments.push(updateDataInputTemplate({ typeName, fields: update }));
}
schemaFragments.push(selectorInputTemplate({ typeName, fields: selector }));
schemaFragments.push(selectorUniqueInputTemplate({ typeName, fields: selectorUnique }));
schemaFragments.push(orderByInputTemplate({ typeName, fields: orderBy }));
2018-06-05 10:17:59 +09:00
const graphQLSchema = schemaFragments.join('\n\n') + `\n\n\n`;
// console.log(graphQLSchema)
return graphQLSchema;
}
};
Vulcan.getGraphQLSchema = () => {
const schema = GraphQLSchema.finalSchema[0];
2018-01-25 15:03:03 -06:00
// eslint-disable-next-line no-console
console.log(schema);
return schema;
}
export const addGraphQLCollection = GraphQLSchema.addCollection.bind(GraphQLSchema);
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);