-
Notifications
You must be signed in to change notification settings - Fork 14
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
Comments
Hi @justdvl, I can't provide you a nice example for that. We built our own script using |
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. |
@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 |
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. |
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):
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
The text was updated successfully, but these errors were encountered: