graphql#specifiedDirectives TypeScript Examples

The following examples show how to use graphql#specifiedDirectives. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: build-federated-schema.ts    From opensaas with MIT License 6 votes vote down vote up
export async function buildFederatedSchema(
  options: Omit<BuildSchemaOptions, 'skipCheck'>,
  referenceResolvers?: GraphQLResolverMap<any>,
) {
  const schema = await buildSchema({
    ...options,
    directives: [...specifiedDirectives, ...federationDirectives, ...(options.directives || [])],
    skipCheck: true,
  });

  const federatedSchema = buildApolloFederationSchema({
    typeDefs: gql(printSchema(schema)),
    resolvers: createResolversMap(schema) as any,
  });

  if (referenceResolvers) {
    addResolversToSchema(federatedSchema, referenceResolvers);
  }
  return federatedSchema;
}
Example #2
Source File: typegraphql-options-federation.factory.ts    From typegraphql-nestjs with MIT License 5 votes vote down vote up
async createGqlOptions(): Promise<GqlModuleOptions> {
    const { globalMiddlewares } = this.rootModuleOptions;
    const {
      resolversClasses,
      container,
      orphanedTypes,
      featureModuleOptionsArray,
    } = this.optionsPreparatorService.prepareOptions<TypeGraphQLFeatureFedarationModuleOptions>(
      TYPEGRAPHQL_FEATURE_FEDERATION_MODULE_OPTIONS,
      globalMiddlewares,
    );

    const referenceResolversArray = [...featureModuleOptionsArray].filter(
      it => it.referenceResolvers,
    );

    const referenceResolvers =
      referenceResolversArray.length > 0
        ? Object.fromEntries(
            referenceResolversArray.flatMap(it =>
              Object.entries(it.referenceResolvers!),
            ),
          )
        : undefined;

    const baseSchema = await buildSchema({
      ...this.rootModuleOptions,
      directives: [...specifiedDirectives, ...federationDirectives],
      resolvers: resolversClasses as NonEmptyArray<ClassType>,
      orphanedTypes,
      container,
    });

    const schema = buildFederatedSchema({
      typeDefs: gql(printSchema(baseSchema)),
      resolvers: createResolversMap(baseSchema) as GraphQLResolverMap<any>,
    });

    if (referenceResolvers) {
      addResolversToSchema(schema, referenceResolvers);
    }

    return {
      ...this.rootModuleOptions,
      schema,
    };
  }
Example #3
Source File: index.ts    From graphql-mesh with MIT License 5 votes vote down vote up
async getMeshSource() {
    this.config.requestTimeout = this.config.requestTimeout || 200000;

    this.schemaComposer.add(GraphQLBigInt);
    this.schemaComposer.add(GraphQLByte);
    this.schemaComposer.add(GraphQLUnsignedInt);
    this.schemaComposer.add(GraphQLVoid);
    this.schemaComposer.add(GraphQLJSON);
    // identical of grpc's ConnectivityState
    this.schemaComposer.createEnumTC({
      name: 'ConnectivityState',
      values: {
        IDLE: { value: 0 },
        CONNECTING: { value: 1 },
        READY: { value: 2 },
        TRANSIENT_FAILURE: { value: 3 },
        SHUTDOWN: { value: 4 },
      },
    });

    this.logger.debug(`Getting channel credentials`);
    const creds = await this.getCredentials();

    this.logger.debug(`Getting stored root and decoded descriptor set objects`);
    const artifacts = await this.getCachedDescriptorSets(creds);

    for (const { name, rootJson, decodedDescriptorSet } of artifacts) {
      const rootLogger = this.logger.child(name);

      rootLogger.debug(`Creating package definition from file descriptor set object`);
      const packageDefinition = loadFileDescriptorSetFromObject(decodedDescriptorSet);

      rootLogger.debug(`Creating service client for package definition`);
      const grpcObject = loadPackageDefinition(packageDefinition);

      this.logger.debug(`Building the schema structure based on the root object`);
      this.visit({ nested: rootJson, name: '', currentPath: [], rootJson, creds, grpcObject, rootLogger });
    }

    // graphql-compose doesn't add @defer and @stream to the schema
    specifiedDirectives.forEach(directive => this.schemaComposer.addDirective(directive));

    this.logger.debug(`Building the final GraphQL Schema`);
    const schema = this.schemaComposer.buildSchema();

    return {
      schema,
    };
  }
Example #4
Source File: getGraphQLSchemaFromDereferencedJSONSchema.ts    From graphql-mesh with MIT License 5 votes vote down vote up
export async function getGraphQLSchemaFromDereferencedJSONSchema(
  fullyDeferencedSchema: JSONSchemaObject,
  {
    fetch,
    logger,
    operations,
    operationHeaders,
    baseUrl,
    pubsub,
    generateInterfaceFromSharedFields,
    queryParams,
  }: AddExecutionLogicToComposerOptions & { generateInterfaceFromSharedFields?: boolean }
) {
  logger.debug(`Generating GraphQL Schema from the bundled JSON Schema`);
  const visitorResult = await getComposerFromJSONSchema(
    fullyDeferencedSchema,
    logger,
    generateInterfaceFromSharedFields
  );

  const schemaComposerWithoutExecutionLogic = visitorResult.output;

  if (!(schemaComposerWithoutExecutionLogic instanceof SchemaComposer)) {
    throw new Error('The visitor result should be a SchemaComposer instance.');
  }

  // graphql-compose doesn't add @defer and @stream to the schema
  for (const directive of specifiedDirectives) {
    schemaComposerWithoutExecutionLogic.addDirective(directive);
  }

  const schemaComposerWithExecutionLogic = await addExecutionLogicToComposer(schemaComposerWithoutExecutionLogic, {
    fetch,
    logger,
    operations,
    operationHeaders,
    baseUrl,
    pubsub,
    queryParams,
  });

  if (schemaComposerWithExecutionLogic.Query.getFieldNames().length === 0) {
    schemaComposerWithExecutionLogic.Query.addFields({
      dummy: {
        type: 'String',
        resolve: () => 'dummy',
      },
    });
  }

  return schemaComposerWithExecutionLogic.buildSchema();
}
Example #5
Source File: index.ts    From graphql-mesh with MIT License 4 votes vote down vote up
async getMeshSource(): Promise<MeshSource> {
    if (this.config.connectionString) {
      connect(this.config.connectionString, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
      } as ConnectOptions).catch(e => console.error(e));

      await this.pubsub.subscribe('destroy', () => disconnect());
    }

    const schemaComposer = new SchemaComposer();
    await Promise.all([
      Promise.all(
        this.config.models?.map(async modelConfig => {
          const model = await loadFromModuleExportExpression<Model<Document<any, any, any>>>(modelConfig.path, {
            defaultExportName: modelConfig.name,
            cwd: this.baseDir,
            importFn: this.importFn,
          });
          if (!model) {
            throw new Error(`Model ${modelConfig.name} cannot be imported ${modelConfig.path}!`);
          }
          const modelTC = composeWithMongoose(model, modelConfig.options as any);
          await Promise.all([
            Promise.all(
              modelQueryOperations.map(async queryOperation =>
                schemaComposer.Query.addFields({
                  [`${modelConfig.name}_${queryOperation}`]: modelTC.getResolver(queryOperation),
                })
              )
            ),
            Promise.all(
              modelMutationOperations.map(async mutationOperation =>
                schemaComposer.Mutation.addFields({
                  [`${modelConfig.name}_${mutationOperation}`]: modelTC.getResolver(mutationOperation),
                })
              )
            ),
          ]);
          if (this.config.autoTypeMerging) {
            modelTC.setDirectiveByName('key', {
              selectionSet: /* GraphQL */ `
                {
                  id
                }
              `,
            });
            modelTC.setFieldDirectiveByName(`${modelConfig.name}_dataLoaderMany`, 'merge');
          }
        }) || []
      ),
      Promise.all(
        this.config.discriminators?.map(async discriminatorConfig => {
          const discriminator = await loadFromModuleExportExpression<any>(discriminatorConfig.path, {
            defaultExportName: discriminatorConfig.name,
            cwd: this.baseDir,
            importFn: this.importFn,
          });
          const discriminatorTC = composeWithMongooseDiscriminators(discriminator, discriminatorConfig.options as any);
          await Promise.all([
            Promise.all(
              modelQueryOperations.map(async queryOperation =>
                schemaComposer.Query.addFields({
                  [`${discriminatorConfig.name}_${queryOperation}`]: discriminatorTC.getResolver(queryOperation),
                })
              )
            ),
            Promise.all(
              modelMutationOperations.map(async mutationOperation =>
                schemaComposer.Mutation.addFields({
                  [`${discriminatorConfig.name}_${mutationOperation}`]: discriminatorTC.getResolver(mutationOperation),
                })
              )
            ),
          ]);
          if (this.config.autoTypeMerging) {
            discriminatorTC.setDirectiveByName('key', {
              selectionSet: /* GraphQL */ `
                {
                  id
                }
              `,
            });
            discriminatorTC.setFieldDirectiveByName(`${discriminatorConfig.name}_dataLoaderMany`, 'merge');
          }
        }) || []
      ),
    ]);

    // graphql-compose doesn't add @defer and @stream to the schema
    specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive));

    if (this.config.autoTypeMerging) {
      const defaultStitchingDirectives = stitchingDirectives();
      defaultStitchingDirectives.allStitchingDirectives.forEach(directive => schemaComposer.addDirective(directive));
    }

    const schema = schemaComposer.buildSchema();

    return {
      schema,
    };
  }
Example #6
Source File: index.ts    From graphql-mesh with MIT License 4 votes vote down vote up
async getMeshSource(): Promise<MeshSource> {
    const { pool: configPool } = this.config;
    const schemaComposer = new SchemaComposer<MysqlContext>();
    const pool: Pool = configPool
      ? typeof configPool === 'string'
        ? await loadFromModuleExportExpression(configPool, {
            cwd: this.baseDir,
            defaultExportName: 'default',
            importFn: this.importFn,
          })
        : configPool
      : createPool({
          supportBigNumbers: true,
          bigNumberStrings: true,
          dateStrings: true,
          trace: !!process.env.DEBUG,
          debug: !!process.env.DEBUG,
          host: this.config.host && stringInterpolator.parse(this.config.host, { env: process.env }),
          port:
            this.config.port && parseInt(stringInterpolator.parse(this.config.port.toString(), { env: process.env })),
          user: this.config.user && stringInterpolator.parse(this.config.user, { env: process.env }),
          password: this.config.password && stringInterpolator.parse(this.config.password, { env: process.env }),
          database: this.config.database && stringInterpolator.parse(this.config.database, { env: process.env }),
          ...this.config,
        });

    pool.on('connection', connection => {
      upgrade(connection);
      introspection(connection);
    });

    const introspectionConnection = this.getCachedIntrospectionConnection(pool);

    schemaComposer.add(GraphQLBigInt);
    schemaComposer.add(GraphQLJSON);
    schemaComposer.add(GraphQLDate);
    schemaComposer.add(GraphQLTime);
    schemaComposer.add(GraphQLDateTime);
    schemaComposer.add(GraphQLTimestamp);
    schemaComposer.add(GraphQLUnsignedInt);
    schemaComposer.add(GraphQLUnsignedFloat);
    schemaComposer.createEnumTC({
      name: 'OrderBy',
      values: {
        asc: {
          value: 'asc',
        },
        desc: {
          value: 'desc',
        },
      },
    });
    const tables = await introspectionConnection.getDatabaseTables(pool.config.connectionConfig.database);
    const tableNames = this.config.tables || Object.keys(tables);
    await Promise.all(
      tableNames.map(async tableName => {
        if (this.config.tables && !this.config.tables.includes(tableName)) {
          return;
        }
        const table = tables[tableName];
        const objectTypeName = sanitizeNameForGraphQL(table.TABLE_NAME);
        const insertInputName = sanitizeNameForGraphQL(table.TABLE_NAME + '_InsertInput');
        const updateInputName = sanitizeNameForGraphQL(table.TABLE_NAME + '_UpdateInput');
        const whereInputName = sanitizeNameForGraphQL(table.TABLE_NAME + '_WhereInput');
        const orderByInputName = sanitizeNameForGraphQL(table.TABLE_NAME + '_OrderByInput');
        const tableTC = schemaComposer.createObjectTC({
          name: objectTypeName,
          description: table.TABLE_COMMENT,
          extensions: table,
          fields: {},
        });
        const tableInsertIC = schemaComposer.createInputTC({
          name: insertInputName,
          description: table.TABLE_COMMENT,
          extensions: table,
          fields: {},
        });
        const tableUpdateIC = schemaComposer.createInputTC({
          name: updateInputName,
          description: table.TABLE_COMMENT,
          extensions: table,
          fields: {},
        });
        const tableWhereIC = schemaComposer.createInputTC({
          name: whereInputName,
          description: table.TABLE_COMMENT,
          extensions: table,
          fields: {},
        });
        const tableOrderByIC = schemaComposer.createInputTC({
          name: orderByInputName,
          description: table.TABLE_COMMENT,
          extensions: table,
          fields: {},
        });
        const primaryKeys = new Set<string>();
        const fields = await introspectionConnection.getTableFields(tableName);
        const fieldNames =
          this.config.tableFields?.find(({ table }) => table === tableName)?.fields || Object.keys(fields);
        await Promise.all(
          fieldNames.map(async fieldName => {
            const tableField = fields[fieldName];
            if (tableField.Key === 'PRI') {
              primaryKeys.add(fieldName);
            }
            const typePattern = tableField.Type;
            const [realTypeNameCased, restTypePattern] = typePattern.split('(');
            const [typeDetails] = restTypePattern?.split(')') || [];
            const realTypeName = realTypeNameCased.toLowerCase();
            let type: string = SCALARS[realTypeName];
            if (realTypeName === 'enum' || realTypeName === 'set') {
              const enumValues = typeDetails.split(`'`).join('').split(',');
              const enumTypeName = sanitizeNameForGraphQL(tableName + '_' + fieldName);
              schemaComposer.createEnumTC({
                name: enumTypeName,
                values: enumValues.reduce((prev, curr) => {
                  const enumKey = sanitizeNameForGraphQL(curr);
                  return {
                    ...prev,
                    [enumKey]: {
                      value: curr,
                    },
                  };
                }, {} as EnumTypeComposerValueConfigDefinition),
              });
              type = enumTypeName;
            }
            if (!type) {
              console.warn(`${realTypeName} couldn't be mapped to a type. It will be mapped to JSON as a fallback.`);
              type = 'JSON';
            }
            if (tableField.Null.toLowerCase() === 'no') {
              type += '!';
            }
            tableTC.addFields({
              [fieldName]: {
                type,
                description: tableField.Comment,
              },
            });
            tableInsertIC.addFields({
              [fieldName]: {
                type,
                description: tableField.Comment,
              },
            });
            tableUpdateIC.addFields({
              [fieldName]: {
                type: type.replace('!', ''),
                description: tableField.Comment,
              },
            });
            tableWhereIC.addFields({
              [fieldName]: {
                type: 'String',
                description: tableField.Comment,
              },
            });
            tableOrderByIC.addFields({
              [fieldName]: {
                type: 'OrderBy',
                description: tableField.Comment,
              },
            });
          })
        );
        const tableForeigns = await introspectionConnection.getTableForeigns(tableName);
        const tableForeignNames = Object.keys(tableForeigns);
        await Promise.all(
          tableForeignNames.map(async foreignName => {
            const tableForeign = tableForeigns[foreignName];
            const columnName = tableForeign.COLUMN_NAME;
            if (!fieldNames.includes(columnName)) {
              return;
            }
            const foreignTableName = tableForeign.REFERENCED_TABLE_NAME;
            const foreignColumnName = tableForeign.REFERENCED_COLUMN_NAME;

            const foreignObjectTypeName = sanitizeNameForGraphQL(foreignTableName);
            const foreignWhereInputName = sanitizeNameForGraphQL(foreignTableName + '_WhereInput');
            const foreignOrderByInputName = sanitizeNameForGraphQL(foreignTableName + '_OrderByInput');
            tableTC.addFields({
              [foreignTableName]: {
                type: '[' + foreignObjectTypeName + ']',
                args: {
                  where: {
                    type: foreignWhereInputName,
                  },
                  orderBy: {
                    type: foreignOrderByInputName,
                  },
                  limit: {
                    type: 'Int',
                  },
                  offset: {
                    type: 'Int',
                  },
                },
                extensions: tableForeign,
                resolve: async (root, args, { mysqlConnection }, info) => {
                  const where = {
                    [foreignColumnName]: root[columnName],
                    ...args?.where,
                  };
                  // Generate limit statement
                  const limit: number[] = [args.limit, args.offset].filter(Boolean);
                  const fields = getFieldsFromResolveInfo(info);
                  if (limit.length) {
                    return mysqlConnection.selectLimit(foreignTableName, fields, limit, where, args?.orderBy);
                  } else {
                    return mysqlConnection.select(foreignTableName, fields, where, args?.orderBy);
                  }
                },
              },
            });
            const foreignOTC = schemaComposer.getOTC(foreignObjectTypeName);
            foreignOTC.addFields({
              [tableName]: {
                type: '[' + objectTypeName + ']',
                args: {
                  limit: {
                    type: 'Int',
                  },
                  offset: {
                    type: 'Int',
                  },
                  where: {
                    type: whereInputName,
                  },
                  orderBy: {
                    type: orderByInputName,
                  },
                },
                extensions: {
                  COLUMN_NAME: foreignColumnName,
                },
                resolve: (root, args, { mysqlConnection }, info) => {
                  const where = {
                    [columnName]: root[foreignColumnName],
                    ...args?.where,
                  };
                  const fieldMap: Record<string, any> = graphqlFields(info);
                  const fields: string[] = [];
                  for (const fieldName in fieldMap) {
                    if (fieldName !== '__typename') {
                      const subFieldMap = fieldMap[fieldName];
                      if (Object.keys(subFieldMap).length === 0) {
                        fields.push(fieldName);
                      } else {
                        const tableForeign = schemaComposer.getOTC(objectTypeName).getField(fieldName)
                          .extensions as TableForeign;
                        fields.push(tableForeign.COLUMN_NAME);
                      }
                    }
                  }
                  // Generate limit statement
                  const limit = [args.limit, args.offset].filter(Boolean);
                  if (limit.length) {
                    return mysqlConnection.selectLimit(tableName, fields, limit, where, args?.orderBy);
                  } else {
                    return mysqlConnection.select(tableName, fields, where, args?.orderBy);
                  }
                },
              },
            });
          })
        );
        schemaComposer.Query.addFields({
          [tableName]: {
            type: '[' + objectTypeName + ']',
            args: {
              limit: {
                type: 'Int',
              },
              offset: {
                type: 'Int',
              },
              where: {
                type: whereInputName,
              },
              orderBy: {
                type: orderByInputName,
              },
            },
            resolve: (root, args, { mysqlConnection }, info) => {
              const fieldMap: Record<string, any> = graphqlFields(info);
              const fields: string[] = [];
              for (const fieldName in fieldMap) {
                if (fieldName !== '__typename') {
                  const subFieldMap = fieldMap[fieldName];
                  if (Object.keys(subFieldMap).length === 0) {
                    fields.push(fieldName);
                  } else {
                    const tableForeign = schemaComposer.getOTC(objectTypeName).getField(fieldName)
                      .extensions as TableForeign;
                    fields.push(tableForeign.COLUMN_NAME);
                  }
                }
              }
              // Generate limit statement
              const limit = [args.limit, args.offset].filter(Boolean);
              if (limit.length) {
                return mysqlConnection.selectLimit(tableName, fields, limit, args.where, args?.orderBy);
              } else {
                return mysqlConnection.select(tableName, fields, args.where, args?.orderBy);
              }
            },
          },
        });
        schemaComposer.Query.addFields({
          [`count_${tableName}`]: {
            type: 'Int',
            args: {
              where: {
                type: whereInputName,
              },
            },
            resolve: (root, args, { mysqlConnection }, info) => mysqlConnection.count(tableName, args.where),
          },
        });
        schemaComposer.Mutation.addFields({
          [`insert_${tableName}`]: {
            type: objectTypeName,
            args: {
              [tableName]: {
                type: insertInputName + '!',
              },
            },
            resolve: async (root, args, { mysqlConnection }, info) => {
              const input = args[tableName];
              const { recordId } = await mysqlConnection.insert(tableName, input);
              const fields = getFieldsFromResolveInfo(info);
              const where: Record<string, any> = {};
              for (const primaryColumnName of primaryKeys) {
                where[primaryColumnName] = input[primaryColumnName] || recordId;
              }
              const result = await mysqlConnection.select(tableName, fields, where, {});
              return result[0];
            },
          },
          [`update_${tableName}`]: {
            type: objectTypeName,
            args: {
              [tableName]: {
                type: updateInputName + '!',
              },
              where: {
                type: whereInputName,
              },
            },
            resolve: async (root, args, { mysqlConnection }, info) => {
              await mysqlConnection.update(tableName, args[tableName], args.where);
              const fields = getFieldsFromResolveInfo(info);
              const result = await mysqlConnection.select(tableName, fields, args.where, {});
              return result[0];
            },
          },
          [`delete_${tableName}`]: {
            type: 'Boolean',
            args: {
              where: {
                type: whereInputName,
              },
            },
            resolve: (root, args, { mysqlConnection }) =>
              mysqlConnection.deleteRow(tableName, args.where).then(result => !!result?.affectedRows),
          },
        });
      })
    );
    introspectionConnection.release();

    const id$ = this.pubsub.subscribe('destroy', () => {
      pool.end();
      id$.then(id => this.pubsub.unsubscribe(id)).catch(err => console.error(err));
    });

    // graphql-compose doesn't add @defer and @stream to the schema
    specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive));

    const schema = schemaComposer.buildSchema();

    const executor = createDefaultExecutor(schema);

    return {
      schema,
      async executor(executionRequest: ExecutionRequest) {
        const mysqlConnection = await getPromisifiedConnection(pool);
        try {
          return await executor({
            ...executionRequest,
            context: {
              ...executionRequest.context,
              mysqlConnection,
            },
          });
        } catch (e: any) {
          return e;
        } finally {
          mysqlConnection.release();
        }
      },
    };
  }
