Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphql schema interception / types generation with multiple endpoints #390

Closed
david-vendel opened this issue Oct 10, 2022 · 4 comments
Closed

Comments

@david-vendel
Copy link

david-vendel commented Oct 10, 2022

Hello, I successfully use your library to query 2 endpoints. Now, how do I auto-generate types from 2 graphql schemas, when using 2 endpoints?

Scripts I used up until now (with standard 1 endpoint) uses Apollo codegen (source):

"schema": "npx apollo service:download --endpoint=http://localhost:8080/graphql graphql-schema.json",
"types": "npm run schema && apollo client:codegen --localSchemaFile=graphql-schema.json --variant=development --target=typescript --addTypename --queries=./src/**/*.graphql --useReadOnlyTypes --globalTypesFile=src/globalTypes.ts . && npm run prettier"

Possible solution is also generating 2 introspection schemas and then merging them together, I just didn't find how.

I'm also open to move to Graphql Code Generator.

Is there any example, guidance, or link please?
Thank you

@jean9696
Copy link
Member

Hi @justdvl, I can't provide you a nice example for that. We built our own script using graphql-tools to merge schemas locally and then generate the types. But I think Graphql Code Generator can manage it now.

@david-vendel
Copy link
Author

Thanks. This script might be exactly what I would need - I can get 2 graphql-schema.json files, just don't know how to merge them. If you will consider creating a package out of it, it would be helpful.
Agree, it seems I will need to migrate to Graphql Code Generator.

@jean9696
Copy link
Member

@justdvl This is what we have

import { loadSchema } from '@graphql-tools/load'
import { mergeSchemas } from '@graphql-tools/merge'
import { UrlLoader } from '@graphql-tools/url-loader'
import { promises as fs } from 'fs'
import glob from 'glob'
import { GraphQLSchema, printSchema } from 'graphql'

// Use your own libs here
import { Envs, getEndpoints } from '@habx/graphql-config'

export type Endpoints = {
  [endpoint: string]: string
}

type SchemaBuilderConfig = {
  endpoints: Endpoints
  ignoredEndpoints?: string[]
  ignoredLocalFilePattern?: string
}

class SchemaBuilder {
  private readonly endpoints: Endpoints
  private readonly nonIgnoredEndpoints: Endpoints
  private readonly ignoredLocalFilePattern: string

  constructor(config: SchemaBuilderConfig) {
    this.endpoints = config.endpoints
    this.nonIgnoredEndpoints = this.buildValidEndpointsList(
      config.endpoints,
      config.ignoredEndpoints
    )

    this.ignoredLocalFilePattern = config.ignoredLocalFilePattern ?? ''
  }

  private static log(...parameters: Parameters<typeof console.log>) {
    // eslint-disable-next-line no-console
    console.log(...parameters)
  }

  private buildValidEndpointsList = (
    endpoints: Endpoints,
    ignoredEndpoints: string[] = []
  ) => {
    const validEndpoints: Endpoints = {}

    for (const endpoint in endpoints) {
      if (endpoints.hasOwnProperty(endpoint)) {
        if (ignoredEndpoints.includes(endpoint)) {
          SchemaBuilder.log(`Endpoint ignored : ${endpoint}`)
        } else {
          validEndpoints[endpoint] = `${endpoints[endpoint]}/graphql`
        }
      }
    }

    return validEndpoints
  }

  public run = async () => {
    const remoteSchemas = await this.fetchRemoteSchemas()
    const localSchema = await this.getLocalSchemas()
    const apiDirective = await this.buildAPIDirective()

    await this.mergeSchemas(
      [...remoteSchemas, localSchema, apiDirective].filter(
        (el) => !!el
      ) as GraphQLSchema[]
    )
    SchemaBuilder.log('Schema successfully generated')
  }

  private buildHeaders() {
    let version: string
    try {
      version = require('./version.json').version
    } catch {
      version = 'local'
    }
    let userAgent = `graphql-scripts/${version}`

    if (process.env.CIRCLECI) {
      userAgent += `/${process.env.CIRCLE_PROJECT_REPONAME}/${process.env.CIRCLE_WORKFLOW_ID}`
    }
    return {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'user-agent': userAgent,
    }
  }

  private fetchRemoteSchemas = async () => {
    const schemas = await Promise.all(
      Object.keys(this.nonIgnoredEndpoints).map((remoteURL) =>
        this.fetchRemoteSchema(remoteURL)
      )
    )

    return schemas.filter((el) => !!el) as GraphQLSchema[]
  }

  private fetchRemoteSchema = async (endpointName: string) => {
    const url = this.nonIgnoredEndpoints[endpointName]

    try {
      const schema = await loadSchema(url, {
        loaders: [new UrlLoader()],
        headers: this.buildHeaders(),
      })
      SchemaBuilder.log(`Remote schema found : ${url}`)
      return schema
    } catch (error) {
      SchemaBuilder.log(`Error on remote schema ${endpointName}\n`)
      SchemaBuilder.log(`${error}`)
      return null
    }
  }

  private getLocalSchemas = async () => {
    const localSchemasPath = glob.sync('**/*.graphql', {
      absolute: false,
      ignore: this.ignoredLocalFilePattern,
    })

    let mergedSchemaString = ''

    for (const path of localSchemasPath) {
      if (
        path.includes('node_modules')
      ) {
        continue
      }

      const schemaString = await fs.readFile(path, 'utf8')

      let isGeneratedByGraphQLScripts: boolean
      try {
        const schema = await loadSchema(schemaString, { loaders: [] })
        isGeneratedByGraphQLScripts = !!schema.getType('APIEndpoints')
      } catch {
        isGeneratedByGraphQLScripts = false
      }

      if (!isGeneratedByGraphQLScripts) {
        SchemaBuilder.log(`Local schema found : ${path}`)
        mergedSchemaString += `${schemaString}\n`
      } else {
        SchemaBuilder.log(`Matching local schema ignored : ${path}`)
      }
    }

    if (mergedSchemaString.length === 0) {
      SchemaBuilder.log('No local schema found')
      return null
    }

    return loadSchema(mergedSchemaString, {
      loaders: [],
    })
  }

  private buildAPIDirective = async () => {
    return loadSchema(
      `
enum APIEndpoints {
  ${Object.keys(this.endpoints).join('\n')}
}
enum ErrorLevels {
  ${Object.keys(ErrorLevels).join('\n')}
}
directive @api(name: APIEndpoints!, contextKey: String) on QUERY | MUTATION | SUBSCRIPTION
    `,
      {
        loaders: [],
      }
    )
  }

  private mergeSchemas = (schemas: GraphQLSchema[]) => {
    const mergedSchemas = mergeSchemas({
      schemas,
    })

    return fs.writeFile('./schema.graphql', printSchema(mergedSchemas))
  }
}

const buildSchema = async (options: {
  ignoreEndpoints?: string
  ignoreLocalFilePattern?: string
  env: Envs
}) => {
  const ignoredEndpoints = options.ignoreEndpoints
    ? options.ignoreEndpoints.split(',').map((el: string) => el.trim())
    : []


  const builder = new SchemaBuilder({
    endpoints: getEndpoints(options.env),
    ignoredEndpoints,
    ignoredLocalFilePattern: options.ignoreLocalFilePattern
  })

  await builder.run()
}

export default buildSchema

@david-vendel
Copy link
Author

Hello @jean9696 thank you very much. I won't study/use your solution immediately, because I already implemented the solution using Graphql codegen, as it seems to be the most up-to-date and maintained library. But surely someone else will find it useful.
My solution, if others are curious, is basically copied from this random public repository: https://github.com/UrsaMaritimus/orca/blob/main/libs/graphql/README.md
I actually like Graphql Generator because it auto-generates not only types, but also hooks for queries/mutations, so it seems my BE code became smaller and easier to write. What I didn't like is that I had to put all my graphql operations in one folder per BE service (see the link I attached), but hey, life is about change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants