Skip to content

Instantly share code, notes, and snippets.

@zhenwenc
Last active November 3, 2021 22:48
Show Gist options
  • Save zhenwenc/09c9e78faefa4c35bdff6280a010ab81 to your computer and use it in GitHub Desktop.
Save zhenwenc/09c9e78faefa4c35bdff6280a010ab81 to your computer and use it in GitHub Desktop.
Generate Apollo API based on remote GraphQL schema merging with schemas for local state
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const https = require('https');
const yargs = require('yargs');
const fetch = require('node-fetch');
const { parse, execute, buildSchema } = require('graphql');
const { makeExecutableSchema } = require('graphql-tools');
const {
fileLoader,
mergeTypes,
mergeResolvers,
} = require('merge-graphql-schemas');
const {
printSchema,
printIntrospectionSchema,
buildClientSchema,
introspectionQuery,
} = require('graphql/utilities');
// Make sure unhandled errors in async code are propagated correctly
process.on('uncaughtException', error => {
console.error(error);
process.exit(1);
});
process.on('unhandledRejection', error => {
throw error;
});
async function fetchRemoteSchema(url, insecure) {
const agent =
/^https:\/\//i.test(url) && insecure
? new https.Agent({ rejectUnauthorized: false })
: undefined;
const body = JSON.stringify({ query: introspectionQuery });
const method = 'POST';
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
try {
const response = await fetch(url, {
agent,
method,
headers,
body,
});
const result = await response.json();
if (result.errors) {
throw new Error(`Errors in introspection query result: ${result.errors}`);
}
if (!result.data) {
throw new Error(
`No introspection query result data from: ${JSON.stringify(result)}`
);
}
return buildClientSchema(result.data);
} catch (e) {
throw new Error(`Error while fetching introspection query: ${e.message}`);
}
}
// Copy from graphql-js library, will be released in new version
// https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionFromSchema.js
async function introspectionFromSchema(schema /* GraphQLSchema */) {
const queryAST = parse(introspectionQuery);
const result = await execute(schema, queryAST);
return result.data; /* IntrospectionQuery */
}
async function introspectSchema(remoteURL, clientURL, output, insecure) {
return fetchRemoteSchema(remoteURL, true)
.then(schema => {
const clientSchemas = fileLoader(clientURL);
const remoteSchema = printSchema(schema);
const typeDefs = mergeTypes([...clientSchemas, remoteSchema], {
all: true,
});
return makeExecutableSchema({ typeDefs });
})
.then(schema => {
return introspectionFromSchema(schema);
})
.then(introspection => {
const json = JSON.stringify(introspection, null, 2);
fs.writeFileSync(output, json);
})
.catch(err => {
console.error('Error while generating types for schema:', err);
});
}
const remoteSchema = 'https://<Your magic URL>/graphql';
const clientSchema = path.resolve('<Root Dir>', 'src/**/*.graphql');
const output = path.resolve('<Root Dir>', 'src/schema.json');
// Generate an introspection JSON format from remote GraphQL server merging
// with any local GraphQL schemas
introspectSchema(remoteSchema, clientSchema, output, true);
@ZenSoftware
Copy link

This was a life saver. Thank you very much for this!

@mvanlonden
Copy link

Thanks so much. Ran this through my linter and removed some unused vars

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const https = require('https');
const fetch = require('node-fetch');

const { parse, execute } = require('graphql');
const { makeExecutableSchema } = require('graphql-tools');
const { fileLoader, mergeTypes } = require('merge-graphql-schemas');
const { printSchema, buildClientSchema, introspectionQuery } = require('graphql/utilities');

// Make sure unhandled errors in async code are propagated correctly
process.on('uncaughtException', error => {
  console.error(error);
  process.exit(1);
});
process.on('unhandledRejection', error => {
  throw error;
});

async function fetchRemoteSchema(url, insecure) {
  const agent =
    /^https:\/\//i.test(url) && insecure
      ? new https.Agent({ rejectUnauthorized: false })
      : undefined;
  const body = JSON.stringify({ query: introspectionQuery });
  const method = 'POST';
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json'
  };

  try {
    const response = await fetch(url, {
      agent,
      method,
      headers,
      body
    });
    const result = await response.json();

    if (result.errors) {
      throw new Error(`Errors in introspection query result: ${result.errors}`);
    }
    if (!result.data) {
      throw new Error(`No introspection query result data from: ${JSON.stringify(result)}`);
    }
    return buildClientSchema(result.data);
  } catch (e) {
    throw new Error(`Error while fetching introspection query: ${e.message}`);
  }
}

// Copy from graphql-js library, will be released in new version
// https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionFromSchema.js
async function introspectionFromSchema(schema /* GraphQLSchema */) {
  const queryAST = parse(introspectionQuery);
  const result = await execute(schema, queryAST);
  return result.data; /* IntrospectionQuery */
}

async function introspectSchema(remoteURL, clientURL, output) {
  return fetchRemoteSchema(remoteURL, true)
    .then(schema => {
      const clientSchemas = fileLoader(clientURL);
      const remoteSchema = printSchema(schema);
      const typeDefs = mergeTypes([...clientSchemas, remoteSchema], {
        all: true
      });
      return makeExecutableSchema({ typeDefs });
    })
    .then(schema => {
      return introspectionFromSchema(schema);
    })
    .then(introspection => {
      const json = JSON.stringify(introspection, null, 2);
      fs.writeFileSync(output, json);
    })
    .catch(err => {
      console.error('Error while generating types for schema:', err);
    });
}

const remoteSchema = 'http://localhost:8000/graphql';
const clientSchema = path.resolve('<Root Dir>', 'src/**/*.graphql');
const output = path.resolve('<Root Dir>', 'schema.json');

// Generate an introspection JSON format from remote GraphQL server merging
// with any local GraphQL schemas
introspectSchema(remoteSchema, clientSchema, output, true);

@michaelbromley
Copy link

Just a note if anyone is getting the error:

Error while generating types for schema: TypeError: Cannot convert undefined or null to object
    at Function.from (native)
    at toConsumableArray (..\node_modules\merge-graphql-schemas\dist\index.cjs.js:4241:18)

I am working with TypeScript, and my local queries are like this:

import gql from 'graphql-tag';

export const GET_IN_FLIGHT_REQUESTS = gql`
    query GetInFlightRequests {
        network @client {
            inFlightRequests
        }
    }
`;

To fix the error I had to change line 78 in the original gist to:

const clientSchemas = fileLoader(clientURL)
  .map(module => Object.values(module))
  .reduce((flattened, schemas) => flattened.concat(schemas), []);

to convert the array of object hashes into an array of schemas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment