mirror of
https://github.com/vale981/Vulcan
synced 2025-03-09 12:16:37 -04:00
247 lines
No EOL
7.1 KiB
JavaScript
247 lines
No EOL
7.1 KiB
JavaScript
/*
|
|
|
|
Utilities to generate the app's GraphQL schema
|
|
|
|
*/
|
|
|
|
import deepmerge from 'deepmerge';
|
|
import GraphQLJSON from 'graphql-type-json';
|
|
import GraphQLDate from 'graphql-date';
|
|
import Vulcan from './config.js'; // used for global export
|
|
import { Utils } from './utils.js';
|
|
import { disableFragmentWarnings } from 'graphql-tag';
|
|
|
|
disableFragmentWarnings();
|
|
|
|
// get GraphQL type for a given schema and field name
|
|
const getGraphQLType = (schema, fieldName) => {
|
|
|
|
const field = schema[fieldName];
|
|
const type = field.type.singleType;
|
|
const typeName = 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: [],
|
|
addCollection(collection) {
|
|
this.collections.push(collection);
|
|
},
|
|
// generate GraphQL schemas for all registered collections
|
|
getCollectionsSchemas() {
|
|
const collectionsSchemas = this.collections.map(collection => {
|
|
return this.generateSchema(collection);
|
|
}).join('\n');
|
|
return collectionsSchemas;
|
|
},
|
|
|
|
// additional schemas
|
|
schemas: [],
|
|
addSchema(schema) {
|
|
this.schemas.push(schema);
|
|
},
|
|
// get extra schemas defined manually
|
|
getAdditionalSchemas() {
|
|
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,
|
|
},
|
|
addResolvers(resolvers) {
|
|
this.resolvers = deepmerge(this.resolvers, resolvers);
|
|
},
|
|
removeResolver(typeName, resolverName) {
|
|
delete this.resolvers[typeName][resolverName];
|
|
},
|
|
|
|
// add objects to context
|
|
context: {},
|
|
addToContext(object) {
|
|
this.context = deepmerge(this.context, object);
|
|
},
|
|
|
|
// generate a GraphQL schema corresponding to a given collection
|
|
generateSchema(collection) {
|
|
|
|
const collectionName = collection.options.collectionName;
|
|
|
|
const mainTypeName = collection.typeName ? collection.typeName : Utils.camelToSpaces(_.initial(collectionName).join('')); // default to posts -> Post
|
|
|
|
// backward-compatibility code: we do not want user.telescope fields in the graphql schema
|
|
const schema = Utils.stripTelescopeNamespace(collection.simpleSchema()._schema);
|
|
|
|
let mainSchema = [], inputSchema = [], unsetSchema = [], graphQLSchema = '';
|
|
|
|
_.forEach(schema, (field, fieldName) => {
|
|
// console.log(field, 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 ? `# ${field.description}` : '';
|
|
|
|
// if field has a resolveAs, push it to schema
|
|
if (field.resolveAs) {
|
|
|
|
if (typeof field.resolveAs === 'string') {
|
|
// if resolveAs is a string, push it and done
|
|
mainSchema.push(field.resolveAs);
|
|
} else {
|
|
|
|
// get resolver name from resolveAs object, or else default to field name
|
|
const resolverName = field.resolveAs.fieldName || fieldName;
|
|
|
|
// if resolveAs is an object, first push its type definition
|
|
// include arguments if there are any
|
|
mainSchema.push(`${resolverName}${field.resolveAs.arguments ? `(${field.resolveAs.arguments})` : ''}: ${field.resolveAs.type}`);
|
|
|
|
// then build actual resolver object and pass it to addGraphQLResolvers
|
|
const resolver = {
|
|
[mainTypeName]: {
|
|
[resolverName]: field.resolveAs.resolver
|
|
}
|
|
};
|
|
addGraphQLResolvers(resolver);
|
|
}
|
|
|
|
// if addOriginalField option is enabled, also add original field to schema
|
|
if (field.resolveAs.addOriginalField && fieldType) {
|
|
mainSchema.push(
|
|
`${fieldDescription}
|
|
${fieldName}: ${fieldType}
|
|
`);
|
|
}
|
|
|
|
} else {
|
|
// try to guess GraphQL type
|
|
if (fieldType) {
|
|
mainSchema.push(
|
|
`${fieldDescription}
|
|
${fieldName}: ${fieldType}
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (field.insertableBy || field.editableBy) {
|
|
|
|
// note: marking a field as required makes it required for updates, too,
|
|
// which makes partial updates impossible
|
|
// const isRequired = field.optional ? '' : '!';
|
|
|
|
const isRequired = '';
|
|
|
|
// 2. input schema
|
|
inputSchema.push(`${fieldName}: ${fieldType}${isRequired}`);
|
|
|
|
// 3. unset schema
|
|
unsetSchema.push(`${fieldName}: Boolean`);
|
|
|
|
}
|
|
}
|
|
});
|
|
|
|
const { interfaces = [] } = collection.options;
|
|
const graphQLInterfaces = interfaces.length ? `implements ${interfaces.join(`, `)} ` : '';
|
|
|
|
const description = collection.options.description ? collection.options.description : `Type for ${collectionName}`
|
|
|
|
if (mainSchema.length) {
|
|
|
|
graphQLSchema +=
|
|
`# ${description}
|
|
type ${mainTypeName} ${graphQLInterfaces}{
|
|
${mainSchema.join('\n ')}
|
|
}
|
|
`
|
|
}
|
|
|
|
if (inputSchema.length) {
|
|
graphQLSchema +=
|
|
`# ${description} (input type)
|
|
input ${collectionName}Input {
|
|
${inputSchema.join('\n ')}
|
|
}
|
|
`
|
|
}
|
|
|
|
if (unsetSchema.length) {
|
|
graphQLSchema +=
|
|
`# ${description} (unset input type)
|
|
input ${collectionName}Unset {
|
|
${unsetSchema.join('\n ')}
|
|
}
|
|
`
|
|
}
|
|
|
|
return graphQLSchema;
|
|
}
|
|
};
|
|
|
|
Vulcan.getGraphQLSchema = () => {
|
|
const schema = GraphQLSchema.finalSchema[0];
|
|
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);
|
|
export const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(GraphQLSchema);
|
|
export const addToGraphQLContext = GraphQLSchema.addToContext.bind(GraphQLSchema); |