repo_description: Parse Server for Node.js / Express
repo_topics: baas backend file-storage graphql graphql-api graphql-relay graphql-server hacktoberfest mbaas mongodb nodejs notifications object-storage parse-platform parse-server postgres relay rest-api
code: import Parse from 'parse/node';
  import { GraphQLSchema, GraphQLObjectType, DocumentNode, GraphQLNamedType } from 'graphql';
  import { mergeSchemas } from '@graphql-tools/schema';
  import { mergeTypeDefs } from '@graphql-tools/merge';
  import { isDeepStrictEqual } from 'util';
  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 SchemaCache from '../Adapters/Cache/SchemaCache';
  import { toGraphQLError } from './parseGraphQLUtils';
  import * as schemaDirectives from './loaders/schemaDirectives';
  import * as schemaTypes from './loaders/schemaTypes';
  import { getFunctionNames } from '../triggers';
  import * as defaultRelaySchema from './loaders/defaultRelaySchema';
  
  const RESERVED_GRAPHQL_TYPE_NAMES = [
    'String',
    'Boolean',
    'Int',
    'Float',
    'ID',
    'ArrayResult',
    'Query',
    'Mutation',
    'Subscription',
    'CreateFileInput',
    'CreateFilePayload',
    'Viewer',
    'SignUpInput',
    'SignUpPayload',
    'LogInInput',
    'LogInPayload',
    'LogOutInput',
    'LogOutPayload',
    'CloudCodeFunction',
    'CallCloudCodeInput',
    'CallCloudCodePayload',
    'CreateClassInput',
    'CreateClassPayload',
    'UpdateClassInput',
    'UpdateClassPayload',
    'DeleteClassInput',
    'DeleteClassPayload',
    'PageInfo',
  ];
  const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes', 'cloudConfig'];
  const RESERVED_GRAPHQL_MUTATION_NAMES = [
    'signUp',
    'logIn',
    'logOut',
    'createFile',
    'callCloudCode',
    'createClass',
    'updateClass',
    'deleteClass',
    'updateCloudConfig',
  ];
  
  class ParseGraphQLSchema {
    databaseController: DatabaseController;
    parseGraphQLController: ParseGraphQLController;
    parseGraphQLConfig: ParseGraphQLConfig;
    log: any;
    appId: string;
    graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]);
    schemaCache: any;
  
    constructor(
      params: {
        databaseController: DatabaseController,
        parseGraphQLController: ParseGraphQLController,
        log: any,
        appId: string,
        graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]),
      } = {}
    ) {
      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;
      this.appId = params.appId || requiredParameter('You must provide the appId!');
      this.schemaCache = SchemaCache;
      this.logCache = {};
    }
  
    async load() {
      const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
      const parseClassesArray = await this._getClassesForSchema(parseGraphQLConfig);
      const functionNames = await this._getFunctionNames();
      const functionNamesString = functionNames.join();
  
      const parseClasses = parseClassesArray.reduce((acc, clazz) => {
        acc[clazz.className] = clazz;
        return acc;
      }, {});
      if (
        !this._hasSchemaInputChanged({
          parseClasses,
          parseGraphQLConfig,
          functionNamesString,
        })
      ) {
        return this.graphQLSchema;
      }
  
      this.parseClasses = parseClasses;
      this.parseGraphQLConfig = parseGraphQLConfig;
      this.functionNames = functionNames;
      this.functionNamesString = functionNamesString;
      this.parseClassTypes = {};
      this.viewerType = null;
      this.cloudConfigType = null;
      this.graphQLAutoSchema = null;
      this.graphQLSchema = null;
      this.graphQLTypes = [];
      this.graphQLQueries = {};
      this.graphQLMutations = {};
      this.graphQLSubscriptions = {};
      this.graphQLSchemaDirectivesDefinitions = null;
      this.graphQLSchemaDirectives = {};
      this.relayNodeInterface = null;
  
      defaultGraphQLTypes.load(this);
      defaultRelaySchema.load(this);
      schemaTypes.load(this);
  
      this._getParseClassesWithConfig(parseClassesArray, parseGraphQLConfig).forEach(
        ([parseClass, parseClassConfig]) => {
          // Some times schema return the _auth_data_ field
          // it will lead to unstable graphql generation order
          if (parseClass.className === '_User') {
            Object.keys(parseClass.fields).forEach(fieldName => {
              if (fieldName.startsWith('_auth_data_')) {
                delete parseClass.fields[fieldName];
              }
            });
          }
  
          // Fields order inside the schema seems to not be consistent across
          // restart so we need to ensure an alphabetical order
          // also it's better for the playground documentation
          const orderedFields = {};
          Object.keys(parseClass.fields)
            .sort()
            .forEach(fieldName => {
              orderedFields[fieldName] = parseClass.fields[fieldName];
            });
          parseClass.fields = orderedFields;
          parseClassTypes.load(this, parseClass, parseClassConfig);
          parseClassQueries.load(this, parseClass, parseClassConfig);
          parseClassMutations.load(this, parseClass, parseClassConfig);
        }
      );
  
      defaultGraphQLTypes.loadArrayResult(this, parseClassesArray);
      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);
        if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
          // In following code we use underscore attr to keep the direct variable reference
          const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs._typeMap;
          const findAndReplaceLastType = (parent, key) => {
            if (parent[key].name) {
              if (
                this.graphQLAutoSchema._typeMap[parent[key].name] &&
                this.graphQLAutoSchema._typeMap[parent[key].name] !== parent[key]
              ) {
                // To avoid unresolved field on overloaded schema
                // replace the final type with the auto schema one
                parent[key] = this.graphQLAutoSchema._typeMap[parent[key].name];
              }
            } else {
              if (parent[key].ofType) {
                findAndReplaceLastType(parent[key], 'ofType');
              }
            }
          };
          // Add non shared types from custom schema to auto schema
          // note: some non shared types can use some shared types
          // so this code need to be ran before the shared types addition
          // we use sort to ensure schema consistency over restarts
          Object.keys(customGraphQLSchemaTypeMap)
            .sort()
            .forEach(customGraphQLSchemaTypeKey => {
              const customGraphQLSchemaType = customGraphQLSchemaTypeMap[customGraphQLSchemaTypeKey];
              if (
                !customGraphQLSchemaType ||
                !customGraphQLSchemaType.name ||
                customGraphQLSchemaType.name.startsWith('__')
              ) {
                return;
              }
              const autoGraphQLSchemaType = this.graphQLAutoSchema._typeMap[
                customGraphQLSchemaType.name
              ];
              if (!autoGraphQLSchemaType) {
                this.graphQLAutoSchema._typeMap[
                  customGraphQLSchemaType.name
                ] = customGraphQLSchemaType;
              }
            });
          // Handle shared types
          // We pass through each type and ensure that all sub field types are replaced
          // we use sort to ensure schema consistency over restarts
          Object.keys(customGraphQLSchemaTypeMap)
            .sort()
            .forEach(customGraphQLSchemaTypeKey => {
              const customGraphQLSchemaType = customGraphQLSchemaTypeMap[customGraphQLSchemaTypeKey];
              if (
                !customGraphQLSchemaType ||
                !customGraphQLSchemaType.name ||
                customGraphQLSchemaType.name.startsWith('__')
              ) {
                return;
              }
              const autoGraphQLSchemaType = this.graphQLAutoSchema._typeMap[
                customGraphQLSchemaType.name
              ];
  
              if (autoGraphQLSchemaType && typeof customGraphQLSchemaType.getFields === 'function') {
                Object.keys(customGraphQLSchemaType._fields)
                  .sort()
                  .forEach(fieldKey => {
                    const field = customGraphQLSchemaType._fields[fieldKey];
                    findAndReplaceLastType(field, 'type');
                    autoGraphQLSchemaType._fields[field.name] = field;
                  });
              }
            });
          this.graphQLSchema = this.graphQLAutoSchema;
        } else if (typeof this.graphQLCustomTypeDefs === 'function') {
          this.graphQLSchema = await this.graphQLCustomTypeDefs({
            directivesDefinitionsSchema: this.graphQLSchemaDirectivesDefinitions,
            autoSchema: this.graphQLAutoSchema,
            graphQLSchemaDirectives: this.graphQLSchemaDirectives,
          });
        } else {
          this.graphQLSchema = mergeSchemas({
            schemas: [this.graphQLAutoSchema],
            typeDefs: mergeTypeDefs([
              this.graphQLCustomTypeDefs,
              this.graphQLSchemaDirectivesDefinitions,
            ]),
          });
          this.graphQLSchema = this.graphQLSchemaDirectives(this.graphQLSchema);
        }
      } else {
        this.graphQLSchema = this.graphQLAutoSchema;
      }
  
      return this.graphQLSchema;
    }
  
    _logOnce(severity, message) {
      if (this.logCache[message]) {
        return;
      }
      this.log[severity](message);
      this.logCache[message] = true;
    }
  
    addGraphQLType(type, throwError = false, ignoreReserved = false, ignoreConnection = false) {
      if (
        (!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
        this.graphQLTypes.find(existingType => existingType.name === type.name) ||
        (!ignoreConnection && type.name.endsWith('Connection'))
      ) {
        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._logOnce('warn', message);
        return undefined;
      }
      this.graphQLTypes.push(type);
      return type;
    }
  
    addGraphQLQuery(fieldName, field, throwError = false, ignoreReserved = false) {
      if (
        (!ignoreReserved && RESERVED_GRAPHQL_QUERY_NAMES.includes(fieldName)) ||
        this.graphQLQueries[fieldName]
      ) {
        const message = `Query ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
        if (throwError) {
          throw new Error(message);
        }
        this._logOnce('warn', message);
        return undefined;
      }
      this.graphQLQueries[fieldName] = field;
      return field;
    }
  
    addGraphQLMutation(fieldName, field, throwError = false, ignoreReserved = false) {
      if (
        (!ignoreReserved && RESERVED_GRAPHQL_MUTATION_NAMES.includes(fieldName)) ||
        this.graphQLMutations[fieldName]
      ) {
        const message = `Mutation ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
        if (throwError) {
          throw new Error(message);
        }
        this._logOnce('warn', message);
        return undefined;
      }
      this.graphQLMutations[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];
      });
    }
  
    async _getFunctionNames() {
      return await getFunctionNames(this.appId).filter(functionName => {
        if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
          return true;
        } else {
          this._logOnce(
            'warn',
            `Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
          );
          return false;
        }
      });
    }
  
    /**
     * 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,
      parseGraphQLConfig: ?ParseGraphQLConfig,
      functionNamesString: string,
    }): boolean {
      const { parseClasses, parseGraphQLConfig, functionNamesString } = params;
  
      // First init
      if (!this.graphQLSchema) {
        return true;
      }
  
      if (
        isDeepStrictEqual(this.parseGraphQLConfig, parseGraphQLConfig) &&
        this.functionNamesString === functionNamesString &&
        isDeepStrictEqual(this.parseClasses, parseClasses)
      ) {
        return false;
      }
      return true;
    }
  }
  
  export { ParseGraphQLSchema };
  