Example #7
Source File: index.ts    From graphql-mesh with MIT License 4 votes vote down vote up
async getMeshSource(): Promise<MeshSource> {
    let fetch: ReturnType<typeof getCachedFetch>;
    if (this.config.customFetch) {
      fetch =
        typeof this.config.customFetch === 'string'
          ? await loadFromModuleExportExpression<ReturnType<typeof getCachedFetch>>(this.config.customFetch, {
              cwd: this.baseDir,
              importFn: this.importFn,
              defaultExportName: 'default',
            })
          : this.config.customFetch;
    } else {
      fetch = getCachedFetch(this.cache);
    }

    const { baseUrl: nonInterpolatedBaseUrl, operationHeaders } = this.config;
    const baseUrl = stringInterpolator.parse(nonInterpolatedBaseUrl, {
      env: process.env,
    });

    const schemaComposer = new SchemaComposer();
    schemaComposer.add(GraphQLBigInt);
    schemaComposer.add(GraphQLGUID);
    schemaComposer.add(GraphQLDateTime);
    schemaComposer.add(GraphQLJSON);
    schemaComposer.add(GraphQLByte);
    schemaComposer.add(GraphQLDate);
    schemaComposer.add(GraphQLISO8601Duration);

    const aliasNamespaceMap = new Map<string, string>();

    const metadataJson = await this.getCachedMetadataJson(fetch);
    const schemas = metadataJson.Edmx[0].DataServices[0].Schema;
    const multipleSchemas = schemas.length > 1;
    const namespaces = new Set<string>();

    const contextDataloaderName = Symbol(`${this.name}DataLoader`);

    function getNamespaceFromTypeRef(typeRef: string) {
      let namespace = '';
      namespaces?.forEach(el => {
        if (
          typeRef.startsWith(el) &&
          el.length > namespace.length && // It can be deeper namespace
          !typeRef.replace(el + '.', '').includes('.') // Typename cannot have `.`
        ) {
          namespace = el;
        }
      });
      return namespace;
    }

    function getTypeNameFromRef({
      typeRef,
      isInput,
      isRequired,
    }: {
      typeRef: string;
      isInput: boolean;
      isRequired: boolean;
    }) {
      const typeRefArr = typeRef.split('Collection(');
      const arrayDepth = typeRefArr.length;
      let actualTypeRef = typeRefArr.join('').split(')').join('');
      const typeNamespace = getNamespaceFromTypeRef(actualTypeRef);
      if (aliasNamespaceMap.has(typeNamespace)) {
        const alias = aliasNamespaceMap.get(typeNamespace);
        actualTypeRef = actualTypeRef.replace(typeNamespace, alias);
      }
      const actualTypeRefArr = actualTypeRef.split('.');
      const typeName = multipleSchemas
        ? pascalCase(actualTypeRefArr.join('_'))
        : actualTypeRefArr[actualTypeRefArr.length - 1];
      let realTypeName = typeName;
      if (SCALARS.has(actualTypeRef)) {
        realTypeName = SCALARS.get(actualTypeRef);
      } else if (schemaComposer.isEnumType(typeName)) {
        realTypeName = typeName;
      } else if (isInput) {
        realTypeName += 'Input';
      }
      const fakeEmptyArr = new Array(arrayDepth);
      realTypeName = fakeEmptyArr.join('[') + realTypeName + fakeEmptyArr.join(']');
      if (isRequired) {
        realTypeName += '!';
      }
      return realTypeName;
    }

    function getUrlString(url: URL) {
      return decodeURIComponent(url.toString()).split('+').join(' ');
    }

    function handleResponseText(responseText: string, urlString: string, info: GraphQLResolveInfo) {
      let responseJson: any;
      try {
        responseJson = JSON.parse(responseText);
      } catch (error) {
        const actualError = new Error(responseText);
        Object.assign(actualError, {
          extensions: {
            url: urlString,
          },
        });
        throw actualError;
      }
      if (responseJson.error) {
        const actualError = new Error(responseJson.error.message || responseJson.error) as any;
        actualError.extensions = responseJson.error;
        throw actualError;
      }
      const urlStringWithoutSearchParams = urlString.split('?')[0];
      if (isListType(info.returnType)) {
        const actualReturnType = getNamedType(info.returnType) as GraphQLObjectType;
        const entityTypeExtensions = actualReturnType.extensions as unknown as EntityTypeExtensions;
        if ('Message' in responseJson && !('value' in responseJson)) {
          const error = new Error(responseJson.Message);
          Object.assign(error, { extensions: responseJson });
          throw error;
        }
        const returnList: any[] = responseJson.value;
        return returnList.map(element => {
          if (!entityTypeExtensions?.entityInfo) {
            return element;
          }
          const urlOfElement = new URL(urlStringWithoutSearchParams);
          addIdentifierToUrl(
            urlOfElement,
            entityTypeExtensions.entityInfo.identifierFieldName,
            entityTypeExtensions.entityInfo.identifierFieldTypeRef,
            element
          );
          const identifierUrl = element['@odata.id'] || getUrlString(urlOfElement);
          const fieldMap = actualReturnType.getFields();
          for (const fieldName in element) {
            if (entityTypeExtensions.entityInfo.navigationFields.includes(fieldName)) {
              const field = element[fieldName];
              let fieldType = fieldMap[fieldName].type;
              if ('ofType' in fieldType) {
                fieldType = fieldType.ofType;
              }
              const { entityInfo: fieldEntityInfo } = (fieldType as any).extensions as EntityTypeExtensions;
              if (field instanceof Array) {
                for (const fieldElement of field) {
                  const urlOfField = new URL(urljoin(identifierUrl, fieldName));
                  addIdentifierToUrl(
                    urlOfField,
                    fieldEntityInfo.identifierFieldName,
                    fieldEntityInfo.identifierFieldTypeRef,
                    fieldElement
                  );
                  fieldElement['@odata.id'] = fieldElement['@odata.id'] || getUrlString(urlOfField);
                }
              } else {
                const urlOfField = new URL(urljoin(identifierUrl, fieldName));
                addIdentifierToUrl(
                  urlOfField,
                  fieldEntityInfo.identifierFieldName,
                  fieldEntityInfo.identifierFieldTypeRef,
                  field
                );
                field['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField);
              }
            }
          }
          return {
            '@odata.id': identifierUrl,
            ...element,
          };
        });
      } else {
        const actualReturnType = info.returnType as GraphQLObjectType;
        const entityTypeExtensions = actualReturnType.extensions as unknown as EntityTypeExtensions;
        if (!entityTypeExtensions?.entityInfo) {
          return responseJson;
        }
        const identifierUrl = responseJson['@odata.id'] || urlStringWithoutSearchParams;
        const fieldMap = actualReturnType.getFields();
        for (const fieldName in responseJson) {
          if (entityTypeExtensions?.entityInfo.navigationFields.includes(fieldName)) {
            const field = responseJson[fieldName];
            let fieldType = fieldMap[fieldName].type;
            if ('ofType' in fieldType) {
              fieldType = fieldType.ofType;
            }
            const { entityInfo: fieldEntityInfo } = (fieldType as any).extensions as EntityTypeExtensions;
            if (field instanceof Array) {
              for (const fieldElement of field) {
                const urlOfField = new URL(urljoin(identifierUrl, fieldName));
                addIdentifierToUrl(
                  urlOfField,
                  fieldEntityInfo.identifierFieldName,
                  fieldEntityInfo.identifierFieldTypeRef,
                  fieldElement
                );
                fieldElement['@odata.id'] = fieldElement['@odata.id'] || getUrlString(urlOfField);
              }
            } else {
              const urlOfField = new URL(urljoin(identifierUrl, fieldName));
              addIdentifierToUrl(
                urlOfField,
                fieldEntityInfo.identifierFieldName,
                fieldEntityInfo.identifierFieldTypeRef,
                field
              );
              field['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField);
            }
          }
        }
        return {
          '@odata.id': responseJson['@odata.id'] || urlStringWithoutSearchParams,
          ...responseJson,
        };
      }
    }

    schemaComposer.createEnumTC({
      name: 'InlineCount',
      values: {
        allpages: {
          value: 'allpages',
          description:
            'The OData MUST include a count of the number of entities in the collection identified by the URI (after applying any $filter System Query Options present on the URI)',
        },
        none: {
          value: 'none',
          description:
            'The OData service MUST NOT include a count in the response. This is equivalence to a URI that does not include a $inlinecount query string parameter.',
        },
      },
    });

    schemaComposer.createInputTC({
      name: 'QueryOptions',
      fields: queryOptionsFields,
    });

    const origHeadersFactory = getInterpolatedHeadersFactory(operationHeaders);
    const headersFactory = (resolverData: ResolverData, method: string) => {
      const headers = origHeadersFactory(resolverData);
      if (headers.accept == null) {
        headers.accept = 'application/json';
      }
      if (headers['content-type'] == null && method !== 'GET') {
        headers['content-type'] = 'application/json';
      }
      return headers;
    };
    const { args: commonArgs, contextVariables } = parseInterpolationStrings([
      ...Object.values(operationHeaders || {}),
      baseUrl,
    ]);

    function getTCByTypeNames(...typeNames: string[]) {
      for (const typeName of typeNames) {
        try {
          return schemaComposer.getAnyTC(typeName);
        } catch {}
      }
      return null;
    }

    function addIdentifierToUrl(url: URL, identifierFieldName: string, identifierFieldTypeRef: string, args: any) {
      url.href += `/${args[identifierFieldName]}/`;
    }

    function rebuildOpenInputObjects(input: any) {
      if (typeof input === 'object') {
        if ('rest' in input) {
          Object.assign(input, input.rest);
          delete input.rest;
        }
        for (const fieldName in input) {
          rebuildOpenInputObjects(input[fieldName]);
        }
      }
    }

    function handleBatchJsonResults(batchResponseJson: any, requests: Request[]) {
      if ('error' in batchResponseJson) {
        const error = new Error(batchResponseJson.error.message);
        Object.assign(error, {
          extensions: batchResponseJson.error,
        });
        throw error;
      }
      if (!('responses' in batchResponseJson)) {
        const error = new Error(
          batchResponseJson.ExceptionMessage ||
            batchResponseJson.Message ||
            `Batch Request didn't return a valid response.`
        );
        Object.assign(error, {
          extensions: batchResponseJson,
        });
        throw error;
      }
      return requests.map((_req, index) => {
        const responseObj = batchResponseJson.responses.find((res: any) => res.id === index.toString());
        return new Response(JSON.stringify(responseObj.body), {
          status: responseObj.status,
          headers: responseObj.headers,
        });
      });
    }

    const DATALOADER_FACTORIES = {
      multipart: (context: any) =>
        new DataLoader(async (requests: Request[]): Promise<Response[]> => {
          let requestBody = '';
          const requestBoundary = 'batch_' + Date.now();
          for (const requestIndex in requests) {
            requestBody += `--${requestBoundary}\n`;
            const request = requests[requestIndex];
            requestBody += `Content-Type: application/http\n`;
            requestBody += `Content-Transfer-Encoding:binary\n`;
            requestBody += `Content-ID: ${requestIndex}\n\n`;
            requestBody += `${request.method} ${request.url} HTTP/1.1\n`;
            request.headers?.forEach((value, key) => {
              requestBody += `${key}: ${value}\n`;
            });
            if (request.body) {
              const bodyAsStr = await request.text();
              requestBody += `Content-Length: ${bodyAsStr.length}`;
              requestBody += `\n`;
              requestBody += bodyAsStr;
            }
            requestBody += `\n`;
          }
          requestBody += `--${requestBoundary}--\n`;
          const batchHeaders = headersFactory(
            {
              context,
              env: process.env,
            },
            'POST'
          );
          batchHeaders['content-type'] = `multipart/mixed;boundary=${requestBoundary}`;
          const batchResponse = await fetch(urljoin(baseUrl, '$batch'), {
            method: 'POST',
            body: requestBody,
            headers: batchHeaders,
          });
          if (batchResponse.headers.get('content-type').includes('json')) {
            const batchResponseJson = await batchResponse.json();
            return handleBatchJsonResults(batchResponseJson, requests);
          }
          const batchResponseText = await batchResponse.text();
          const responseLines = batchResponseText.split('\n');
          const responseBoundary = responseLines[0];
          const actualResponse = responseLines.slice(1, responseLines.length - 2).join('\n');
          const responseTextArr = actualResponse.split(responseBoundary);
          return responseTextArr.map(responseTextWithContentHeader => {
            const responseText = responseTextWithContentHeader.split('\n').slice(4).join('\n');
            const { body, headers, statusCode, statusMessage } = parseResponse(responseText);
            return new Response(body, {
              headers,
              status: parseInt(statusCode),
              statusText: statusMessage,
            });
          });
        }),
      json: (context: any) =>
        new DataLoader(async (requests: Request[]): Promise<Response[]> => {
          const batchHeaders = headersFactory(
            {
              context,
              env: process.env,
            },
            'POST'
          );
          batchHeaders['content-type'] = 'application/json';
          const batchResponse = await fetch(urljoin(baseUrl, '$batch'), {
            method: 'POST',
            body: JSON.stringify({
              requests: await Promise.all(
                requests.map(async (request, index) => {
                  const id = index.toString();
                  const url = request.url.replace(baseUrl, '');
                  const method = request.method;
                  const headers: HeadersInit = {};
                  request.headers?.forEach((value, key) => {
                    headers[key] = value;
                  });
                  return {
                    id,
                    url,
                    method,
                    body: request.body && (await request.json()),
                    headers,
                  };
                })
              ),
            }),
            headers: batchHeaders,
          });
          const batchResponseJson = await batchResponse.json();
          return handleBatchJsonResults(batchResponseJson, requests);
        }),
      none: () =>
        new DataLoader(
          (requests: Request[]): Promise<Response[]> => Promise.all(requests.map(request => fetch(request)))
        ),
    };

    const dataLoaderFactory = memoize1(DATALOADER_FACTORIES[this.config.batch || 'none']);

    function buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) {
      const alias = aliasNamespaceMap.get(schemaNamespace) || schemaNamespace;
      const ref = alias + '.' + name;
      return multipleSchemas ? pascalCase(ref.split('.').join('_')) : name;
    }

    schemas?.forEach((schemaObj: any) => {
      const schemaNamespace = schemaObj.attributes.Namespace;
      namespaces.add(schemaNamespace);
      const schemaAlias = schemaObj.attributes.Alias;
      if (schemaAlias) {
        aliasNamespaceMap.set(schemaNamespace, schemaAlias);
      }
    });

    schemas?.forEach((schemaObj: any) => {
      const schemaNamespace = schemaObj.attributes.Namespace;

      schemaObj.EnumType?.forEach((enumObj: any) => {
        const values: Record<string, EnumTypeComposerValueConfigDefinition> = {};
        enumObj.Member?.forEach((memberObj: any) => {
          const key = memberObj.attributes.Name;
          // This doesn't work.
          // const value = memberElement.getAttribute('Value')!;
          values[key] = {
            value: key,
            extensions: { memberObj },
          };
        });
        const enumTypeName = buildName({ schemaNamespace, name: enumObj.attributes.Name });
        schemaComposer.createEnumTC({
          name: enumTypeName,
          values,
          extensions: { enumObj },
        });
      });

      const allTypes = (schemaObj.EntityType || []).concat(schemaObj.ComplexType || []);
      const typesWithBaseType = allTypes.filter((typeObj: any) => typeObj.attributes.BaseType);

      allTypes?.forEach((typeObj: any) => {
        const entityTypeName = buildName({ schemaNamespace, name: typeObj.attributes.Name });
        const isOpenType = typeObj.attributes.OpenType === 'true';
        const isAbstract = typeObj.attributes.Abstract === 'true';
        const eventEmitter = new EventEmitter();
        eventEmitter.setMaxListeners(Infinity);
        this.eventEmitterSet.add(eventEmitter);
        const extensions: EntityTypeExtensions = {
          entityInfo: {
            actualFields: [],
            navigationFields: [],
            isOpenType,
          },
          typeObj,
          eventEmitter,
        };
        const inputType = schemaComposer.createInputTC({
          name: entityTypeName + 'Input',
          fields: {},
          extensions: () => extensions,
        });
        let abstractType: InterfaceTypeComposer;
        if (
          typesWithBaseType.some((typeObj: any) => typeObj.attributes.BaseType.includes(`.${entityTypeName}`)) ||
          isAbstract
        ) {
          abstractType = schemaComposer.createInterfaceTC({
            name: isAbstract ? entityTypeName : `I${entityTypeName}`,
            extensions,
            resolveType: (root: any) => {
              const typeRef = root['@odata.type']?.replace('#', '');
              if (typeRef) {
                const typeName = getTypeNameFromRef({
                  typeRef: root['@odata.type'].replace('#', ''),
                  isInput: false,
                  isRequired: false,
                });
                return typeName;
              }
              return isAbstract ? `T${entityTypeName}` : entityTypeName;
            },
          });
        }
        const outputType = schemaComposer.createObjectTC({
          name: isAbstract ? `T${entityTypeName}` : entityTypeName,
          extensions,
          interfaces: abstractType ? [abstractType] : [],
        });

        abstractType?.setInputTypeComposer(inputType);
        outputType.setInputTypeComposer(inputType);

        const propertyRefObj = typeObj.Key && typeObj.Key[0].PropertyRef[0];
        if (propertyRefObj) {
          extensions.entityInfo.identifierFieldName = propertyRefObj.attributes.Name;
        }

        typeObj.Property?.forEach((propertyObj: any) => {
          const propertyName = propertyObj.attributes.Name;
          extensions.entityInfo.actualFields.push(propertyName);
          const propertyTypeRef = propertyObj.attributes.Type;
          if (propertyName === extensions.entityInfo.identifierFieldName) {
            extensions.entityInfo.identifierFieldTypeRef = propertyTypeRef;
          }
          const isRequired = propertyObj.attributes.Nullable === 'false';
          inputType.addFields({
            [propertyName]: {
              type: getTypeNameFromRef({
                typeRef: propertyTypeRef,
                isInput: true,
                isRequired,
              }),
              extensions: { propertyObj },
            },
          });
          const field: ObjectTypeComposerFieldConfigDefinition<any, unknown> = {
            type: getTypeNameFromRef({
              typeRef: propertyTypeRef,
              isInput: false,
              isRequired,
            }),
            extensions: { propertyObj },
          };
          abstractType?.addFields({
            [propertyName]: field,
          });
          outputType.addFields({
            [propertyName]: field,
          });
        });
        typeObj.NavigationProperty?.forEach((navigationPropertyObj: any) => {
          const navigationPropertyName = navigationPropertyObj.attributes.Name;
          extensions.entityInfo.navigationFields.push(navigationPropertyName);
          const navigationPropertyTypeRef = navigationPropertyObj.attributes.Type;
          const isRequired = navigationPropertyObj.attributes.Nullable === 'false';
          const isList = navigationPropertyTypeRef.startsWith('Collection(');
          if (isList) {
            const singularField: ObjectTypeComposerFieldConfigDefinition<any, unknown> = {
              type: getTypeNameFromRef({
                typeRef: navigationPropertyTypeRef,
                isInput: false,
                isRequired,
              })
                .replace('[', '')
                .replace(']', ''),
              args: {
                ...commonArgs,
                id: {
                  type: 'ID',
                },
              },
              extensions: { navigationPropertyObj },
              resolve: async (root, args, context, info) => {
                if (navigationPropertyName in root) {
                  return root[navigationPropertyName];
                }
                const url = new URL(root['@odata.id']);
                url.href = urljoin(url.href, '/' + navigationPropertyName);
                const returnType = info.returnType as GraphQLObjectType;
                const { entityInfo } = returnType.extensions as unknown as EntityTypeExtensions;
                addIdentifierToUrl(url, entityInfo.identifierFieldName, entityInfo.identifierFieldTypeRef, args);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            };
            const pluralField: ObjectTypeComposerFieldConfigDefinition<any, unknown> = {
              type: getTypeNameFromRef({
                typeRef: navigationPropertyTypeRef,
                isInput: false,
                isRequired,
              }),
              args: {
                ...commonArgs,
                queryOptions: { type: 'QueryOptions' },
              },
              extensions: { navigationPropertyObj },
              resolve: async (root, args, context, info) => {
                if (navigationPropertyName in root) {
                  return root[navigationPropertyName];
                }
                const url = new URL(root['@odata.id']);
                url.href = urljoin(url.href, '/' + navigationPropertyName);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            };
            abstractType?.addFields({
              [navigationPropertyName]: pluralField,
              [`${navigationPropertyName}ById`]: singularField,
            });
            outputType.addFields({
              [navigationPropertyName]: pluralField,
              [`${navigationPropertyName}ById`]: singularField,
            });
          } else {
            const field: ObjectTypeComposerFieldConfigDefinition<any, unknown> = {
              type: getTypeNameFromRef({
                typeRef: navigationPropertyTypeRef,
                isInput: false,
                isRequired,
              }),
              args: {
                ...commonArgs,
              },
              extensions: { navigationPropertyObj },
              resolve: async (root, args, context, info) => {
                if (navigationPropertyName in root) {
                  return root[navigationPropertyName];
                }
                const url = new URL(root['@odata.id']);
                url.href = urljoin(url.href, '/' + navigationPropertyName);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            };
            abstractType?.addFields({
              [navigationPropertyName]: field,
            });
            outputType.addFields({
              [navigationPropertyName]: field,
            });
          }
        });
        if (isOpenType || outputType.getFieldNames().length === 0) {
          extensions.entityInfo.isOpenType = true;
          inputType.addFields({
            rest: {
              type: 'JSON',
            },
          });
          abstractType?.addFields({
            rest: {
              type: 'JSON',
              resolve: (root: any) => root,
            },
          });
          outputType.addFields({
            rest: {
              type: 'JSON',
              resolve: (root: any) => root,
            },
          });
        }
        const updateInputType = inputType.clone(`${entityTypeName}UpdateInput`);
        updateInputType.getFieldNames()?.forEach(fieldName => updateInputType.makeOptional(fieldName));
        // Types might be considered as unused implementations of interfaces so we must prevent that
        schemaComposer.addSchemaMustHaveType(outputType);
      });

      const handleUnboundFunctionObj = (unboundFunctionObj: any) => {
        const functionName = unboundFunctionObj.attributes.Name;
        const returnTypeRef = unboundFunctionObj.ReturnType[0].attributes.Type;
        const returnType = getTypeNameFromRef({
          typeRef: returnTypeRef,
          isInput: false,
          isRequired: false,
        });
        schemaComposer.Query.addFields({
          [functionName]: {
            type: returnType,
            args: {
              ...commonArgs,
            },
            resolve: async (root, args, context, info) => {
              const url = new URL(baseUrl);
              url.href = urljoin(url.href, '/' + functionName);
              url.href += `(${Object.entries(args)
                .filter(argEntry => argEntry[0] !== 'queryOptions')
                .map(argEntry => argEntry.join(' = '))
                .join(', ')})`;
              const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
              const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
              searchParams?.forEach((value, key) => {
                url.searchParams.set(key, value);
              });
              const urlString = getUrlString(url);
              const method = 'GET';
              const request = new Request(urlString, {
                method,
                headers: headersFactory(
                  {
                    root,
                    args,
                    context,
                    info,
                    env: process.env,
                  },
                  method
                ),
              });
              const response = await context[contextDataloaderName].load(request);
              const responseText = await response.text();
              return handleResponseText(responseText, urlString, info);
            },
          },
        });
        unboundFunctionObj.Parameter?.forEach((parameterObj: any) => {
          const parameterName = parameterObj.attributes.Name;
          const parameterTypeRef = parameterObj.attributes.Type;
          const isRequired = parameterObj.attributes.Nullable === 'false';
          const parameterType = getTypeNameFromRef({
            typeRef: parameterTypeRef,
            isInput: true,
            isRequired,
          });
          schemaComposer.Query.addFieldArgs(functionName, {
            [parameterName]: {
              type: parameterType,
            },
          });
        });
      };

      const handleBoundFunctionObj = (boundFunctionObj: any) => {
        const functionName = boundFunctionObj.attributes.Name;
        const functionRef = schemaNamespace + '.' + functionName;
        const returnTypeRef = boundFunctionObj.ReturnType[0].attributes.Type;
        const returnType = getTypeNameFromRef({
          typeRef: returnTypeRef,
          isInput: false,
          isRequired: false,
        });
        const args: ObjectTypeComposerArgumentConfigMapDefinition<any> = {
          ...commonArgs,
        };
        // eslint-disable-next-line prefer-const
        let entitySetPath = boundFunctionObj.attributes.EntitySetPath?.split('/')[0];
        let field: ObjectTypeComposerFieldConfigDefinition<any, any, any>;
        let boundEntityTypeName: string;
        boundFunctionObj.Parameter?.forEach((parameterObj: any) => {
          const parameterName = parameterObj.attributes.Name;
          const parameterTypeRef = parameterObj.attributes.Type;
          const isRequired = parameterObj.attributes.Nullable === 'false';
          const parameterTypeName = getTypeNameFromRef({
            typeRef: parameterTypeRef,
            isInput: true,
            isRequired,
          });
          // If entitySetPath is not available, take first parameter as entity
          // The first segment of the entity set path must match the binding parameter name
          // (see: http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530388)
          entitySetPath = (entitySetPath && entitySetPath.split('/')[0]) || parameterName;
          if (entitySetPath === parameterName) {
            boundEntityTypeName = getTypeNameFromRef({
              typeRef: parameterTypeRef,
              isInput: false,
              isRequired: false,
            })
              .replace('[', '')
              .replace(']', '');
            field = {
              type: returnType,
              args,
              resolve: async (root, args, context, info) => {
                const url = new URL(root['@odata.id']);
                url.href = urljoin(url.href, '/' + functionRef);
                const argsEntries = Object.entries(args);
                if (argsEntries.length) {
                  url.href += `(${argsEntries
                    .filter(argEntry => argEntry[0] !== 'queryOptions')
                    .map(([argName, value]) => [argName, typeof value === 'string' ? `'${value}'` : value])
                    .map(argEntry => argEntry.join('='))
                    .join(',')})`;
                }
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            };
          }
          args[parameterName] = {
            type: parameterTypeName,
          };
        });
        const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer;
        const boundEntityOtherType = getTCByTypeNames(
          'I' + boundEntityTypeName,
          'T' + boundEntityTypeName
        ) as InterfaceTypeComposer;
        boundEntityType.addFields({
          [functionName]: field,
        });
        boundEntityOtherType?.addFields({
          [functionName]: field,
        });
      };

      schemaObj.Function?.forEach((functionObj: any) => {
        if (functionObj.attributes?.IsBound === 'true') {
          handleBoundFunctionObj(functionObj);
        } else {
          handleUnboundFunctionObj(functionObj);
        }
      });

      const handleUnboundActionObj = (unboundActionObj: any) => {
        const actionName = unboundActionObj.attributes.Name;
        schemaComposer.Mutation.addFields({
          [actionName]: {
            type: 'JSON',
            args: {
              ...commonArgs,
            },
            resolve: async (root, args, context, info) => {
              const url = new URL(baseUrl);
              url.href = urljoin(url.href, '/' + actionName);
              const urlString = getUrlString(url);
              const method = 'POST';
              const request = new Request(urlString, {
                method,
                headers: headersFactory(
                  {
                    root,
                    args,
                    context,
                    info,
                    env: process.env,
                  },
                  method
                ),
                body: JSON.stringify(args),
              });
              const response = await context[contextDataloaderName].load(request);
              const responseText = await response.text();
              return handleResponseText(responseText, urlString, info);
            },
          },
        });

        unboundActionObj.Parameter?.forEach((parameterObj: any) => {
          const parameterName = parameterObj.attributes.Name;
          const parameterTypeRef = parameterObj.attributes.Type;
          const isRequired = parameterObj.attributes.Nullable === 'false';
          const parameterType = getTypeNameFromRef({
            typeRef: parameterTypeRef,
            isInput: true,
            isRequired,
          });
          schemaComposer.Mutation.addFieldArgs(actionName, {
            [parameterName]: {
              type: parameterType,
            },
          });
        });
      };

      const handleBoundActionObj = (boundActionObj: any) => {
        const actionName = boundActionObj.attributes.Name;
        const actionRef = schemaNamespace + '.' + actionName;
        const args: ObjectTypeComposerArgumentConfigMapDefinition<any> = {
          ...commonArgs,
        };
        let entitySetPath = boundActionObj.attributes.EntitySetPath;
        let boundField: ObjectTypeComposerFieldConfigDefinition<any, any, any>;
        let boundEntityTypeName: string;
        boundActionObj.Parameter?.forEach((parameterObj: any) => {
          const parameterName = parameterObj.attributes.Name;
          const parameterTypeRef = parameterObj.attributes.Type;
          const isRequired = parameterObj.attributes.Nullable === 'false';
          const parameterTypeName = getTypeNameFromRef({
            typeRef: parameterTypeRef,
            isInput: true,
            isRequired,
          });
          // If entitySetPath is not available, take first parameter as entity
          entitySetPath = entitySetPath || parameterName;
          if (entitySetPath === parameterName) {
            boundEntityTypeName = getTypeNameFromRef({
              typeRef: parameterTypeRef,
              isInput: false,
              isRequired: false,
            })
              .replace('[', '')
              .replace(']', ''); // Todo temp workaround
            boundField = {
              type: 'JSON',
              args,
              resolve: async (root, args, context, info) => {
                const url = new URL(root['@odata.id']);
                url.href = urljoin(url.href, '/' + actionRef);
                const urlString = getUrlString(url);
                const method = 'POST';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                  body: JSON.stringify(args),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            };
          }
          args[parameterName] = {
            type: parameterTypeName,
          };
        });
        const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer;
        boundEntityType.addFields({
          [actionName]: boundField,
        });
        const otherType = getTCByTypeNames(
          `I${boundEntityTypeName}`,
          `T${boundEntityTypeName}`
        ) as InterfaceTypeComposer;
        otherType?.addFields({
          [actionName]: boundField,
        });
      };

      schemaObj.Action?.forEach((actionObj: any) => {
        if (actionObj.attributes?.IsBound === 'true') {
          handleBoundActionObj(actionObj);
        } else {
          handleUnboundActionObj(actionObj);
        }
      });

      // Rearrange fields for base types and implementations
      typesWithBaseType?.forEach((typeObj: any) => {
        const typeName = buildName({
          schemaNamespace,
          name: typeObj.attributes.Name,
        });
        const inputType = schemaComposer.getITC(typeName + 'Input') as InputTypeComposer;
        const abstractType = getTCByTypeNames('I' + typeName, typeName) as InterfaceTypeComposer;
        const outputType = getTCByTypeNames('T' + typeName, typeName) as ObjectTypeComposer;
        const baseTypeRef = typeObj.attributes.BaseType;
        const { entityInfo, eventEmitter } = outputType.getExtensions() as EntityTypeExtensions;
        const baseTypeName = getTypeNameFromRef({
          typeRef: baseTypeRef,
          isInput: false,
          isRequired: false,
        });
        const baseInputType = schemaComposer.getAnyTC(baseTypeName + 'Input') as InputTypeComposer;
        const baseAbstractType = getTCByTypeNames('I' + baseTypeName, baseTypeName) as InterfaceTypeComposer;
        const baseOutputType = getTCByTypeNames('T' + baseTypeName, baseTypeName) as ObjectTypeComposer;
        const { entityInfo: baseEntityInfo, eventEmitter: baseEventEmitter } =
          baseOutputType.getExtensions() as EntityTypeExtensions;
        const baseEventEmitterListener = () => {
          inputType.addFields(baseInputType.getFields());
          entityInfo.identifierFieldName = baseEntityInfo.identifierFieldName || entityInfo.identifierFieldName;
          entityInfo.identifierFieldTypeRef =
            baseEntityInfo.identifierFieldTypeRef || entityInfo.identifierFieldTypeRef;
          entityInfo.actualFields.unshift(...baseEntityInfo.actualFields);
          abstractType?.addFields(baseAbstractType?.getFields());
          outputType.addFields(baseOutputType.getFields());
          if (baseAbstractType instanceof InterfaceTypeComposer) {
            // abstractType.addInterface(baseAbstractType.getTypeName());
            outputType.addInterface(baseAbstractType.getTypeName());
          }
          eventEmitter.emit('onFieldChange');
        };
        baseEventEmitter.on('onFieldChange', baseEventEmitterListener);
        baseEventEmitterListener();
      });
    });

    schemas?.forEach((schemaObj: any) => {
      schemaObj.EntityContainer?.forEach((entityContainerObj: any) => {
        entityContainerObj.Singleton?.forEach((singletonObj: any) => {
          const singletonName = singletonObj.attributes.Name;
          const singletonTypeRef = singletonObj.attributes.Type;
          const singletonTypeName = getTypeNameFromRef({
            typeRef: singletonTypeRef,
            isInput: false,
            isRequired: false,
          });
          schemaComposer.Query.addFields({
            [singletonName]: {
              type: singletonTypeName,
              args: {
                ...commonArgs,
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + singletonName);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
          });
        });

        entityContainerObj?.EntitySet?.forEach((entitySetObj: any) => {
          const entitySetName = entitySetObj.attributes.Name;
          const entitySetTypeRef = entitySetObj.attributes.EntityType;
          const entityTypeName = getTypeNameFromRef({
            typeRef: entitySetTypeRef,
            isInput: false,
            isRequired: false,
          });
          const entityOutputTC = getTCByTypeNames('I' + entityTypeName, entityTypeName) as
            | InterfaceTypeComposer
            | ObjectTypeComposer;
          const { entityInfo } = entityOutputTC.getExtensions() as EntityTypeExtensions;
          const identifierFieldName = entityInfo.identifierFieldName;
          const identifierFieldTypeRef = entityInfo.identifierFieldTypeRef;
          const identifierFieldTypeName = entityOutputTC.getFieldTypeName(identifierFieldName);
          const typeName = entityOutputTC.getTypeName();
          const commonFields: Record<string, ObjectTypeComposerFieldConfigDefinition<any, any>> = {
            [entitySetName]: {
              type: `[${typeName}]`,
              args: {
                ...commonArgs,
                queryOptions: { type: 'QueryOptions' },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + entitySetName);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
            [`${entitySetName}By${identifierFieldName}`]: {
              type: typeName,
              args: {
                ...commonArgs,
                [identifierFieldName]: {
                  type: identifierFieldTypeName,
                },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + entitySetName);
                addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args);
                const parsedInfoFragment = parseResolveInfo(info) as ResolveTree;
                const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema);
                searchParams?.forEach((value, key) => {
                  url.searchParams.set(key, value);
                });
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
          };
          schemaComposer.Query.addFields({
            ...commonFields,
            [`${entitySetName}Count`]: {
              type: 'Int',
              args: {
                ...commonArgs,
                queryOptions: { type: 'QueryOptions' },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, `/${entitySetName}/$count`);
                const urlString = getUrlString(url);
                const method = 'GET';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return responseText;
              },
            },
          });
          schemaComposer.Mutation.addFields({
            ...commonFields,
            [`create${entitySetName}`]: {
              type: typeName,
              args: {
                ...commonArgs,
                input: {
                  type: entityTypeName + 'Input',
                },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + entitySetName);
                const urlString = getUrlString(url);
                rebuildOpenInputObjects(args.input);
                const method = 'POST';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                  body: JSON.stringify(args.input),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
            [`delete${entitySetName}By${identifierFieldName}`]: {
              type: 'JSON',
              args: {
                ...commonArgs,
                [identifierFieldName]: {
                  type: identifierFieldTypeName,
                },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + entitySetName);
                addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args);
                const urlString = getUrlString(url);
                const method = 'DELETE';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
            [`update${entitySetName}By${identifierFieldName}`]: {
              type: typeName,
              args: {
                ...commonArgs,
                [identifierFieldName]: {
                  type: identifierFieldTypeName,
                },
                input: {
                  type: entityTypeName + 'UpdateInput',
                },
              },
              resolve: async (root, args, context, info) => {
                const url = new URL(baseUrl);
                url.href = urljoin(url.href, '/' + entitySetName);
                addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args);
                const urlString = getUrlString(url);
                rebuildOpenInputObjects(args.input);
                const method = 'PATCH';
                const request = new Request(urlString, {
                  method,
                  headers: headersFactory(
                    {
                      root,
                      args,
                      context,
                      info,
                      env: process.env,
                    },
                    method
                  ),
                  body: JSON.stringify(args.input),
                });
                const response = await context[contextDataloaderName].load(request);
                const responseText = await response.text();
                return handleResponseText(responseText, urlString, info);
              },
            },
          });
        });
      });
    });

    // graphql-compose doesn't add @defer and @stream to the schema
    specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive));

    const schema = schemaComposer.buildSchema();
    this.eventEmitterSet.forEach(ee => ee.removeAllListeners());
    this.eventEmitterSet.clear();

    const executor = createDefaultExecutor(schema);

    return {
      schema,
      executor: <TResult>(executionRequest: ExecutionRequest) => {
        const odataContext = {
          [contextDataloaderName]: dataLoaderFactory(executionRequest.context),
        };
        return executor({
          ...executionRequest,
          context: {
            ...executionRequest.context,
            ...odataContext,
          },
        }) as ExecutionResult<TResult>;
      },
      contextVariables,
      batch: true,
    };
  }