Skip to content
Developer home
X3

Creating the node.js client application

  Less than to read

In this section, you will build a simple application using Node.js which will perform GraphQL queries on the Sage X3 on-premise configuration.

Initializing the application

Open a command prompt and enter the following commands:


mkdir x3-graphql-tuto

cd x3-graphql-tuto

npm init -y

npm install typescript @types/node rimraf --save-dev

mkdir src

Configuring TypeScript

The client application is written in TypeScript and requires some additional configuration steps:

  1. Generate the tsconfig.json file:

     npx tsc --init --rootDir src --outDir build \
     --esModuleInterop --resolveJsonModule --target es2020 --lib es2020 \
     --module commonjs --allowJs true --noImplicitAny true
    
  2. Edit the package.json file and add the following lines in the script before the “test” entry:

     "start": "npm run build && node build/index.js",
     "build": "rimraf ./build && tsc",
    

Adding the required dependencies

The application requires some external dependencies to manipulate the JSON web tokens and to make HTTP requests.

npm install axios jsonwebtoken
npm install @types/jsonwebtoken --save-dev

The final package.json file should be like the following:

{
    "name": "x3-graphql-tuto",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "node build/index.js",
        "build": "rimraf ./build && tsc",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "@types/jsonwebtoken": "^8.5.1",
        "@types/node": "^14.14.41",
        "rimraf": "^3.0.2",
        "typescript": "^4.2.4"
    },
    "dependencies": {
        "axios": "^0.21.1",
        "jsonwebtoken": "^8.5.1"
    }
}

Editing the source code

index.ts

import { ConnectedAppOptions, getAuthToken } from './auth';
import { query } from './graphql';
 
const config = require('../config.json');
 
const [tokenName, endpoint, request] = process.argv.slice(2);
 
const usage = () => { console.log('\n\u{1F4E2} usage: npm start [token-name] [endpoint] [graphql-request]\n'); process.exit(1); }
const error = (msg:string) => console.error(`\u{274C} ${msg}`);
 
if (!tokenName || !request) {
	error('missing argument');
	usage();
}
 
const tokenInfo = config[tokenName] as ConnectedAppOptions | undefined;
if (!tokenInfo) {
	error(`bad token name: ${tokenName}, check your config.json file!`);
	usage();
}
 
(async () => {
	try {
		const token = getAuthToken(tokenInfo!);
		const result = await query(tokenInfo!.url, endpoint, token, request);
		console.log(JSON.stringify(result.data, null, 2));
	} catch(e){
		error(e.message);
	}
})();

auth.ts

import { sign,  } from 'jsonwebtoken';
 
interface JwtTokenInfo {
	iss: string;
	sub?: string;
	aud?: string;
	iat: number;
	exp: number;
};
 
export interface ConnectedAppOptions{
	clientId: string;
	secret: string;
	url: string;
	user:string;
	lifetime: number;
}
 
export function getAuthToken(options:ConnectedAppOptions, audience?:string): string {
	const {clientId,secret,user,lifetime} = options;
	// If the token is issued in the future, the server will return an error 21
	// In some cases, the time difference between computer might cause this issue,
	// so it may require to adjust the issued at (iat) value.
	const nowInSeconds = Math.floor(Date.now() / 1000) - 30;
	const expInSeconds = nowInSeconds + (lifetime || 300);
	const token:JwtTokenInfo = {
		iss: clientId,
		aud: audience || '',
		iat: nowInSeconds,
		exp: expInSeconds,
	};
	if (user) token.sub = user;
 
	return sign(token, secret);
}

graphql.ts

import * as https from 'https';
 
const axios = require('axios').create({
	httpsAgent: new https.Agent({
		// set this only in dev environment to work with self-signed certificates
		rejectUnauthorized: false,
	}),
});
 
export async function query(url:string, endpoint:string, token:string, request:string) : Promise<any> {
	return await axios.post(url, {
		query: request
	},{
		headers: {
			authorization: `Bearer ${token}`,
			'x-xtrem-endpoint': endpoint,
		},
	});
}

Adding a configuration file for the client application

This test application uses parameters stored in a config.json file to generate the token.

  1. Create the config.json file at the root of the application with the following template. Replace the values with the ones created during the previous steps.

     {
         "adc":{
             "clientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
             "secret": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
             "user": "adc",
             "lifetime":600,
             "url": "http://scmx3-dev-xxx.sagefr.adinternal.com:8124/xtrem/api"
         }
     }
    
  2. Build and run the application. In the following example, REPOSX3_REPOSX3 is the name of the connection endpoint. You can find its name in the Endpoints function (Administration > Administration > Endpoints > Endpoints).

     npm run build
     npm run start adc REPOSX3_REPOSX3 "{xtremX3System{country{query{edges{node{code}}}}}}"
    
  3. A response similar to the following example displays.

{
  "data": {
	"xtremX3System": {
		"country": {
			"query": {
				"edges": [
					{
						"node": {
						"code": "AD"
						}
					},
					{
						"node": {
						"code": "AE"							
						}
					},
					{
						"node": {
						"code": "AF"
						}
					},
					{
						"node": {
						"code": "AL"
						}
					},
					{
						"node": {
						"code": "AM"
						}
					},
					{
						"node": {
						"code": "AN"
						}
					},
					{
						"node": {
						"code": "AO"
						}
					},
					{
						"node": {
						"code": "AR"
						}
					},
					{
						"node": {
						"code": "AT"
						}
					},
					{
						"node": {
						"code": "AU"
						}
					},
					{
						"node": {
						"code": "AW"
						}
					},
					{
						"node": {
						"code": "AX"
						}
					},
					{
						"node": {
						"code": "AZ"
						}
					},
					{
						"node": {
						"code": "BA"
						}
					},
					{
						"node": {
						"code": "BB"
						}
					},
					{
						"node": {
						"code": "BD"
						}
					},
					{
						"node": {
						"code": "BE"
						}
					},
					{
						"node": {
						"code": "BF"
						}
					},
					{
						"node": {
						"code": "BG"
						}
					},
					{
						"node": {
						"code": "BH"
						}
					}
					]
				}
			}
		}
	},
	"extensions": {
			"diagnoses": []
	}
}