import Parse from 'parse/node'; import { GraphQLSchema, GraphQLObjectType } from 'graphql'; import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools'; import requiredParameter from '../requiredParameter'; import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes'; import * as parseClassTypes from './loaders/parseClassTypes'; import * as parseClassQueries from './loaders/parseClassQueries'; import * as parseClassMutations from './loaders/parseClassMutations'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; import ParseGraphQLController, { ParseGraphQLConfig, } from '../Controllers/ParseGraphQLController'; import DatabaseController from '../Controllers/DatabaseController'; import { toGraphQLError } from './parseGraphQLUtils'; import * as schemaDirectives from './loaders/schemaDirectives'; const RESERVED_GRAPHQL_TYPE_NAMES = [ 'String', 'Boolean', 'Int', 'Float', 'ID', 'ArrayResult', 'Query', 'Mutation', 'Subscription', 'ObjectsQuery', 'UsersQuery', 'ObjectsMutation', 'FilesMutation', 'UsersMutation', 'FunctionsMutation', 'Viewer', 'SignUpFieldsInput', 'LogInFieldsInput', ]; const RESERVED_GRAPHQL_OBJECT_QUERY_NAMES = ['get', 'find']; const RESERVED_GRAPHQL_OBJECT_MUTATION_NAMES = ['create', 'update', 'delete']; class ParseGraphQLSchema { databaseController: DatabaseController; parseGraphQLController: ParseGraphQLController; parseGraphQLConfig: ParseGraphQLConfig; graphQLCustomTypeDefs: any; constructor( params: { databaseController: DatabaseController, parseGraphQLController: ParseGraphQLController, log: any, } = {} ) { this.parseGraphQLController = params.parseGraphQLController || requiredParameter('You must provide a parseGraphQLController instance!'); this.databaseController = params.databaseController || requiredParameter('You must provide a databaseController instance!'); this.log = params.log || requiredParameter('You must provide a log instance!'); this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; } async load() { const { parseGraphQLConfig } = await this._initializeSchemaAndConfig(); const parseClasses = await this._getClassesForSchema(parseGraphQLConfig); const parseClassesString = JSON.stringify(parseClasses); if ( this.graphQLSchema && !this._hasSchemaInputChanged({ parseClasses, parseClassesString, parseGraphQLConfig, }) ) { return this.graphQLSchema; } this.parseClasses = parseClasses; this.parseClassesString = parseClassesString; this.parseGraphQLConfig = parseGraphQLConfig; this.parseClassTypes = {}; this.viewerType = null; this.graphQLAutoSchema = null; this.graphQLSchema = null; this.graphQLTypes = []; this.graphQLObjectsQueries = {}; this.graphQLQueries = {}; this.graphQLObjectsMutations = {}; this.graphQLMutations = {}; this.graphQLSubscriptions = {}; this.graphQLSchemaDirectivesDefinitions = null; this.graphQLSchemaDirectives = {}; defaultGraphQLTypes.load(this); this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( ([parseClass, parseClassConfig]) => { parseClassTypes.load(this, parseClass, parseClassConfig); parseClassQueries.load(this, parseClass, parseClassConfig); parseClassMutations.load(this, parseClass, parseClassConfig); } ); defaultGraphQLTypes.loadArrayResult(this, parseClasses); defaultGraphQLQueries.load(this); defaultGraphQLMutations.load(this); let graphQLQuery = undefined; if (Object.keys(this.graphQLQueries).length > 0) { graphQLQuery = new GraphQLObjectType({ name: 'Query', description: 'Query is the top level type for queries.', fields: this.graphQLQueries, }); this.addGraphQLType(graphQLQuery, true, true); } let graphQLMutation = undefined; if (Object.keys(this.graphQLMutations).length > 0) { graphQLMutation = new GraphQLObjectType({ name: 'Mutation', description: 'Mutation is the top level type for mutations.', fields: this.graphQLMutations, }); this.addGraphQLType(graphQLMutation, true, true); } let graphQLSubscription = undefined; if (Object.keys(this.graphQLSubscriptions).length > 0) { graphQLSubscription = new GraphQLObjectType({ name: 'Subscription', description: 'Subscription is the top level type for subscriptions.', fields: this.graphQLSubscriptions, }); this.addGraphQLType(graphQLSubscription, true, true); } this.graphQLAutoSchema = new GraphQLSchema({ types: this.graphQLTypes, query: graphQLQuery, mutation: graphQLMutation, subscription: graphQLSubscription, }); if (this.graphQLCustomTypeDefs) { schemaDirectives.load(this); this.graphQLSchema = mergeSchemas({ schemas: [ this.graphQLSchemaDirectivesDefinitions, this.graphQLAutoSchema, this.graphQLCustomTypeDefs, ], mergeDirectives: true, }); const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap(); Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => { const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName]; if (typeof graphQLSchemaType.getFields === 'function') { const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find( definition => definition.name.value === graphQLSchemaTypeName ); if (graphQLCustomTypeDef) { const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields(); Object.keys(graphQLSchemaTypeFieldMap).forEach( graphQLSchemaTypeFieldName => { const graphQLSchemaTypeField = graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName]; if (!graphQLSchemaTypeField.astNode) { const astNode = graphQLCustomTypeDef.fields.find( field => field.name.value === graphQLSchemaTypeFieldName ); if (astNode) { graphQLSchemaTypeField.astNode = astNode; } } } ); } } }); SchemaDirectiveVisitor.visitSchemaDirectives( this.graphQLSchema, this.graphQLSchemaDirectives ); } else { this.graphQLSchema = this.graphQLAutoSchema; } return this.graphQLSchema; } addGraphQLType(type, throwError = false, ignoreReserved = false) { if ( (!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) || this.graphQLTypes.find(existingType => existingType.name === type.name) ) { const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`; if (throwError) { throw new Error(message); } this.log.warn(message); return undefined; } this.graphQLTypes.push(type); return type; } addGraphQLObjectQuery( fieldName, field, throwError = false, ignoreReserved = false ) { if ( (!ignoreReserved && RESERVED_GRAPHQL_OBJECT_QUERY_NAMES.includes(fieldName)) || this.graphQLObjectsQueries[fieldName] ) { const message = `Object query ${fieldName} could not be added to the auto schema because it collided with an existing field.`; if (throwError) { throw new Error(message); } this.log.warn(message); return undefined; } this.graphQLObjectsQueries[fieldName] = field; return field; } addGraphQLObjectMutation( fieldName, field, throwError = false, ignoreReserved = false ) { if ( (!ignoreReserved && RESERVED_GRAPHQL_OBJECT_MUTATION_NAMES.includes(fieldName)) || this.graphQLObjectsMutations[fieldName] ) { const message = `Object mutation ${fieldName} could not be added to the auto schema because it collided with an existing field.`; if (throwError) { throw new Error(message); } this.log.warn(message); return undefined; } this.graphQLObjectsMutations[fieldName] = field; return field; } handleError(error) { if (error instanceof Parse.Error) { this.log.error('Parse error: ', error); } else { this.log.error('Uncaught internal server error.', error, error.stack); } throw toGraphQLError(error); } async _initializeSchemaAndConfig() { const [schemaController, parseGraphQLConfig] = await Promise.all([ this.databaseController.loadSchema(), this.parseGraphQLController.getGraphQLConfig(), ]); this.schemaController = schemaController; return { parseGraphQLConfig, }; } /** * Gets all classes found by the `schemaController` * minus those filtered out by the app's parseGraphQLConfig. */ async _getClassesForSchema(parseGraphQLConfig: ParseGraphQLConfig) { const { enabledForClasses, disabledForClasses } = parseGraphQLConfig; const allClasses = await this.schemaController.getAllClasses(); if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) { let includedClasses = allClasses; if (enabledForClasses) { includedClasses = allClasses.filter(clazz => { return enabledForClasses.includes(clazz.className); }); } if (disabledForClasses) { // Classes included in `enabledForClasses` that // are also present in `disabledForClasses` will // still be filtered out includedClasses = includedClasses.filter(clazz => { return !disabledForClasses.includes(clazz.className); }); } this.isUsersClassDisabled = !includedClasses.some(clazz => { return clazz.className === '_User'; }); return includedClasses; } else { return allClasses; } } /** * This method returns a list of tuples * that provide the parseClass along with * its parseClassConfig where provided. */ _getParseClassesWithConfig( parseClasses, parseGraphQLConfig: ParseGraphQLConfig ) { const { classConfigs } = parseGraphQLConfig; // Make sures that the default classes and classes that // starts with capitalized letter will be generated first. const sortClasses = (a, b) => { a = a.className; b = b.className; if (a[0] === '_') { if (b[0] !== '_') { return -1; } } if (b[0] === '_') { if (a[0] !== '_') { return 1; } } if (a === b) { return 0; } else if (a < b) { return -1; } else { return 1; } }; return parseClasses.sort(sortClasses).map(parseClass => { let parseClassConfig; if (classConfigs) { parseClassConfig = classConfigs.find( c => c.className === parseClass.className ); } return [parseClass, parseClassConfig]; }); } /** * Checks for changes to the parseClasses * objects (i.e. database schema) or to * the parseGraphQLConfig object. If no * changes are found, return true; */ _hasSchemaInputChanged(params: { parseClasses: any, parseClassesString: string, parseGraphQLConfig: ?ParseGraphQLConfig, }): boolean { const { parseClasses, parseClassesString, parseGraphQLConfig } = params; if ( JSON.stringify(this.parseGraphQLConfig) === JSON.stringify(parseGraphQLConfig) ) { if (this.parseClasses === parseClasses) { return false; } if (this.parseClassesString === parseClassesString) { this.parseClasses = parseClasses; return false; } } return true; } } export { ParseGraphQLSchema };