mirror of
https://github.com/vale981/Vulcan
synced 2025-03-07 02:21:43 -05:00
523 lines
16 KiB
JavaScript
523 lines
16 KiB
JavaScript
// TODO: this should not be loaded on the client?
|
|
|
|
/*
|
|
|
|
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';
|
|
import {isIntlField} from './intl.js';
|
|
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';
|
|
|
|
disableFragmentWarnings();
|
|
|
|
// get GraphQL type for a given schema and field name
|
|
const getGraphQLType = (schema, fieldName, isInput = false) => {
|
|
const field = schema[fieldName];
|
|
const type = field.type.singleType;
|
|
const typeName =
|
|
typeof type === 'object'
|
|
? 'Object'
|
|
: typeof type === 'function'
|
|
? type.name
|
|
: type;
|
|
|
|
if (field.isIntlData) {
|
|
return isInput ? '[IntlValueInput]' : '[IntlValue]';
|
|
}
|
|
|
|
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('');
|
|
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);
|
|
},
|
|
|
|
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);
|
|
const inputFieldType = getGraphQLType(schema, fieldName, true);
|
|
|
|
// 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
|
|
// OpenCRUD backwards compatibility
|
|
if (
|
|
(field.canRead ||
|
|
field.canCreate ||
|
|
field.canUpdate ||
|
|
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
|
|
if (field.resolveAs) {
|
|
// get resolver name from resolveAs object, or else default to field name
|
|
const resolverName = field.resolveAs.fieldName || fieldName;
|
|
|
|
// 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,
|
|
args: field.resolveAs.arguments,
|
|
type: fieldGraphQLType,
|
|
});
|
|
|
|
// then build actual resolver object and pass it to addGraphQLResolvers
|
|
const resolver = {
|
|
[typeName]: {
|
|
[resolverName]: (document, args, context, info) => {
|
|
const {Users, currentUser} = context;
|
|
// check that current user has permission to access the original non-resolved field
|
|
const canReadField = Users.canReadField(
|
|
currentUser,
|
|
field,
|
|
document
|
|
);
|
|
return canReadField
|
|
? field.resolveAs.resolver(document, args, context, info)
|
|
: null;
|
|
},
|
|
},
|
|
};
|
|
addGraphQLResolvers(resolver);
|
|
|
|
// if addOriginalField option is enabled, also add original field to schema
|
|
if (field.resolveAs.addOriginalField && fieldType) {
|
|
fields.mainType.push({
|
|
description: fieldDescription,
|
|
name: fieldName,
|
|
args: fieldArguments,
|
|
type: fieldType,
|
|
directive: fieldDirective,
|
|
});
|
|
}
|
|
} else {
|
|
// try to guess GraphQL type
|
|
if (fieldType) {
|
|
fields.mainType.push({
|
|
description: fieldDescription,
|
|
name: fieldName,
|
|
args: fieldArguments,
|
|
type: fieldType,
|
|
directive: fieldDirective,
|
|
});
|
|
}
|
|
}
|
|
|
|
// OpenCRUD backwards compatibility
|
|
if (field.canCreate || field.insertableBy) {
|
|
fields.create.push({
|
|
name: fieldName,
|
|
type: inputFieldType,
|
|
required: !field.optional,
|
|
});
|
|
}
|
|
// OpenCRUD backwards compatibility
|
|
if (field.canUpdate || field.editableBy) {
|
|
fields.update.push({
|
|
name: fieldName,
|
|
type: inputFieldType,
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
fields.selector.push({
|
|
name: fieldName,
|
|
type: inputFieldType,
|
|
});
|
|
}
|
|
|
|
if (field.selectable && field.unique) {
|
|
fields.selectorUnique.push({
|
|
name: fieldName,
|
|
type: inputFieldType,
|
|
});
|
|
}
|
|
|
|
if (field.orderable) {
|
|
fields.orderBy.push(fieldName);
|
|
}
|
|
}
|
|
});
|
|
return fields;
|
|
},
|
|
|
|
// generate a GraphQL schema corresponding to a given collection
|
|
generateSchema(collection) {
|
|
let graphQLSchema = '';
|
|
|
|
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 = [], resolvers, mutations} = collection.options;
|
|
|
|
const description = collection.options.description
|
|
? collection.options.description
|
|
: `Type for ${collectionName}`;
|
|
|
|
const {
|
|
mainType,
|
|
create,
|
|
update,
|
|
selector,
|
|
selectorUnique,
|
|
orderBy,
|
|
} = fields;
|
|
|
|
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})
|
|
);
|
|
}
|
|
|
|
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}));
|
|
|
|
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(Utils.pluralize(typeName))
|
|
] = resolvers.multi.resolver.bind(resolvers.multi);
|
|
}
|
|
addGraphQLResolvers({Query: {...queryResolvers}});
|
|
}
|
|
|
|
if (!_.isEmpty(mutations)) {
|
|
const mutationResolvers = {};
|
|
// create
|
|
if (mutations.create) {
|
|
// e.g. "createMovie(input: CreateMovieInput) : Movie"
|
|
if (create.length === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`// Warning: you defined a "create" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "create" mutation or define a "canCreate" property on a field to disable this warning`
|
|
);
|
|
} else {
|
|
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"
|
|
if (update.length === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`// Warning: you defined an "update" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "update" mutation or define a "canUpdate" property on a field to disable this warning`
|
|
);
|
|
} else {
|
|
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"
|
|
if (update.length === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`// Warning: you defined an "upsert" mutation for collection ${collectionName}, but it doesn't have any mutable fields, so no corresponding mutation types can be generated. Remove the "upsert" mutation or define a "canUpdate" property on a field to disable this warning`
|
|
);
|
|
} else {
|
|
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';
|
|
} 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`
|
|
);
|
|
}
|
|
|
|
return graphQLSchema;
|
|
},
|
|
|
|
// getters
|
|
getSchema() {
|
|
if (!(this.finalSchema && this.finalSchema.length)) {
|
|
throw new Error(
|
|
'Warning: trying to access schema before it has been created by the server.'
|
|
);
|
|
}
|
|
return this.finalSchema[0];
|
|
},
|
|
getExecutableSchema() {
|
|
if (!this.executableSchema) {
|
|
throw new Error(
|
|
'Warning: trying to access executable schema before it has been created by the server.'
|
|
);
|
|
}
|
|
return this.executableSchema;
|
|
},
|
|
};
|
|
|
|
Vulcan.getGraphQLSchema = () => {
|
|
if (!GraphQLSchema.finalSchema) {
|
|
throw new Error(
|
|
'Warning: trying to access graphQL schema before it has been created by the server.'
|
|
);
|
|
}
|
|
const schema = GraphQLSchema.finalSchema[0];
|
|
// 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
|
|
);
|
|
export const removeGraphQLResolver = GraphQLSchema.removeResolver.bind(
|
|
GraphQLSchema
|
|
);
|
|
export const addToGraphQLContext = GraphQLSchema.addToContext.bind(
|
|
GraphQLSchema
|
|
);
|
|
export const addGraphQLDirective = GraphQLSchema.addDirective.bind(
|
|
GraphQLSchema
|
|
);
|