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 inconfig.json
, you can also use theinfo.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 theconfig.json
file, but instead you should set an environment variable namedADMIN_PERSONAL_KEY
. This can be set as a GitHub Actions secret. The value inconfig.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 anautomation
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 theconfig.json
file, but instead you should set an environment variable namedAPI_OWNER_KEY
. This can be set as a GitHub Actions secret. The value inconfig.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 totrue
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 theinfo.version
value in the OAS file fromv1.0.1
tov1.0.2
will not create a major version. Ifmajor_version.create
is set totrue
and the version is changed fromv1.0.1
tov2.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 ifmajor_version_create
isfalse
.major_version.create_as_current
- If set totrue
(andversion_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 ifmajor_version_create
isfalse
.rapidapi_host_gql
- TheX-RapidAPI-Host
value taken from sample code when testing the GraphQL Platform API.base_url_gql
- Theurl
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.
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.
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.
Updated 9 months ago