Example: Creating and Updating an API Using GitHub Actions (GQL Platform API)

This example shows how to use the GraphQL Platform API and GitHub Actions to automatically create and/or update an API on the Enterprise Hub when a developer commits a change to the OAS file. This automatically keeps your Enterprise Hub up to date with the latest API information.

This example uses GitHub Actions and NodeJS, but you can use the same techniques for other CI/CD tools (such as Jenkins) or programming languages (such as C#). This example also works from a local command line, which is helpful as you configure or customize the code.

Here is a video demo of this feature:

Configuring the example

The example uses a config.json object to configure the following properties:

  • oas_filename - The name of the OAS file that describes the API. If you are using GitHub Actions, you could instead extract this file name from the Action's metadata.
  • api_owner_id - The ID of the team or personal account that will own the API in the Enterprise Hub.
  • category - The category that the API should be assigned to. All APIs in the Enterprise Hub must be assigned a category. If the category is not specified in config.json, you can also use the info.x-category field in the OAS document. If this is not specified, the code sets the category to "Other".
  • admin_personal_key - This should not be set in the config.json file, but instead you should set an environment variable named ADMIN_PERSONAL_KEY. This can be set as a GitHub Actions secret. The value in config.json is only read if the environment variable is not set. The value is the personal account API key of a team member that owns the API. You can create an automation user and make that user an org admin, which automatically adds them to every team in the org.
  • api_owner_key - This should not be set in the config.json file, but instead you should set an environment variable named API_OWNER_KEY. This can be set as a GitHub Actions secret. The value in config.json is only read if the environment variable is not set. The value is the API key of a team that owns the the API (or a personal API key if it will be owned in a Personal Account). This key can be obtained from the Apps tab with the team selected from the dropdown.
  • major_version.create - Set to true if you would like to create a new major version of the API. A new major version will be created assuming the API already exists and the latest major version integer is higher than the previous major version integer. For example, changing the info.version value in the OAS file from v1.0.1 to v1.0.2 will not create a major version. If major_version.create is set to true and the version is changed from v1.0.1 to v2.0.1, a new major version will be created.
  • major_version.version_status - Set to "active" or "draft". If this is set to "active", API consumers will be able to select the newly created major version. This value is not used if major_version_create is false.
  • major_version.create_as_current - If set to true (and version_status is "active"), this will create the major version as the current version. This is the default version that API consumers will see. This value is not used if major_version_create is false.
  • rapidapi_host_gql - The X-RapidAPI-Host value taken from sample code when testing the GraphQL Platform API.
  • base_url_gql - The url value taken from sample code when testing the GraphQL Platform API.

The config.json file should be placed in the same directory as the Node.js code.

{
   "oas_filename": "openapi.json",
   "api_owner_id": "6028339",
   "category": "Data",
   "admin_personal_key": "use ADMIN_PERSONAL_KEY env variable",
   "api_owner_key": "use API_OWNER_KEY env variable",
   "major_version": {
    "create": true,
    "version_status": "active",
    "create_as_current": true
   },
   "rapidapi_host_gql": "[YOUR HOST FROM SAMPLE CODE]",
   "base_url_gql": "[YOUR URL FROM SAMPLE CODE]"
}

Example Node.js code

The following code creates an API if an API with the same name does not exist for the context specified in config.json as api_owner_id. If an API of the same name exists, the existing version of the API will be updated, unless major_version.create is set to true in the config.json object, and the new version major integer is higher than the previous integer (see major_version.create above).

// set specific Hub urls, keys, category and preferences in config.json, environment variables, and/or secrets
let config = require('./config.json');
const filename = config.oas_filename;

const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');

// recommend setting keys using secrets and/or env variables (not config.json)
// e.g. at Mac or Linux terminal: export ADMIN_PERSONAL_KEY=0a3...
const identityKey = process.env.ADMIN_PERSONAL_KEY || config.admin_personal_key;
const apiOwnerKey = process.env.API_OWNER_KEY || config.api_owner_key;
// console.log("API owner key:" + apiOwnerKey.slice(0,3) + "...");

let file = fs.readFileSync(filename);

file = JSON.parse(file);
const apiName = file.info.title;
const newVersionName = file.info.version;
// OPTIONALLY MODIFY OAS DOCUMENT HERE
if (config.category) file.info['x-category'] = config.category; // category in config.json takes precedence
if (!file.info['x-category']) file.info['x-category'] = 'Other';
file = JSON.stringify(file);

checkIfApiExists();

function checkIfApiExists() {
  const query = `
query apis($where: ApiWhereInput, $orderBy: ApiOrderByInput, $pagination: PaginationInput) {
  apis(where: $where, orderBy: $orderBy, pagination: $pagination) {
    edges {
      node {
        id
        name
        versions {
          id
          name
        }
      }
    }
  }
}`;

  const variables = {
    where: {
      name: [apiName],
      ownerId: [config['api_owner_id']],
    },
    pagination: {
      first: 5,
    },
  };

  const options = {
    method: 'POST',
    url: config.base_url_gql,
    headers: {
      'content-type': 'application/json',
      'x-rapidapi-identity-key': identityKey,
      'X-RapidAPI-Key': apiOwnerKey,
      'X-RapidAPI-Host': config.rapidapi_host_gql,
    },
    data: {variables,query},
  };

  axios
    .request(options)
    .then((response) => {
      // console.log(JSON.stringify(response.data));
      if (JSON.stringify(response.data).includes('error')) {
        process.exit(1);
      }
      if (JSON.stringify(response.data).includes(apiName)) {
        
        const oldVersionId = response.data.data.apis.edges[0].node.versions[0].id;
        const apiId = response.data.data.apis.edges[0].node.id;
        const versionNames = response.data.data.apis.edges[0].node.versions.map(version => version.name);
        console.log(`API exists with apiId=${apiId} and versionId=${oldVersionId}`);

        if (config.major_version?.create == true && isNewVersionInteger(newVersionName, versionNames)) {          
          createMajorVersion(apiId, newVersionName, versionNames);
        } else {
          updateApiMinorVersion(oldVersionId);
        } 
      } else {
        console.log(`API doesn't exist. Creating an API...`);
        addApi();
      }
    })
    .catch((error) => {
      console.error(error.response?.data || error);
      process.exit(1);
    });
}

function addApi() {
  const query = `
  mutation createApisFromRapidOas($creations: [ApiCreateFromRapidOasInput!]!) {
    createApisFromRapidOas(creations: $creations) {
      apiId
      trackingId
      warnings {
        type
        critical
        text
        info
      }
    }
  }`;

  const variables = {
    creations: {
      spec: null, 
    },
  };

  const formData = new FormData();
  formData.append('operations', JSON.stringify({ query, variables }));
  formData.append('map', '{"0":["variables.creations.spec"]}');
  formData.append('0', file, filename);

  const options = {
    method: 'POST',
    url: config.base_url_gql,
    headers: {
      ...formData.getHeaders(),
      'x-rapidapi-identity-key': identityKey,
      'X-RapidAPI-Key': apiOwnerKey,
      'X-RapidAPI-Host': config.rapidapi_host_gql,
    },
    data: formData,
  };

  axios
    .request(options)
    .then((response) => {
      // console.log(JSON.stringify(response.data));
      if (JSON.stringify(response.data).includes('error')) {
        process.exit(1);
      }
      console.log(`Created API ID=${response.data.data.createApisFromRapidOas[0].apiId}`)
    })
    .catch((error) => {
      console.error(error.response?.data || error);
      process.exit(1);
    });
}

function updateApiMinorVersion(versionID) {
  const query = `
  mutation updateApisFromRapidOas($updates: [ApiUpdateFromRapidOasInput!]!) {
    updateApisFromRapidOas(updates: $updates) {
      apiId
      trackingId
      warnings {
        type
        critical
        text
        info
      }
    }
  }`;
  const variables = {
    updates: {
      spec: file,
      apiVersionId: versionID,
    },
  };

  const formData = new FormData();
  formData.append('operations', JSON.stringify({ query, variables }));
  formData.append('map', '{"0":["variables.updates.spec"]}');
  formData.append('0', file, filename);

  const options = {
    method: 'POST',
    url: config.base_url_gql,
    headers: {
      ...formData.getHeaders(),
      'x-rapidapi-identity-key': identityKey,
      'X-RapidAPI-Key': apiOwnerKey,
      'X-RapidAPI-Host': config.rapidapi_host_gql,
    },
    data: formData,
  };

  axios
    .request(options)
    .then((response) => {
      //console.log(JSON.stringify(response.data));
      if (JSON.stringify(response.data).includes('error')) {
        process.exit(1);
      }
      console.log(`Updated API version ${versionID} from OAS file`);
    })
    .catch((error) => {
      console.error(error.response?.data || error);
      process.exit(1);
    });
}

function createMajorVersion(apiId, newVersionName, versionNames){
  if (versionNames.includes(newVersionName)) {
    console.log(`Error. Major version with name "${newVersionName}" was already created.`);
    process.exit(1);
  }
  const query = `
  mutation createApiVersions($apiVersions: [ApiVersionCreateInput!]!) {
    createApiVersions(apiVersions: $apiVersions) {
      id
    }
  }`;
  const variables = {
    'apiVersions': {
      'api': apiId,
      'name': newVersionName
    }
  };

  const options = {
    method: 'POST',
    url: config.base_url_gql,
    headers: {
      'content-type': 'application/json',
      'x-rapidapi-identity-key': identityKey,
      'X-RapidAPI-Key': apiOwnerKey,
      'X-RapidAPI-Host': config.rapidapi_host_gql,
    },
    data: {variables,query},
  };

  axios
    .request(options)
    .then((response) => {
      // console.log(JSON.stringify(response.data));
      if (JSON.stringify(response.data).includes('error')) {
        process.exit(1);
      }
      const versionId = response.data.data.createApiVersions[0].id;
      console.log(`Major version "${newVersionName}" created with versionId=${versionId}`);
      if (config.major_version?.version_status.toLowerCase() === 'active' || config.major_version?.create_as_current === true){
        updateApiMajorVersion(versionId, config.major_version?.version_status, config.major_version?.create_as_current);
      } else {
        updateApiMinorVersion(versionId);
      }
    })
    .catch((error) => {
      console.error(error.response?.data || error);
      process.exit(1);
    });
}

function updateApiMajorVersion(apiVersionId, versionStatus, current){
  const query = `
  mutation updateApiVersions($apiVersions: [ApiVersionUpdateInput!]!) {
    updateApiVersions(apiVersions: $apiVersions) {
      id
      api
      current
      name
      versionStatus
      apiVersionType
    }
  }`;
  const variables = {
    'apiVersions': {
      'apiVersionId': apiVersionId,
      'versionStatus': versionStatus,
      'current': current
    }
  };

  const options = {
    method: 'POST',
    url: config.base_url_gql,
    headers: {
      'content-type': 'application/json',
      'x-rapidapi-identity-key': identityKey,
      'X-RapidAPI-Key': apiOwnerKey,
      'X-RapidAPI-Host': config.rapidapi_host_gql,
    },
    data: {variables,query},
  };

  axios
    .request(options)
    .then((response) => {
      // console.log(JSON.stringify(response.data));
      if (JSON.stringify(response.data).includes('error')) {
        process.exit(1);
      }
      console.log(`Version status updated to versionStatus=${versionStatus} and current=${current}`);
      updateApiMinorVersion(apiVersionId);
    })
    .catch((error) => {
      console.error(error.response?.data || error);
      process.exit(1);
    });
}

function isNewVersionInteger(newVersionName, versionNames) {
  const versionIntegers = versionNames.map(version => parseInt(version.replace ( /[^\d.]/g, '' )));
  const largestInteger = Math.max(...versionIntegers);
  return parseInt(newVersionName.replace ( /[^\d.]/g, '' )) > largestInteger
}

Running the example locally

Place the config.json (which correct values for your Enterprise Hub) and create-or-update-api.js files in the same directory as the directory that contain's your API's OAS file. If you are setting the ADMIN_PERSONAL_KEY and API_OWNER_KEY as environment variables, do so in the command line before executing the script.

Executing the example code using the local command line.

Executing the example code using the local command line.

Running the example as a GitHub Action

Place the config.json (which correct values for your Enterprise Hub) and create-or-update-api.js files in your GitHub repository that contains the API's OAS file. It is recommended that you set ADMIN_PERSONAL_KEY and API_OWNER_KEY as GitHub Actions secrets (as shown in the workflow file below).

Place the following upload-oas-doc.yml file into your GitHub repository's .github/workflows folder.

name: Upload OAS Document
on:
  push:
    branches:    
      - 'main'
    paths:
      - 'openapi.json'
jobs:
  Upload-OAS-Document:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
      - run: npm install axios
      - run: node create-or-update-api
        env: 
          ADMIN_PERSONAL_KEY: ${{secrets.ADMIN_PERSONAL_KEY}}
          API_OWNER_KEY: ${{secrets.API_OWNER_KEY}}

The GitHub Action will execute every time a change is made to the openapi.json file (the OAS file for the API) and committed to the main repository branch. Expanding the "Run node create-or-update-api" step in the executed GitHub Action should show output similar to what you would see at the command line.

Viewing the output of a GitHub Action.

Viewing the output of a GitHub Action.

You are encouraged to customize this example for your specific business needs. Please reach out to your Rapid representative if you have any issues or questions.